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

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

पब्लिश होने की तारीख: 5 जुलाई, 2021

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

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

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

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

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

बड़ी मीडिया फ़ाइल डाउनलोड और सेव करना

प्रोग्रेसिव वेब ऐप्लिकेशन, आम तौर पर Cache API का इस्तेमाल करते हैं. इससे ऑफ़लाइन अनुभव देने के लिए ज़रूरी ऐसेट को डाउनलोड और सेव किया जा सकता है: दस्तावेज़, स्टाइलशीट, इमेज वगैरह.

यहां सर्विस वर्कर में Cache API का इस्तेमाल करने का एक बुनियादी उदाहरण दिया गया है:

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 ये काम नहीं करता:

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

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

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

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

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

इस मामले में, IndexedDB 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 डेटाबेस में सुरक्षित तरीके से सेव किया जाएगा. इसके बाद, अपने ऐप्लिकेशन में डाउनलोड फिर से शुरू करने के लिए एक बटन दिखाया जा सकता है. 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 के लिए कस्टम राइट बफ़र

IndexedDB डेटाबेस में dataChunk वैल्यू लिखने की प्रोसेस आसान होती है. ये वैल्यू पहले से ही 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 में मिलेंगी:

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

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