অফলাইন স্ট্রিমিং সহ PWA

ডেরেক হারম্যান
Derek Herman
জারোস্লাভ পোলাকোভিচ
Jaroslav Polakovič

প্রগতিশীল ওয়েব অ্যাপগুলি ওয়েবে নেটিভ অ্যাপ্লিকেশনগুলির জন্য পূর্বে সংরক্ষিত অনেক বৈশিষ্ট্য নিয়ে আসে৷ PWA এর সাথে যুক্ত সবচেয়ে বিশিষ্ট বৈশিষ্ট্যগুলির মধ্যে একটি হল অফলাইন অভিজ্ঞতা।

এর চেয়েও ভালো হবে একটি অফলাইন স্ট্রিমিং মিডিয়া অভিজ্ঞতা, যা একটি বর্ধন যা আপনি আপনার ব্যবহারকারীদের বিভিন্ন উপায়ে অফার করতে পারেন। যাইহোক, এটি সত্যিই একটি অনন্য সমস্যা তৈরি করে — মিডিয়া ফাইলগুলি খুব বড় হতে পারে। তাই আপনি জিজ্ঞাসা করতে পারেন:

  • আমি কিভাবে একটি বড় ভিডিও ফাইল ডাউনলোড এবং সংরক্ষণ করব?
  • এবং কিভাবে আমি এটি ব্যবহারকারীকে পরিবেশন করব?

এই প্রবন্ধে আমরা এই প্রশ্নগুলির উত্তর নিয়ে আলোচনা করব, আমাদের তৈরি করা কিনো ডেমো PWA-এর উল্লেখ করার সময় যেটি আপনাকে ব্যবহারিক উদাহরণ প্রদান করে যে আপনি কীভাবে কোনও কার্যকরী বা উপস্থাপনামূলক কাঠামো ব্যবহার না করে একটি অফলাইন স্ট্রিমিং মিডিয়া অভিজ্ঞতা বাস্তবায়ন করতে পারেন। নিম্নলিখিত উদাহরণগুলি প্রধানত শিক্ষামূলক উদ্দেশ্যে, কারণ বেশিরভাগ ক্ষেত্রে এই বৈশিষ্ট্যগুলি প্রদান করতে আপনার সম্ভবত বিদ্যমান মিডিয়া ফ্রেমওয়ার্কগুলির একটি ব্যবহার করা উচিত।

আপনার নিজের বিকাশের জন্য আপনার কাছে একটি ভাল ব্যবসায়িক কেস না থাকলে, অফলাইন স্ট্রিমিং সহ একটি PWA তৈরির চ্যালেঞ্জ রয়েছে। এই নিবন্ধে আপনি ব্যবহারকারীদেরকে উচ্চ-মানের অফলাইন মিডিয়া অভিজ্ঞতা প্রদান করতে ব্যবহৃত API এবং কৌশলগুলি সম্পর্কে শিখবেন।

একটি বড় মিডিয়া ফাইল ডাউনলোড এবং সংরক্ষণ করা হচ্ছে

প্রগতিশীল ওয়েব অ্যাপগুলি সাধারণত অফলাইন অভিজ্ঞতা প্রদানের জন্য প্রয়োজনীয় সম্পদ ডাউনলোড এবং সংরক্ষণ করতে সুবিধাজনক ক্যাশে API ব্যবহার করে: নথি, স্টাইলশীট, ছবি এবং অন্যান্য।

এখানে একজন পরিষেবা কর্মীর মধ্যে ক্যাশে 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',
      ]);
    })
  );
});

যদিও উপরের উদাহরণটি প্রযুক্তিগতভাবে কাজ করে, ক্যাশে এপিআই ব্যবহার করার বেশ কয়েকটি সীমাবদ্ধতা রয়েছে যা বড় ফাইলগুলির সাথে এটি ব্যবহারকে অব্যবহারিক করে তোলে।

উদাহরণস্বরূপ, ক্যাশে API এটি করে না:

  • আপনাকে সহজে বিরতি এবং ডাউনলোড পুনরায় শুরু করার অনুমতি দেয়
  • আপনাকে ডাউনলোডের অগ্রগতি ট্র্যাক করতে দিন
  • HTTP পরিসরের অনুরোধে সঠিকভাবে সাড়া দেওয়ার একটি উপায় অফার করুন

এই সমস্ত সমস্যা যেকোনো ভিডিও অ্যাপ্লিকেশনের জন্য বেশ গুরুতর সীমাবদ্ধতা। আসুন কিছু অন্যান্য বিকল্প পর্যালোচনা করি যা আরও উপযুক্ত হতে পারে।

আজকাল, Fetch API হল দূরবর্তী ফাইলগুলিকে অ্যাসিঙ্ক্রোনাসভাবে অ্যাক্সেস করার একটি ক্রস-ব্রাউজার উপায়৷ আমাদের ব্যবহারের ক্ষেত্রে এটি আপনাকে একটি স্ট্রীম হিসাবে বড় ভিডিও ফাইলগুলি অ্যাক্সেস করতে এবং একটি HTTP রেঞ্জ অনুরোধ ব্যবহার করে খণ্ড হিসাবে ক্রমবর্ধমানভাবে সংরক্ষণ করতে দেয়।

এখন যেহেতু আপনি Fetch API এর সাথে ডেটার অংশগুলি পড়তে পারেন আপনাকে সেগুলি সংরক্ষণ করতে হবে। আপনার মিডিয়া ফাইলের সাথে একগুচ্ছ মেটাডেটা যুক্ত হওয়ার সম্ভাবনা আছে যেমন: নাম, বিবরণ, রানটাইম দৈর্ঘ্য, বিভাগ ইত্যাদি।

আপনি শুধুমাত্র একটি মিডিয়া ফাইল সংরক্ষণ করছেন না, আপনি একটি কাঠামোগত বস্তু সংরক্ষণ করছেন, এবং মিডিয়া ফাইলটি তার বৈশিষ্ট্যগুলির মধ্যে একটি মাত্র।

এই ক্ষেত্রে IndexedDB API মিডিয়া ডেটা এবং মেটাডেটা উভয় সঞ্চয় করার জন্য একটি চমৎকার সমাধান প্রদান করে। এটি সহজেই বিপুল পরিমাণ বাইনারি ডেটা ধারণ করতে পারে এবং এটি এমন সূচীও অফার করে যা আপনাকে খুব দ্রুত ডেটা লুকআপ করতে দেয়।

ফেচ API ব্যবহার করে মিডিয়া ফাইল ডাউনলোড করা হচ্ছে

আমরা আমাদের ডেমো PWA-তে Fetch API এর চারপাশে কয়েকটি আকর্ষণীয় বৈশিষ্ট্য তৈরি করেছি, যেটিকে আমরা Kino নাম দিয়েছি — সোর্স কোডটি সর্বজনীন তাই নির্দ্বিধায় এটি পর্যালোচনা করুন৷

  • অসম্পূর্ণ ডাউনলোডগুলি বিরতি এবং পুনরায় শুরু করার ক্ষমতা।
  • ডাটাবেসে ডেটার খণ্ডগুলি সংরক্ষণের জন্য একটি কাস্টম বাফার।

এই বৈশিষ্ট্যগুলি কীভাবে প্রয়োগ করা হয় তা দেখানোর আগে, আমরা প্রথমে ফাইলগুলি ডাউনলোড করতে কীভাবে ফেচ 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 ডাটাবেসে সংরক্ষণ করা হবে। তারপরে আপনি আপনার অ্যাপ্লিকেশনে একটি ডাউনলোড পুনরায় শুরু করতে একটি বোতাম প্রদর্শন করতে পারেন। কারণ কিনো ডেমো পিডব্লিউএ সার্ভার 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-এর জন্য কাস্টম লেখা বাফার

কাগজে, একটি 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 লেখার হার সীমিত করতে হবে। কিনো ডেমো 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 অবজেক্টকে ইনস্ট্যান্টিয়েট করতে ব্যবহার করতে পারি এমন বিভিন্ন ধরনের অবজেক্ট আছে: একটি 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;

আমরা কিভাবে IndexedDB থেকে ফাইল ডেটা পড়ছি এবং একটি বাস্তব অ্যাপ্লিকেশনে একটি স্ট্রীম তৈরি করছি তা জানতে নির্দ্বিধায় কিনো ডেমো PWA পরিষেবা কর্মী সোর্স কোডটি দেখুন।

অন্যান্য বিবেচ্য বিষয়

আপনার পথের প্রধান বাধাগুলির সাথে, আপনি এখন আপনার ভিডিও অ্যাপ্লিকেশনে কিছু চমৎকার বৈশিষ্ট্য যুক্ত করা শুরু করতে পারেন৷ এখানে কিছু বৈশিষ্ট্যের উদাহরণ রয়েছে যা আপনি কিনো ডেমো PWA-তে পাবেন:

  • মিডিয়া সেশন API ইন্টিগ্রেশন যা আপনার ব্যবহারকারীদের ডেডিকেটেড হার্ডওয়্যার মিডিয়া কী ব্যবহার করে বা মিডিয়া বিজ্ঞপ্তি পপআপ থেকে মিডিয়া প্লেব্যাক নিয়ন্ত্রণ করতে দেয়।
  • ভাল পুরানো ক্যাশে API ব্যবহার করে মিডিয়া ফাইলের মতো সাবটাইটেল এবং পোস্টার ইমেজের সাথে যুক্ত অন্যান্য সম্পদের ক্যাশিং।
  • অ্যাপের মধ্যে ভিডিও স্ট্রিম (DASH, HLS) ডাউনলোডের জন্য সমর্থন। যেহেতু স্ট্রিম ম্যানিফেস্টগুলি সাধারণত বিভিন্ন বিটরেটের একাধিক উত্স ঘোষণা করে, তাই আপনাকে ম্যানিফেস্ট ফাইলটি রূপান্তর করতে হবে এবং অফলাইন দেখার জন্য এটি সংরক্ষণ করার আগে শুধুমাত্র একটি মিডিয়া সংস্করণ ডাউনলোড করতে হবে৷

পরবর্তীতে, আপনি অডিও এবং ভিডিও প্রিলোড সহ দ্রুত প্লেব্যাক সম্পর্কে শিখবেন।