ऑफ़लाइन स्ट्रीमिंग की सुविधा वाला PWA

डेरिक हरमन
डेरिक हरमन
जारोस्लाव पोलाकोविच
जारोस्लाव पोलाकोविच

प्रोग्रेसिव वेब ऐप्लिकेशन की मदद से, नेटिव ऐप्लिकेशन के लिए पहले से रिज़र्व की गई कई सुविधाओं को वेब पर लाया जाता है. PWA की सबसे अहम सुविधाओं में से एक है, ऑफ़लाइन इस्तेमाल का अनुभव.

ऑफ़लाइन स्ट्रीमिंग का अनुभव बेहतर होगा. इसे अपने उपयोगकर्ताओं को अलग-अलग तरीकों से उपलब्ध कराया जा सकता है. हालांकि, इससे एक अनोखी समस्या पैदा होती है—मीडिया फ़ाइलें बहुत बड़ी हो सकती हैं. इसलिए, आपको यह पूछना पड़ सकता है:

  • मैं बड़ी वीडियो फ़ाइल को डाउनलोड और सेव कैसे करूं?
  • और मैं इसे उपयोगकर्ता के लिए कैसे दिखाऊं?

इस लेख में हम Kino डेमो PWA का रेफ़रंस देते हुए, इन सवालों के जवाबों पर चर्चा करेंगे. इस PWA को असल में, किसी भी फ़ंक्शन या प्रज़ेंटेशन वाले फ़्रेमवर्क का इस्तेमाल किए बिना, ऑफ़लाइन स्ट्रीमिंग की सुविधा को लागू करने के तरीके बताए गए हैं. यहां दिए गए उदाहरण मुख्य तौर पर शिक्षा के मकसद से दिए गए हैं. ऐसा इसलिए है, क्योंकि ज़्यादातर मामलों में आपको ये सुविधाएं देने के लिए, मौजूदा मीडिया फ़्रेमवर्क में से किसी एक का इस्तेमाल करना चाहिए.

ऑफ़लाइन स्ट्रीमिंग के साथ 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',
      ]);
    })
  );
});

हालांकि, ऊपर दिया गया उदाहरण तकनीकी रूप से काम करता है, फिर भी कैश एपीआई का इस्तेमाल करने में कई सीमाएं हैं, जो बड़ी फ़ाइलों के साथ इसके इस्तेमाल को संभव नहीं बनाती हैं.

उदाहरण के लिए, कैश एपीआई ये काम नहीं करता:

  • आपको डाउनलोड को आसानी से रोकने और फिर से शुरू करने की अनुमति देता है
  • डाउनलोड की प्रोग्रेस को ट्रैक करने में मदद मिलती है
  • एचटीटीपी रेंज के अनुरोधों का सही तरीके से जवाब दें

ये सभी समस्याएं किसी भी वीडियो ऐप्लिकेशन के लिए बहुत गंभीर सीमाएं हैं. आइए, कुछ ऐसे अन्य विकल्पों पर नज़र डालते हैं जो ज़्यादा सही हो सकते हैं.

आज के समय में, फ़ेच एपीआई एक क्रॉस-ब्राउज़र तरीका है. इसकी मदद से, रिमोट फ़ाइलों को एसिंक्रोनस तरीके से ऐक्सेस किया जा सकता है. हमारे इस्तेमाल के मामले में, यह आपको एचटीटीपी रेंज अनुरोध का इस्तेमाल करके, बड़ी वीडियो फ़ाइलों को स्ट्रीम के तौर पर ऐक्सेस करने और उन्हें डेटा को हिस्सों के तौर पर सेव करने की सुविधा देता है.

अब आप फ़ेच एपीआई से डेटा के कई हिस्सों को पढ़ सकते हैं. इसलिए, आपको उन्हें सेव भी करना होगा. हो सकता है कि आपकी मीडिया फ़ाइल से जुड़ा बहुत सारा मेटाडेटा हो, जैसे कि नाम, ब्यौरा, रनटाइम की अवधि, कैटगरी वगैरह.

आप सिर्फ़ एक मीडिया फ़ाइल सेव नहीं कर रहे हैं, आप एक स्ट्रक्चर्ड ऑब्जेक्ट को स्टोर कर रहे हैं, और मीडिया फ़ाइल इसकी सिर्फ़ एक प्रॉपर्टी है.

इस मामले में, IndexedDB API, मीडिया डेटा और मेटाडेटा, दोनों को सेव करने का बेहतरीन तरीका है. इसमें बहुत सारा बाइनरी डेटा आसानी से रखा जा सकता है. साथ ही, यह ऐसे इंडेक्स भी देता है जो आपको बहुत तेज़ी से डेटा खोजने की सुविधा देते हैं.

फे़च एपीआई की मदद से मीडिया फ़ाइलें डाउनलोड करना

हमने अपने डेमो PWA में, Fetch API की मदद से कुछ दिलचस्प सुविधाएं जोड़ी हैं. इन्हें हमने 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 सर्वर पर, एचटीटीपी रेंज के अनुरोध काम करते हैं. इसलिए, डाउनलोड को फिर से शुरू करना कुछ हद तक आसान है:

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 के लिखने की दर को सीमित करना होगा. ऐसा करने के लिए, Kino डेमो 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 को चुनना चाहेंगे.

इसके अलावा, क्योंकि हम बड़ी फ़ाइलों के साथ काम कर रहे हैं और चाहते थे कि ब्राउज़र, फ़ाइल के उस हिस्से का ही अनुरोध करें जिसकी उन्हें फ़िलहाल ज़रूरत है. इसलिए, हमें एचटीटीपी रेंज के अनुरोधों के लिए कुछ बुनियादी सुविधाएं लागू करनी होंगी.

/**
 * 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;

यह जानने के लिए कि हम IndexedDB से फ़ाइल डेटा को कैसे पढ़ रहे हैं और लाइव ऐप्लिकेशन में स्ट्रीम कैसे बना रहे हैं, Kino के PWA का सर्विस वर्कर सोर्स कोड देखें.

दूसरी ज़रूरी बातें

आने वाली मुख्य रुकावटों को दूर करने के बाद, अब आपके पास अपने वीडियो ऐप्लिकेशन में कुछ बेहतरीन सुविधाएं जोड़ने का विकल्प है. Kino डेमो PWA में मिलने वाली सुविधाओं के कुछ उदाहरण यहां दिए गए हैं:

  • मीडिया सेशन एपीआई इंटिग्रेशन की मदद से उपयोगकर्ता, हार्डवेयर मीडिया बटन या मीडिया सूचना पॉप-अप का इस्तेमाल करके मीडिया चलाने को कंट्रोल कर सकते हैं.
  • मीडिया फ़ाइलों से जुड़ी अन्य एसेट, जैसे कि सबटाइटल और पोस्टर इमेज को पुराने कैश एपीआई का इस्तेमाल करके कैश मेमोरी में सेव करना.
  • ऐप्लिकेशन में वीडियो स्ट्रीम (DASH, HLS) डाउनलोड करने की सुविधा मिलती है. स्ट्रीम मेनिफ़ेस्ट आम तौर पर अलग-अलग बिटरेट के कई स्रोतों के बारे में बताते हैं. इसलिए, आपको मेनिफ़ेस्ट फ़ाइल को बदलना होगा और ऑफ़लाइन देखने के लिए सेव करने से पहले, उसका सिर्फ़ एक मीडिया वर्शन डाउनलोड करना होगा.

आगे, आपको ऑडियो और वीडियो को पहले से लोड करके तेज़ी से चलाने की सुविधा के बारे में पता चलेगा.