पहले से लोड किए गए ऑडियो और वीडियो के साथ तेज़ी से वीडियो चलाना

संसाधनों को पहले से लोड करके, मीडिया चलाने की प्रोसेस को तेज़ी से कैसे पूरा करें.

फ्रैंको ब्यूफ़ोर्ट
फ़्रैंकोइस ब्यूफ़ोर्ट

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

क्रेडिट: कॉपीराइट ब्लेंडर फ़ाउंडेशन | www.blender.org .

मैं मीडिया फ़ाइलों को पहले से लोड करने के तीन तरीकों के बारे में बताऊँगी. इनकी शुरुआत के फ़ायदे और नुकसान हैं.

यह बहुत बढ़िया है... लेकिन...
वीडियो प्रीलोड एट्रिब्यूट वेब सर्वर पर होस्ट की गई एक यूनीक फ़ाइल के लिए, इस्तेमाल में आसान. ब्राउज़र इस एट्रिब्यूट को पूरी तरह से अनदेखा कर सकते हैं.
एचटीएमएल दस्तावेज़ के पूरी तरह से लोड हो जाने और पार्स होने पर, संसाधन फ़ेच करना शुरू हो जाता है.
मीडिया सोर्स एक्सटेंशन (MSE), मीडिया एलिमेंट पर preload एट्रिब्यूट को अनदेखा कर देता है, क्योंकि MSE को मीडिया उपलब्ध कराने की ज़िम्मेदारी ऐप्लिकेशन की होती है.
लिंक पहले से लोड करना दस्तावेज़ के onload इवेंट को ब्लॉक किए बिना, ब्राउज़र को हर हाल में वीडियो रिसॉर्स के लिए अनुरोध करने के लिए मजबूर करता है. एचटीटीपी रेंज के अनुरोध साथ काम नहीं करते.
MSE और फ़ाइल सेगमेंट के साथ काम करता है. पूरे संसाधन फ़ेच करते समय सिर्फ़ छोटी मीडिया फ़ाइलों (5 एमबी से कम) के लिए इस्तेमाल किया जाना चाहिए.
मैन्युअल बफ़रिंग की सुविधा पूरा कंट्रोल गड़बड़ियों को ठीक करना, वेबसाइट की ज़िम्मेदारी है.

वीडियो प्रीलोड एट्रिब्यूट

अगर वीडियो का सोर्स, वेब सर्वर पर होस्ट की गई एक यूनीक फ़ाइल है, तो ब्राउज़र को यह संकेत देने के लिए वीडियो preload एट्रिब्यूट का इस्तेमाल किया जा सकता है कि प्रीलोड करने के लिए कितनी जानकारी या कॉन्टेंट है. इसका मतलब है कि मीडिया सोर्स एक्सटेंशन (MSE), preload के साथ काम नहीं करता है.

रिसॉर्स फ़ेच करने की प्रोसेस सिर्फ़ तब शुरू होगी, जब शुरुआती एचटीएमएल दस्तावेज़ पूरी तरह से लोड और पार्स हो जाए (जैसे कि DOMContentLoaded इवेंट चालू हो गया हो). साथ ही, रिसॉर्स को असल में फ़ेच किए जाने के बाद ही, अलग-अलग load इवेंट ट्रिगर होगा.

preload एट्रिब्यूट को metadata पर सेट करने से यह पता चलता है कि उपयोगकर्ता को वीडियो की ज़रूरत नहीं है. हालांकि, इसके मेटाडेटा (डाइमेंशन, ट्रैक की सूची, कुल समय वगैरह) को फ़ेच करना ज़रूरी है. ध्यान दें कि Chrome 64 में, preload की डिफ़ॉल्ट वैल्यू metadata है. (पहले यह auto था).

<video id="video" preload="metadata" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

preload एट्रिब्यूट को auto पर सेट करने का मतलब है कि ब्राउज़र इतना डेटा कैश कर सकता है कि वीडियो चलाया जा सकता है. इसके लिए, आगे बफ़रिंग को रोकने की ज़रूरत नहीं है.

<video id="video" preload="auto" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

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

  • डेटा बचाने की सेटिंग चालू होने पर, Chrome preload की वैल्यू को हर हाल में none पर सेट कर देता है.
  • Android 4.3 में, किसी Android गड़बड़ी की वजह से Chrome, preload की वैल्यू को हर हाल में none पर सेट कर देता है.
  • मोबाइल इंटरनेट कनेक्शन (2G, 3G, और 4G) पर Chrome, preload की वैल्यू को ज़बरदस्ती metadata पर सेट करता है.

सलाह

अगर आपकी वेबसाइट में एक ही डोमेन पर कई वीडियो रिसॉर्स हैं, तो हमारा सुझाव है कि आप preload की वैल्यू को metadata पर सेट करें या poster एट्रिब्यूट को तय करके, preload को none पर सेट करें. इस तरह, एक ही डोमेन (एचटीटीपी 1.1 स्पेसिफ़िकेशन के मुताबिक 6) ज़्यादा से ज़्यादा एचटीटीपी कनेक्शन नहीं जोड़े जाएंगे. इससे रिसॉर्स लोड होने रुक सकते हैं. ध्यान दें कि अगर वीडियो आपके मुख्य उपयोगकर्ता अनुभव का हिस्सा नहीं हैं, तो इससे पेज स्पीड भी बेहतर हो सकती है.

जैसा कि अन्य लेखों में बताया गया है, लिंक प्रीलोड एक डिक्लेरेटिव फे़च है. इसकी मदद से, ब्राउज़र को load इवेंट को ब्लॉक किए बिना और पेज डाउनलोड होने के दौरान, किसी संसाधन के लिए अनुरोध करने के लिए मजबूर करना पड़ता है. <link rel="preload"> के ज़रिए लोड किए गए रिसॉर्स, ब्राउज़र में स्थानीय तौर पर सेव किए जाते हैं और तब तक असरदार तरीके से काम नहीं करते, जब तक कि उनके बारे में DOM, JavaScript या सीएसएस में साफ़ तौर पर जानकारी न दी गई हो.

प्रीलोड, प्रीफ़ेच से अलग होता है, क्योंकि यह मौजूदा नेविगेशन पर फ़ोकस करता है. साथ ही, टाइप (स्क्रिप्ट, स्टाइल, फ़ॉन्ट, वीडियो, ऑडियो वगैरह) के हिसाब से प्राथमिकता वाले रिसॉर्स को फ़ेच करता है. इसका इस्तेमाल मौजूदा सेशन के लिए, ब्राउज़र की कैश मेमोरी को गर्म करने के लिए किया जाना चाहिए.

पूरा वीडियो पहले से लोड करें

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

<link rel="preload" as="video" href="https://cdn.com/small-file.mp4">

<video id="video" controls></video>

<script>
  // Later on, after some condition has been met, set video source to the
  // preloaded video URL.
  video.src = 'https://cdn.com/small-file.mp4';
  video.play().then(() => {
    // If preloaded video URL was already cached, playback started immediately.
  });
</script>

इस उदाहरण में दिया गया वीडियो एलिमेंट, पहले से लोड किए गए रिसॉर्स का इस्तेमाल करेगा. इसलिए, as प्रीलोड लिंक की वैल्यू video है. अगर यह कोई ऑडियो एलिमेंट होता, तो यह as="audio" होता.

पहला सेगमेंट पहले से लोड करें

नीचे दिए गए उदाहरण में, <link rel="preload"> की मदद से वीडियो के पहले सेगमेंट को पहले से लोड करने और उसे मीडिया सोर्स एक्सटेंशन के साथ इस्तेमाल करने का तरीका बताया गया है. अगर आपको MSE JavaScript API के बारे में नहीं पता है, तो MSE की बुनियादी बातें देखें.

आसानी के लिए, मान लें कि पूरे वीडियो को file_1.webm, file_2.webm, file_3.webm वगैरह जैसी छोटी फ़ाइलों में बांटा गया है.

<link rel="preload" as="fetch" href="https://cdn.com/file_1.webm">

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // If video is preloaded already, fetch will return immediately a response
    // from the browser cache (memory cache). Otherwise, it will perform a
    // regular network fetch.
    fetch('https://cdn.com/file_1.webm')
    .then(response => response.arrayBuffer())
    .then(data => {
      // Append the data into the new sourceBuffer.
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch file_2.webm when user starts playing video.
    })
    .catch(error => {
      // TODO: Show "Video is not available" message to user.
    });
  }
</script>

सपोर्ट करें

नीचे दिए गए स्निपेट की मदद से, यह पता लगाया जा सकता है कि <link rel=preload> के लिए अलग-अलग तरह के as काम करते हैं या नहीं:

function preloadFullVideoSupported() {
  const link = document.createElement('link');
  link.as = 'video';
  return (link.as === 'video');
}

function preloadFirstSegmentSupported() {
  const link = document.createElement('link');
  link.as = 'fetch';
  return (link.as === 'fetch');
}

मैन्युअल बफ़रिंग

कैश एपीआई और सर्विस वर्कर के बारे में ज़्यादा जानने से पहले, जानते हैं कि MSE के साथ वीडियो को मैन्युअल तरीके से कैसे बफ़र किया जाता है. नीचे दिया गया उदाहरण यह मानता है कि आपका वेब सर्वर, एचटीटीपी Range वाले अनुरोधों के साथ काम करता है. हालांकि, यह फ़ाइल सेगमेंट के जैसा ही होगा. ध्यान रखें कि इसे मैनेज करने के लिए Google की ShakaPlayer, JW Player, और Video.js जैसी कुछ मिडलवेयर लाइब्रेरी बनाई गई हैं.

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // Fetch beginning of the video by setting the Range HTTP request header.
    fetch('file.webm', { headers: { range: 'bytes=0-567139' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      sourceBuffer.appendBuffer(data);
      sourceBuffer.addEventListener('updateend', updateEnd, { once: true });
    });
  }

  function updateEnd() {
    // Video is now ready to play!
    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);

    // Fetch the next segment of video when user starts playing the video.
    video.addEventListener('playing', fetchNextSegment, { once: true });
  }

  function fetchNextSegment() {
    fetch('file.webm', { headers: { range: 'bytes=567140-1196488' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      const sourceBuffer = mediaSource.sourceBuffers[0];
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch further segment and append it.
    });
  }
</script>

ज़रूरी बातें

मीडिया बफ़र करने की सुविधा का कंट्रोल अब आपके पास है. मेरा सुझाव है कि पेजों को पहले से लोड करने के बारे में सोचते समय, डिवाइस की बैटरी लेवल, "डेटा बचाने वाला मोड", और नेटवर्क की जानकारी को ध्यान में रखें.

बैटरी जागरूकता

वीडियो को पहले से लोड करने के बारे में सोचने से पहले उपयोगकर्ताओं के डिवाइस के बैटरी लेवल को ध्यान में रखें. इससे पावर लेवल कम होने पर बैटरी लाइफ़ बरकरार रहेगी.

डिवाइस की बैटरी खत्म होने पर, कम रिज़ॉल्यूशन वाले वीडियो को पहले से लोड करने की सुविधा बंद करें या कम से कम वीडियो पहले से लोड करें.

if ('getBattery' in navigator) {
  navigator.getBattery()
  .then(battery => {
    // If battery is charging or battery level is high enough
    if (battery.charging || battery.level > 0.15) {
      // TODO: Preload the first segment of a video.
    }
  });
}

"डेटा बचाने की सेटिंग" का पता लगाएं

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

ज़्यादा जानने के लिए, Save-Data के साथ तेज़ और कम रफ़्तार वाले ऐप्लिकेशन डिलीवर करना देखें.

नेटवर्क की जानकारी के आधार पर स्मार्ट तरीके से लोड करना

पेजों को पहले से लोड करने से पहले, navigator.connection.type को देखा जा सकता है. जब इसे cellular पर सेट किया जाता है, तब पेजों को पहले से लोड होने से रोका जा सकता है. साथ ही, उपयोगकर्ताओं को सलाह दी जा सकती है कि उनका मोबाइल नेटवर्क ऑपरेटर, शायद बैंडविड्थ के हिसाब से शुल्क ले रहा हो. साथ ही, कैश मेमोरी में सेव किए गए पुराने कॉन्टेंट का अपने-आप चलने वाला वीडियो ही चालू करें.

if ('connection' in navigator) {
  if (navigator.connection.type == 'cellular') {
    // TODO: Prompt user before preloading video
  } else {
    // TODO: Preload the first segment of a video.
  }
}

नेटवर्क में हुए बदलावों पर प्रतिक्रिया देने का तरीका जानने के लिए, नेटवर्क की जानकारी का सैंपल देखें.

कई पहले सेगमेंट को प्री-कैश करें

अब अगर मैं इस बात का पता लगाए बिना कुछ मीडिया कॉन्टेंट को अनुमान के हिसाब से पहले से लोड करना चाहूं कि उपयोगकर्ता कौनसा मीडिया चुनेगा, तो क्या होगा? अगर उपयोगकर्ता किसी ऐसे वेब पेज पर है जिसमें 10 वीडियो हैं, तो हो सकता है कि हमारे पास हर सेगमेंट फ़ाइल को फ़ेच करने के लिए काफ़ी मेमोरी हो. हालांकि, हमें पक्के तौर पर 10 छिपे हुए <video> एलिमेंट और 10 MediaSource ऑब्जेक्ट नहीं बनाने चाहिए और वह डेटा फ़ीड करना शुरू नहीं करना चाहिए.

नीचे दिए गए दो हिस्सों में दिए गए उदाहरण में, बेहतरीन और इस्तेमाल में आसान कैश एपीआई का इस्तेमाल करके, वीडियो के पहले सेगमेंट को प्री-कैश करने का तरीका बताया गया है. ध्यान दें कि IndexedDB के साथ भी कुछ ऐसा ही किया जा सकता है. हम अभी सर्विस वर्कर का इस्तेमाल नहीं कर रहे हैं, क्योंकि कैश एपीआई को भी window ऑब्जेक्ट से ऐक्सेस किया जा सकता है.

फ़ेच और कैश मेमोरी

const videoFileUrls = [
  'bat_video_file_1.webm',
  'cow_video_file_1.webm',
  'dog_video_file_1.webm',
  'fox_video_file_1.webm',
];

// Let's create a video pre-cache and store all first segments of videos inside.
window.caches.open('video-pre-cache')
.then(cache => Promise.all(videoFileUrls.map(videoFileUrl => fetchAndCache(videoFileUrl, cache))));

function fetchAndCache(videoFileUrl, cache) {
  // Check first if video is in the cache.
  return cache.match(videoFileUrl)
  .then(cacheResponse => {
    // Let's return cached response if video is already in the cache.
    if (cacheResponse) {
      return cacheResponse;
    }
    // Otherwise, fetch the video from the network.
    return fetch(videoFileUrl)
    .then(networkResponse => {
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, networkResponse.clone());
      return networkResponse;
    });
  });
}

ध्यान दें कि अगर मुझे एचटीटीपी Range अनुरोधों का इस्तेमाल करना होता, तो मुझे Response ऑब्जेक्ट को मैन्युअल तरीके से फिर से बनाना पड़ता. ऐसा इसलिए, क्योंकि कैश एपीआई अभी Range रिस्पॉन्स के साथ काम नहीं करता. ध्यान रखें कि networkResponse.arrayBuffer() को कॉल करने से, रिस्पॉन्स का पूरा कॉन्टेंट एक साथ रेंडरर मेमोरी में फ़ेच हो जाता है. इसलिए, हो सकता है कि आप छोटी रेंज का इस्तेमाल करना चाहें.

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

    ...
    return fetch(videoFileUrl, { headers: { range: 'bytes=0-567139' } })
    .then(networkResponse => networkResponse.arrayBuffer())
    .then(data => {
      const response = new Response(data);
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, response.clone());
      return response;
    });

वीडियो चलाएं

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

जैसा कि पहले देखा जा चुका है, हम MSE का इस्तेमाल वीडियो के उस पहले सेगमेंट को वीडियो एलिमेंट में फ़ीड करने के लिए करते हैं.

function onPlayButtonClick(videoFileUrl) {
  video.load(); // Used to be able to play video later.

  window.caches.open('video-pre-cache')
  .then(cache => fetchAndCache(videoFileUrl, cache)) // Defined above.
  .then(response => response.arrayBuffer())
  .then(data => {
    const mediaSource = new MediaSource();
    video.src = URL.createObjectURL(mediaSource);
    mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

    function sourceOpen() {
      URL.revokeObjectURL(video.src);

      const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
      sourceBuffer.appendBuffer(data);

      video.play().then(() => {
        // TODO: Fetch the rest of the video when user starts playing video.
      });
    }
  });
}

सर्विस वर्कर की मदद से रेंज रिस्पॉन्स बनाना

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

इसलिए, मैं दिखाता हूं कि इन अनुरोधों को कैसे रोका जाता है और सर्विस वर्कर से पसंद के मुताबिक बनाया गया Range जवाब कैसे लौटाया जाता है.

addEventListener('fetch', event => {
  event.respondWith(loadFromCacheOrFetch(event.request));
});

function loadFromCacheOrFetch(request) {
  // Search through all available caches for this request.
  return caches.match(request)
  .then(response => {

    // Fetch from network if it's not already in the cache.
    if (!response) {
      return fetch(request);
      // Note that we may want to add the response to the cache and return
      // network response in parallel as well.
    }

    // Browser sends a HTTP Range request. Let's provide one reconstructed
    // manually from the cache.
    if (request.headers.has('range')) {
      return response.blob()
      .then(data => {

        // Get start position from Range request header.
        const pos = Number(/^bytes\=(\d+)\-/g.exec(request.headers.get('range'))[1]);
        const options = {
          status: 206,
          statusText: 'Partial Content',
          headers: response.headers
        }
        const slicedResponse = new Response(data.slice(pos), options);
        slicedResponse.setHeaders('Content-Range': 'bytes ' + pos + '-' +
            (data.size - 1) + '/' + data.size);
        slicedResponse.setHeaders('X-From-Cache': 'true');

        return slicedResponse;
      });
    }

    return response;
  }
}

ध्यान दें कि स्लाइस किए गए इस रिस्पॉन्स को फिर से बनाने के लिए मैंने response.blob() का इस्तेमाल किया है. इससे मुझे फ़ाइल को सिर्फ़ एक हैंडल मिलता है. वहीं, response.arrayBuffer() से पूरी फ़ाइल, रेंडरर मेमोरी में चली जाती है.

मेरे कस्टम X-From-Cache एचटीटीपी हेडर का इस्तेमाल, यह जानने के लिए किया जा सकता है कि यह अनुरोध कैश मेमोरी से आया है या नेटवर्क से. इसका इस्तेमाल, ShakaPlayer जैसे प्लेयर पर किया जा सकता है, ताकि वह जवाब देने में लगने वाले समय को नेटवर्क की स्पीड के हिसाब के तौर पर नज़रअंदाज़ कर सके.

Range अनुरोधों को मैनेज करने के तरीके के पूरे समाधान के लिए, आधिकारिक सैंपल मीडिया ऐप्लिकेशन और खास तौर पर इसकी ranged-response.js फ़ाइल देखें.