تطبيق ويب تقدّمي (PWA) مع البث بلا إنترنت

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

تطبيقات الويب التقدّمية توفّر الكثير من الميزات التي كانت محجوزة سابقًا لتطبيقات الويب التطبيقات على الويب. ومن أبرز الميزات المرتبطة تطبيقات الويب التقدّمية (PWA) هي تجربة بلا اتصال بالإنترنت.

والأفضل من ذلك أنها تجربة وسائط يتم بثها بلا اتصال بالإنترنت، وهي التحسين الذي يمكنك تقديمه للمستخدمين بعدة طرق مختلفة. ومع ذلك، يؤدي ذلك إلى حدوث مشكلة فريدة حقًا، إذ يمكن أن تكون ملفات الوسائط كبيرة جدًا. إذًا، قد تسأل:

  • كيف يمكنني تنزيل ملف فيديو كبير وتخزينه؟
  • وكيف يمكنني عرضه للمستخدم؟

في هذه المقالة، سنناقش إجابات هذه الأسئلة، بينما الإشارة إلى تطبيق الويب التقدّمي Kino التجريبي الذي صمّمناه والذي يقدّم لك أمثلة على كيفية تنفيذ تجربة وسائط يتم بثها بلا اتصال بالإنترنت بدون باستخدام أي أطر عمل وظيفية أو عرضية. في ما يلي الأمثلة للأغراض التعليمية في المقام الأول، لأنه في معظم الحالات يجب عليك أحد أُطر عمل الوسائط الحالية لتوفير هذه الميزات.

ما لم يكن لديك دراسة جدوى جيدة لتطوير مشروعك، يمكنك إنشاء تطبيق ويب تقدّمي (PWA) في البث بلا اتصال بالإنترنت بعض التحديات. ستتعرف في هذه المقالة على واجهات برمجة التطبيقات والتقنيات المستخدمة لتزويد المستخدمين بوسائط عالية الجودة غير متصلة بالإنترنت المستخدم.

تنزيل ملف وسائط كبير وتخزينه

تستخدم تطبيقات الويب التقدمية عادةً ذاكرة التخزين المؤقت لواجهة برمجة التطبيقات المناسبة لكلا عمليات التنزيل ونخزّن الأصول المطلوبة لتوفير التجربة بلا اتصال بالإنترنت، مثل المستندات وأوراق الأنماط والصور وغيرها.

في ما يلي مثال أساسي لاستخدام واجهة برمجة تطبيقات ذاكرة التخزين المؤقت داخل مشغّل الخدمات:

const cacheStorageName = 'v1';

this.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheStorageName).then(function(cache) {
      return cache.addAll([
        'index.html',
        'style.css',
        'scripts.js',

        // Don't do this.
        'very-large-video.mp4',
      ]);
    })
  );
});

على الرغم من أنّ المثال أعلاه يحقّق أداءً جيدًا من الناحية الفنية، فإنّ استخدام واجهة برمجة تطبيقات ذاكرة التخزين المؤقت له العديد من القيود التي تجعل استخدامه مع الملفات الكبيرة غير عملي.

على سبيل المثال، لا تفعل واجهة برمجة التطبيقات Cache API ما يلي:

  • تتيح لك إيقاف عمليات التنزيل مؤقتًا واستئنافها بسهولة
  • تتيح لك تتبع تقدم التنزيلات
  • توفير طريقة للرد بشكلٍ صحيح على طلبات نطاق HTTP

تشكّل كل هذه المشاكل قيودًا خطيرة جدًا في أي تطبيق فيديو. لنراجع بعض الخيارات الأخرى التي قد تكون أكثر ملاءمة.

في الوقت الحاضر، تعد واجهة برمجة تطبيقات الجلب طريقة عبر المتصفحات للوصول عن بُعد بشكل غير متزامن الملفات. وفي حالة الاستخدام لدينا، فهي تتيح لك الوصول إلى ملفات الفيديو الكبيرة كتدفق وتخزينها بشكل تدريجي كمجموعات باستخدام طلب نطاق HTTP.

الآن وبعد أن أصبح بإمكانك قراءة أجزاء البيانات باستخدام Fetch API، عليك أيضًا وتخزينها. هناك احتمالات بأن تكون هناك مجموعة من البيانات الوصفية مرتبطة بالوسائط التي تستخدمها. مثل: الاسم والوصف وطول بيئة التشغيل والفئة وما إلى ذلك.

فأنت لا تخزن ملف وسائط واحد فقط، بل كائنًا منظمًا، وملف الوسائط هو إحدى خصائصه فقط.

في هذه الحالة، توفر IndexedDB API حلاً ممتازًا لتخزين بيانات الوسائط وبيانات التعريف. يمكنها استيعاب كميات ضخمة من البيانات الثنائية بسهولة، أيضًا فهارس تتيح لك إجراء عمليات بحث عن البيانات بسرعة كبيرة.

تنزيل ملفات الوسائط باستخدام واجهة برمجة تطبيقات الجلب

لقد أنشأنا بضع ميزات مثيرة للاهتمام حول واجهة برمجة تطبيقات الجلب في تطبيق الويب التقدّمي (PWA) التجريبي، والذي أطلقنا عليه اسم Kino، وهو رمز المصدر متاح للجميع، لذا لا تتردد في مراجعته.

  • إمكانية إيقاف عمليات التنزيل غير المكتملة مؤقتًا واستئنافها
  • مخزن مؤقت مخصص لتخزين مجموعات من البيانات في قاعدة البيانات.

قبل توضيح كيفية تنفيذ هذه الميزات، سنُجري أولاً ملخّص سريع حول كيفية استخدام واجهة Fetch API لتنزيل الملفات.

/**
 * Downloads a single file.
 *
 * @param {string} url URL of the file to be downloaded.
 */
async function downloadFile(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  do {
    const { done, dataChunk } = await reader.read();
    // Store the `dataChunk` to IndexedDB.
  } while (!done);
}

هل تريد ملاحظة أنّ await reader.read() في حلقة تكرار؟ هكذا ستتلقى أجزاء من البيانات الواردة من تدفق قابل للقراءة عند وصولها من الشبكة. ضع في اعتبارك كيف مفيدة للغاية وهي: أنه يمكنك البدء في معالجة بياناتك حتى قبل وصولها جميعًا من الشبكة.

استئناف عمليات التنزيل

عند إيقاف التنزيل مؤقتًا أو مقاطعته، ستبدأ أجزاء البيانات التي وصلت تخزينها بأمان في قاعدة بيانات IndexedDB. يمكنك بعد ذلك عرض زر لاستئناف التنزيل في طلبك. لأنّ خادم Kino التجريبي لتطبيق الويب التقدّمي (PWA) يتوافق مع طلبات نطاق HTTP واستئناف التنزيل أمر واضح إلى حد ما:

async downloadFile() {
  // this.currentFileMeta contains data from IndexedDB.
  const { bytesDownloaded, url, downloadUrl } = this.currentFileMeta;
  const fetchOpts = {};

  // If we already have some data downloaded,
  // request everything from that position on.
  if (bytesDownloaded) {
    fetchOpts.headers = {
      Range: `bytes=${bytesDownloaded}-`,
    };
  }

  const response = await fetch(downloadUrl, fetchOpts);
  const reader = response.body.getReader();

  let dataChunk;
  do {
    dataChunk = await reader.read();
    if (!dataChunk.done) this.buffer.add(dataChunk.value);
  } while (!dataChunk.done && !this.paused);
}

مخزن مؤقت مخصص للكتابة في IndexedDB

على الورق، تتم عملية كتابة قيم dataChunk في قاعدة بيانات IndexedDB. أمر بسيط. هذه القيم هي ArrayBuffer مثيل، وهي قابلة للتخزين. في IndexedDB مباشرةً، لذا يمكننا إنشاء كائن ذي شكل مناسب وتخزينها.

const dataItem = {
  url: fileUrl,
  rangeStart: dataStartByte,
  rangeEnd: dataEndByte,
  data: dataChunk,
}

// Name of the store that will hold your data.
const storeName = 'fileChunksStorage'

// `db` is an instance of `IDBDatabase`.
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const putRequest = store.put(data);

putRequest.onsuccess = () => { ... }

وأثناء تنفيذ هذه الطريقة، ستكتشف على الأرجح أنّ قاعدة البيانات المفهرسة هي أبطأ بكثير من عملية التنزيل. ولا يحدث ذلك لأن أداة IndexedDB تكتب بطيئة، ويرجع ذلك إلى أننا نضيف الكثير من النفقات العامة للمعاملات معاملة جديدة لكل مقطع بيانات نتلقاه من الشبكة.

ويمكن أن تكون المجموعات التي يتم تنزيلها صغيرة إلى حد ما ويمكن أن يطلقها البث خلال تتابع سريع. تحتاج إلى الحد من معدل عمليات الكتابة في IndexedDB. في جلسة المعمل، يستخدم Kino التجريبي تطبيق الويب التقدّمي Kino، ويمكن إجراء ذلك من خلال تنفيذ مخزن مؤقت للكتابة الوسيطة.

عند وصول أجزاء البيانات من الشبكة، فإننا نلحقها بالمورد الاحتياطي لدينا أولاً. في حال حذف عدم ملاءمة البيانات الواردة، فإننا نصفف المورد الاحتياطي الكامل في قاعدة البيانات ومحوه قبل إلحاق باقي البيانات. نتيجةً لذلك، فإن أداة IndexedDB الكتابة أقل تكرارًا، مما يؤدي إلى تحسين كتابة أدائه.

عرض ملف وسائط من مساحة تخزين بلا اتصال بالإنترنت

بعد تنزيل ملف الوسائط، قد تريد على عامل الخدمة تنفيذ ما يلي: من خلال IndexedDB بدلاً من جلبه من الشبكة.

/**
 * The main service worker fetch handler.
 *
 * @param {FetchEvent} event Fetch event.
 */
const fetchHandler = async (event) => {
  const getResponse = async () => {
    // Omitted Cache API code used to serve static assets.

    const videoResponse = await getVideoResponse(event);
    if (videoResponse) return videoResponse;

    // Fallback to network.
    return fetch(event.request);
  };
  event.respondWith(getResponse());
};
self.addEventListener('fetch', fetchHandler);

إذًا، ما هي الإجراءات المطلوبة منك في getVideoResponse()؟

  • تتوقع الطريقة event.respondWith() الكائن Response كمَعلمة.

  • تخبرنا الدالة الإنشائية Response() بوجود عدة أنواع من الكائنات التي لإنشاء مثيل كائن Response: Blob أو BufferSource ReadableStream وغير ذلك

  • نحن بحاجة إلى كائن لا يحتفظ بجميع بياناته في الذاكرة، لذلك ربما يرغبون في اختيار ReadableStream.

ولأننا نتعامل مع الملفات الكبيرة، وأردنا السماح للمتصفّحات فقط جزء من الملف الذي يحتاجون إليه حاليًا، كنّا بحاجة إلى تنفيذ بعض الدعم الأساسي لطلبات نطاق HTTP.

/**
 * Respond to a request to fetch offline video file and construct a response
 * stream.
 *
 * Includes support for `Range` requests.
 *
 * @param {Request} request  Request object.
 * @param {Object}  fileMeta File meta object.
 *
 * @returns {Response} Response object.
 */
const getVideoResponse = (request, fileMeta) => {
  const rangeRequest = request.headers.get('range') || '';
  const byteRanges = rangeRequest.match(/bytes=(?<from>[0-9]+)?-(?<to>[0-9]+)?/);

  // Using the optional chaining here to access properties of
  // possibly nullish objects.
  const rangeFrom = Number(byteRanges?.groups?.from || 0);
  const rangeTo = Number(byteRanges?.groups?.to || fileMeta.bytesTotal - 1);

  // Omitting implementation for brevity.
  const streamSource = {
     pull(controller) {
       // Read file data here and call `controller.enqueue`
       // with every retrieved chunk, then `controller.close`
       // once all data is read.
     }
  }
  const stream = new ReadableStream(streamSource);

  // Make sure to set proper headers when supporting range requests.
  const responseOpts = {
    status: rangeRequest ? 206 : 200,
    statusText: rangeRequest ? 'Partial Content' : 'OK',
    headers: {
      'Accept-Ranges': 'bytes',
      'Content-Length': rangeTo - rangeFrom + 1,
    },
  };
  if (rangeRequest) {
    responseOpts.headers['Content-Range'] = `bytes ${rangeFrom}-${rangeTo}/${fileMeta.bytesTotal}`;
  }
  const response = new Response(stream, responseOpts);
  return response;

لا تتردد في الاطّلاع على رمز مصدر مشغّل الخدمات المستند إلى تطبيق الويب التقدّمي Kino التجريبي للعثور على كيف نقرأ بيانات الملفات من IndexedDB وننشئ تدفقًا في تطبيق حقيقي.

اعتبارات أخرى

بعد أن تجاوزت العقبات الرئيسية أمامك، يمكنك الآن البدء في إضافة بعض ميزات جديدة في تطبيق الفيديو. فيما يلي بعض الأمثلة على الميزات التي ستجدها في تطبيق الويب التقدّمي Kino التجريبي:

  • دمج واجهة برمجة تطبيقات جلسات الوسائط الذي يسمح للمستخدمين بالتحكّم في الوسائط التشغيل باستخدام مفاتيح وسائط مخصّصة للأجهزة أو من إشعارات الوسائط ونوافذ منبثقة.
  • التخزين المؤقت لمواد العرض الأخرى المرتبطة بملفات الوسائط مثل الترجمة صور الملصقات باستخدام واجهة برمجة تطبيقات ذاكرة التخزين المؤقت القديمة الجيدة.
  • إتاحة تنزيل مجموعات بث الفيديو (DASH وHLS) داخل التطبيق لأنّ البث تعلن البيانات بشكل عام عن مصادر متعددة لمعدلات نقل البيانات المختلفة، يجب تحويل ملف البيان وتنزيل نسخة وسائط واحدة فقط قبل تخزينها لمشاهدته في وضع عدم الاتصال.

ستتعرّف تاليًا على التشغيل السريع باستخدام التحميل المسبق للصوت والفيديو.