پخش سریع با پیش بارگذاری صوتی و تصویری

چگونه با بارگذاری فعال منابع، پخش رسانه خود را تسریع کنید.

فرانسوا بوفور
François Beaufort

شروع پخش سریعتر به این معنی است که افراد بیشتری ویدیوی شما را تماشا می کنند یا به صدای شما گوش می دهند. این یک واقعیت شناخته شده است . در این مقاله، تکنیک‌هایی را بررسی می‌کنم که می‌توانید از آنها برای تسریع پخش صدا و ویدیو با پیش بارگذاری فعال منابع بسته به مورد استفاده خود استفاده کنید.

اعتبار: کپی رایت Blender Foundation | www.blender.org .

من سه روش برای بارگذاری پیش‌بارگذاری فایل‌های رسانه‌ای را شرح می‌دهم که از مزایا و معایب آنها شروع می‌شود.

عالیه... اما...
ویژگی پیش بارگذاری ویدیو ساده برای استفاده برای یک فایل منحصر به فرد میزبانی شده بر روی یک وب سرور. ممکن است مرورگرها این ویژگی را به طور کامل نادیده بگیرند.
واکشی منبع زمانی شروع می شود که سند HTML به طور کامل بارگیری و تجزیه شود.
برنامه افزودنی منبع رسانه (MSE) ویژگی preload عناصر رسانه را نادیده می گیرد زیرا برنامه مسئول ارائه رسانه به MSE است.
پیش بارگذاری لینک مرورگر را مجبور می‌کند تا بدون مسدود کردن رویداد onload سند، یک منبع ویدیویی درخواست کند. درخواست‌های محدوده HTTP سازگار نیستند.
سازگار با MSE و بخش های فایل. باید فقط برای فایل های رسانه ای کوچک (<5 مگابایت) هنگام واکشی منابع کامل استفاده شود.
بافر دستی کنترل کامل مسئولیت رسیدگی به خطاهای پیچیده بر عهده وب سایت است.

ویژگی پیش بارگذاری ویدیو

اگر منبع ویدیو یک فایل منحصر به فرد است که روی یک سرور وب میزبانی می شود، ممکن است بخواهید از ویژگی preload ویدیو برای ارائه راهنمایی به مرورگر درباره میزان اطلاعات یا محتوا برای پیش بارگذاری استفاده کنید. این بدان معناست که برنامه افزودنی منبع رسانه (MSE) با preload سازگار نیست.

واکشی منبع فقط زمانی شروع می شود که سند اولیه HTML به طور کامل بارگیری و تجزیه شده باشد (مثلاً رویداد 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، Chrome مقدار preload را به دلیل وجود اشکال در Android، به none می‌دهد.
  • در یک اتصال سلولی (2G، 3G، و 4G)، Chrome مقدار preload را به metadata مجبور می‌کند.

نکات

اگر وب‌سایت شما حاوی منابع ویدیویی زیادی در یک دامنه است، توصیه می‌کنم مقدار preload روی metadata تنظیم کنید یا ویژگی poster را تعریف کنید و preload روی none تنظیم کنید. به این ترتیب، از زدن حداکثر تعداد اتصالات HTTP به یک دامنه (6 مطابق با مشخصات HTTP 1.1) که می‌تواند بارگذاری منابع را متوقف کند، اجتناب می‌کنید. توجه داشته باشید که اگر ویدیوها بخشی از تجربه کاربری اصلی شما نباشند، ممکن است سرعت صفحه را نیز بهبود بخشد.

همانطور که در مقالات دیگر توضیح داده شد ، پیش بارگذاری پیوند یک واکشی اعلامی است که به شما امکان می دهد مرورگر را مجبور کنید تا درخواست یک منبع را بدون مسدود کردن رویداد load و در حالی که صفحه در حال بارگیری است، ارائه دهد. منابع بارگیری شده از طریق <link rel="preload"> به صورت محلی در مرورگر ذخیره می شوند و تا زمانی که به طور صریح در DOM، جاوا اسکریپت یا CSS به آنها ارجاع داده نشود، عملاً بی اثر هستند.

Preload با prefetch متفاوت است زیرا بر روی ناوبری فعلی تمرکز می کند و منابع را با اولویت بر اساس نوع آنها (اسکریپت، سبک، فونت، ویدئو، صدا و غیره) واکشی می کند. باید برای گرم کردن کش مرورگر برای جلسات جاری استفاده شود.

ویدیوی کامل را از قبل بارگیری کنید

در اینجا نحوه از پیش بارگذاری یک ویدیوی کامل در وب سایت خود آورده شده است تا زمانی که جاوا اسکریپت شما درخواست واکشی محتوای ویدیویی می کند، از حافظه پنهان خوانده شود زیرا ممکن است منبع قبلاً توسط مرورگر ذخیره شده باشد. اگر درخواست پیش‌بارگیری هنوز تمام نشده باشد، یک واکشی معمولی شبکه اتفاق می‌افتد.

<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 preload 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>

پشتیبانی کنید

می‌توانید پشتیبانی as مختلف را برای <link rel=preload> با قطعه‌های زیر شناسایی کنید:

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');
}

بافر دستی

قبل از اینکه به سراغ Cache API و Service Workers برویم، بیایید ببینیم که چگونه به صورت دستی یک ویدیو را با MSE بافر کنیم. مثال زیر فرض می‌کند که وب سرور شما از درخواست‌های Range HTTP پشتیبانی می‌کند، اما این با بخش‌های فایل تقریباً مشابه است. توجه داشته باشید که برخی از کتابخانه‌های میان‌افزار مانند Shaka Player Google ، 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.
    }
  });
}

شناسایی "Data-Saver"

از هدر درخواست اشاره مشتری 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 ایجاد کنیم و شروع به تغذیه آن داده ها کنیم.

مثال دو قسمتی زیر به شما نشان می‌دهد که چگونه چندین بخش اول ویدیو را با استفاده از API کش قدرتمند و با کاربرد آسان، پیش کش کنید. توجه داشته باشید که چیزی مشابه با IndexedDB نیز قابل دستیابی است. ما هنوز از Service Workers استفاده نمی‌کنیم، زیرا Cache API از شی 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 HTTP استفاده کنم، باید به صورت دستی یک شی Response را دوباره ایجاد کنم زیرا API Cache هنوز از پاسخ‌های Range پشتیبانی نمی‌کند. توجه داشته باشید که فراخوانی networkResponse.arrayBuffer() کل محتوای پاسخ را به یکباره در حافظه رندر واکشی می کند، به همین دلیل ممکن است بخواهید از محدوده های کوچک استفاده کنید.

برای مرجع، بخشی از مثال بالا را برای ذخیره درخواست‌های محدوده HTTP در پیش کش ویدیو تغییر داده‌ام.

    ...
    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;
    });

پخش ویدیو

وقتی کاربر روی دکمه پخش کلیک می‌کند، اولین بخش از ویدیوی موجود در Cache API را دریافت می‌کنیم تا در صورت وجود، پخش بلافاصله شروع شود. در غیر این صورت، ما به سادگی آن را از شبکه دریافت می کنیم. به خاطر داشته باشید که مرورگرها و کاربران ممکن است تصمیم بگیرند که حافظه پنهان را پاک کنند.

همانطور که قبلا دیده شد، ما از 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.
      });
    }
  });
}

پاسخ های محدوده را با یک سرویس دهنده ایجاد کنید

حالا اگر یک فایل ویدیوی کامل را واکشی کرده باشید و آن را در Cache API ذخیره کرده باشید چه؟ وقتی مرورگر یک درخواست Range HTTP ارسال می‌کند، مطمئناً نمی‌خواهید کل ویدیو را به حافظه رندر بیاورید زیرا API Cache هنوز از پاسخ‌های 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() کل فایل را به حافظه رندر می آورد.

از هدر HTTP X-From-Cache سفارشی من می توان برای دانستن اینکه آیا این درخواست از حافظه پنهان است یا از شبکه استفاده می شود. می تواند توسط بازیکنی مانند ShakaPlayer برای نادیده گرفتن زمان پاسخ به عنوان نشانگر سرعت شبکه استفاده شود.

نگاهی به برنامه رسمی Sample Media و به ویژه فایل ranged-response.js آن بیندازید تا راه حلی کامل برای نحوه رسیدگی به درخواست های Range داشته باشید.