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

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

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

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

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

इस लेख में, हम 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',
      ]);
    })
  );
});

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

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

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

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

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

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

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

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

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

हमने अपने डेमो PWA में, फे़च एपीआई से जुड़ी कुछ दिलचस्प सुविधाएं बनाई हैं, जिन्हें हमने Kino के नाम से जाना है. सोर्स कोड सार्वजनिक है, इसलिए बेझिझक इसकी समीक्षा करें.

  • अधूरे डाउनलोड को रोकने और फिर से शुरू करने की सुविधा.
  • डेटाबेस में डेटा के छोटे-छोटे हिस्सों को सेव करने के लिए पसंद के मुताबिक बनाया गया बफ़र.

इन सुविधाओं को लागू करने का तरीका दिखाने से पहले, हम आपको एक बार फिर से बताएंगे कि आप फ़ाइलें डाउनलोड करने के लिए फे़च एपीआई का इस्तेमाल कैसे कर सकते हैं.

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

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

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

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

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

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