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

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

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

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

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

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

अब जब आप 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 इंस्टेंस हैं, जिन्हें स्टोर किया जा सकता है इंडेक्स की जा सकती है, ताकि हम सिर्फ़ सही आकार का ऑब्जेक्ट बना सकें और उसे सेव कर सकता है.

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 के लिए डेमोग्राफ़िक (पीडब्ल्यूए) का इस्तेमाल करके, हम इंटरमीडियरी राइट बफ़र लागू करते हैं.

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

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