PWA با پخش آفلاین

درک هرمان
Derek Herman
یاروسلاو پولاکوویچ
Jaroslav Polakovič

برنامه‌های وب پیشرفته بسیاری از ویژگی‌ها را که قبلاً برای برنامه‌های کاربردی بومی رزرو شده بودند، به وب می‌آورند. یکی از برجسته ترین ویژگی های مرتبط با PWA ها، تجربه آفلاین است.

حتی بهتر از آن یک تجربه رسانه جریان آفلاین است، که پیشرفتی است که می توانید به چند روش مختلف به کاربران خود ارائه دهید. با این حال، این یک مشکل واقعا منحصر به فرد ایجاد می کند - فایل های رسانه ای می توانند بسیار بزرگ باشند. بنابراین ممکن است بپرسید:

  • چگونه یک فایل ویدیویی بزرگ را دانلود و ذخیره کنم؟
  • و چگونه آن را به کاربر ارائه دهم؟

در این مقاله، پاسخ به این سؤالات را مورد بحث قرار می‌دهیم، در حالی که به کینو دمو PWA که ساخته‌ایم اشاره می‌کنیم که نمونه‌های عملی را در اختیار شما قرار می‌دهد که چگونه می‌توانید یک تجربه رسانه جریان آفلاین را بدون استفاده از هیچ چارچوب عملکردی یا ارائه‌ای پیاده‌سازی کنید. مثال‌های زیر عمدتاً برای اهداف آموزشی هستند، زیرا در بیشتر موارد احتمالاً باید از یکی از Media Framework‌های موجود برای ارائه این ویژگی‌ها استفاده کنید.

ایجاد یک PWA با جریان آفلاین چالش‌های خود را دارد، مگر اینکه یک مورد تجاری خوب برای توسعه خود داشته باشید. در این مقاله با APIها و تکنیک های مورد استفاده برای ارائه تجربه رسانه آفلاین با کیفیت بالا به کاربران آشنا خواهید شد.

دانلود و ذخیره یک فایل رسانه ای بزرگ

برنامه‌های وب پیشرفته معمولاً از API مناسب حافظه پنهان برای بارگیری و ذخیره دارایی‌های مورد نیاز برای ارائه تجربه آفلاین استفاده می‌کنند: اسناد، شیوه نامه‌ها، تصاویر و موارد دیگر.

در اینجا یک مثال اساسی از استفاده از Cache API در یک Service Worker آورده شده است:

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 دارای چندین محدودیت است که استفاده از آن را با فایل های بزرگ غیر عملی می کند.

به عنوان مثال، Cache API این کار را نمی کند:

  • به شما این امکان را می دهد که به راحتی دانلودها را متوقف کرده و از سر بگیرید
  • به شما امکان می دهد پیشرفت دانلودها را پیگیری کنید
  • راهی برای پاسخگویی مناسب به درخواست‌های محدوده HTTP ارائه دهید

همه این مسائل محدودیت های بسیار جدی برای هر برنامه ویدیویی هستند. بیایید گزینه های دیگری را که ممکن است مناسب تر باشند بررسی کنیم.

امروزه Fetch API یک راه بین مرورگر برای دسترسی ناهمزمان به فایل های راه دور است. در مورد استفاده ما این امکان را به شما می دهد تا به فایل های ویدیویی بزرگ به صورت جریانی دسترسی داشته باشید و با استفاده از درخواست محدوده HTTP آنها را به صورت تکه ای ذخیره کنید.

اکنون که می توانید تکه های داده را با Fetch API بخوانید، باید آنها را نیز ذخیره کنید. این احتمال وجود دارد که مجموعه‌ای از ابرداده‌ها با فایل رسانه‌ای شما مرتبط باشند مانند: نام، توضیحات، طول زمان اجرا، دسته‌بندی و غیره.

شما فقط یک فایل رسانه ای را ذخیره نمی کنید، بلکه یک شی ساخت یافته را ذخیره می کنید و فایل رسانه ای تنها یکی از ویژگی های آن است.

در این مورد IndexedDB API یک راه حل عالی برای ذخیره داده های رسانه و ابرداده ارائه می دهد. این می تواند حجم عظیمی از داده های باینری را به راحتی نگهداری کند، و همچنین فهرست هایی را ارائه می دهد که به شما امکان می دهد جستجوی داده ها را بسیار سریع انجام دهید.

دانلود فایل های رسانه ای با استفاده از Fetch API

ما چند ویژگی جالب پیرامون Fetch 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 ذخیره می شوند. سپس می توانید دکمه ای را برای از سرگیری دانلود در برنامه خود نمایش دهید. از آنجایی که سرور PWA نمایشی Kino از درخواست های محدوده 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 کند هستند، به این دلیل است که ما با ایجاد یک تراکنش جدید برای هر تکه داده‌ای که از یک شبکه دریافت می‌کنیم، سربار تراکنش‌های زیادی را اضافه می‌کنیم.

تکه های دانلود شده می توانند نسبتاً کوچک باشند و می توانند توسط جریان به صورت متوالی منتشر شوند. شما باید سرعت نوشتن IndexedDB را محدود کنید. در کینو دمو PWA ما این کار را با پیاده سازی یک بافر نوشتن واسطه انجام می دهیم.

همانطور که تکه های داده از شبکه می رسند، ابتدا آنها را به بافر خود اضافه می کنیم. اگر داده های دریافتی جا نیفتند، بافر کامل را در پایگاه داده تخلیه می کنیم و قبل از اضافه کردن بقیه داده ها، آن را پاک می کنیم. در نتیجه نوشتن های 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;

به راحتی می توانید کد منبع سرویس PWA کینو را بررسی کنید تا دریابید که چگونه داده های فایل را از IndexedDB می خوانیم و یک جریان در یک برنامه واقعی ایجاد می کنیم.

ملاحظات دیگر

با وجود موانع اصلی، اکنون می توانید شروع به اضافه کردن برخی ویژگی های زیبا به برنامه ویدیویی خود کنید. در اینجا چند نمونه از ویژگی هایی که در کینو دمو PWA پیدا خواهید کرد آورده شده است:

  • ادغام Media Session API که به کاربران شما امکان می دهد پخش رسانه را با استفاده از کلیدهای رسانه سخت افزاری اختصاصی یا از پنجره های بازشوی اعلان رسانه کنترل کنند.
  • ذخیره سایر دارایی‌های مرتبط با فایل‌های رسانه مانند زیرنویس‌ها و تصاویر پوستر با استفاده از API خوب قدیمی Cache .
  • پشتیبانی از دانلود جریان های ویدیویی (DASH، HLS) در داخل برنامه. از آنجایی که مانیفست‌های جریان معمولاً چندین منبع با نرخ بیت متفاوت را اعلام می‌کنند، باید فایل مانیفست را تغییر دهید و فقط یک نسخه رسانه را قبل از ذخیره آن برای مشاهده آفلاین دانلود کنید.

در ادامه، با پخش سریع با پیش بارگذاری صوتی و تصویری آشنا خواهید شد.