Pemutaran cepat dengan pramuat audio dan video

Cara mempercepat pemutaran media dengan secara aktif melakukan pramuat resource.

Prancis Beaufort
François Beaufort

Pemutaran yang lebih cepat dimulai berarti lebih banyak orang yang menonton video atau mendengarkan audio Anda. Itu adalah fakta yang telah diketahui. Dalam artikel ini, saya akan mempelajari teknik yang dapat Anda gunakan untuk mempercepat pemutaran audio dan video dengan secara aktif memuat resource terlebih dahulu, bergantung pada kasus penggunaan Anda.

Kredit: hak cipta Blender Foundation | www.blender.org .

Saya akan menjelaskan tiga metode pramuat file media, dimulai dengan kelebihan dan kontranya.

Bagus... Tapi...
Atribut pramuat video Mudah digunakan untuk file unik yang di-{i>host<i} di server web. Browser mungkin sepenuhnya mengabaikan atribut ini.
Pengambilan resource dimulai saat dokumen HTML telah dimuat dan diurai sepenuhnya.
Ekstensi Sumber Media (RKG) mengabaikan atribut preload pada elemen media karena aplikasi bertanggung jawab untuk menyediakan media ke MSE.
Pramuat link Memaksa browser untuk membuat permintaan resource video tanpa memblokir peristiwa onload dokumen. Permintaan Rentang HTTP tidak kompatibel.
Kompatibel dengan segmen file dan MSE. Sebaiknya hanya digunakan untuk file media kecil (<5 MB) saat mengambil resource secara penuh.
Buffering manual Kontrol penuh Penanganan error yang kompleks adalah tanggung jawab situs.

Atribut pramuat video

Jika sumber video adalah file unik yang dihosting di server web, sebaiknya gunakan atribut preload video untuk memberikan petunjuk ke browser tentang berapa banyak informasi atau konten yang harus di-pramuat. Ini berarti Ekstensi Sumber Media (MSE) tidak kompatibel dengan preload.

Pengambilan resource hanya akan dimulai saat dokumen HTML awal telah dimuat dan diuraikan sepenuhnya (misalnya, peristiwa DOMContentLoaded telah diaktifkan), sedangkan peristiwa load yang sangat berbeda akan diaktifkan saat resource benar-benar telah diambil.

Menyetel atribut preload ke metadata menunjukkan bahwa pengguna tidak diharapkan memerlukan video, tetapi pengambilan metadatanya (dimensi, daftar jalur, durasi, dan sebagainya) memang diinginkan. Perlu diketahui bahwa mulai Chrome 64, nilai default untuk preload adalah metadata. (Sebelumnya 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>

Menyetel atribut preload ke auto menunjukkan bahwa browser dapat menyimpan cukup data dalam cache sehingga pemutaran dapat diselesaikan tanpa perlu berhenti untuk buffering lebih lanjut.

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

Namun, ada beberapa peringatan. Karena ini hanyalah petunjuk, browser dapat sepenuhnya mengabaikan atribut preload. Pada saat penulisan ini, berikut adalah beberapa aturan yang diterapkan di Chrome:

  • Saat Penghemat Data diaktifkan, Chrome akan memaksa nilai preload ke none.
  • Di Android 4.3, Chrome memaksa nilai preload ke none karena Bug Android.
  • Pada koneksi seluler (2G, 3G, dan 4G), Chrome memaksa nilai preload ke metadata.

Tips

Jika situs Anda berisi banyak resource video pada domain yang sama, sebaiknya tetapkan nilai preload ke metadata atau tentukan atribut poster dan tetapkan preload ke none. Dengan demikian, Anda akan terhindar dari mencapai jumlah maksimum koneksi HTTP ke domain yang sama (6 menurut spesifikasi HTTP 1.1) yang dapat menghentikan pemuatan resource. Perhatikan bahwa hal ini juga dapat meningkatkan kecepatan halaman jika video bukan bagian dari pengalaman pengguna inti Anda.

Seperti yang dibahas dalam artikel lain, pramuat link adalah pengambilan deklaratif yang memungkinkan Anda memaksa browser membuat permintaan untuk resource tanpa memblokir peristiwa load dan saat halaman sedang didownload. Resource yang dimuat melalui <link rel="preload"> disimpan secara lokal di browser, dan secara efektif tidak aktif hingga resource tersebut secara eksplisit dirujuk di DOM, JavaScript, atau CSS.

Pramuat berbeda dengan pengambilan data karena berfokus pada navigasi saat ini dan mengambil resource dengan prioritas berdasarkan jenisnya (skrip, gaya, font, video, audio, dll.). Kunci ini harus digunakan untuk menyiapkan cache browser untuk sesi saat ini.

Pramuat video lengkap

Berikut ini cara melakukan pramuat video lengkap di situs Anda sehingga saat JavaScript meminta untuk mengambil konten video, konten tersebut akan dibaca dari cache karena resource mungkin telah di-cache oleh browser. Jika permintaan pramuat belum selesai, pengambilan jaringan reguler akan terjadi.

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

Karena resource pramuat akan digunakan oleh elemen video dalam contoh ini, nilai link pramuat as adalah video. Jika yang ada adalah elemen audio, elemen tersebut akan menjadi as="audio".

Pramuat segmen pertama

Contoh di bawah menunjukkan cara melakukan pramuat segmen pertama video dengan <link rel="preload"> dan menggunakannya dengan Ekstensi Sumber Media. Jika Anda belum terbiasa dengan MSE JavaScript API, lihat Dasar-dasar RKG.

Agar lebih praktis, anggaplah seluruh video telah dibagi menjadi file yang lebih kecil seperti file_1.webm, file_2.webm, file_3.webm, dll.

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

Dukungan

Anda dapat mendeteksi dukungan berbagai jenis as untuk <link rel=preload> dengan cuplikan di bawah:

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

Buffering manual

Sebelum kita mempelajari Cache API dan pekerja layanan, mari kita lihat cara melakukan buffering video secara manual dengan MSE. Contoh di bawah mengasumsikan bahwa server web Anda mendukung permintaan Range HTTP, tetapi ini akan sangat mirip dengan segmen file. Perlu diketahui bahwa beberapa library middleware seperti Google Shaka Player, JW Player, dan Video.js dibuat untuk menangani hal ini untuk Anda.

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

Pertimbangan

Karena Anda kini dapat mengontrol seluruh pengalaman buffering media, sebaiknya pertimbangkan level baterai perangkat, preferensi pengguna "Mode Hemat Data", dan informasi jaringan saat mempertimbangkan pramuat.

Kesadaran baterai

Perhitungkan level baterai perangkat pengguna sebelum berpikir untuk melakukan pramuat video. Tindakan ini akan menghemat masa pakai baterai saat level daya rendah.

Nonaktifkan pramuat atau setidaknya pramuat video beresolusi lebih rendah saat perangkat kehabisan daya baterai.

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

Mendeteksi "Penghemat Data"

Gunakan header permintaan petunjuk klien Save-Data untuk menayangkan aplikasi yang cepat dan ringan kepada pengguna yang telah mengaktifkan mode "penghematan data" di browser. Dengan mengidentifikasi header permintaan ini, aplikasi Anda dapat menyesuaikan dan memberikan pengalaman pengguna yang dioptimalkan kepada pengguna dengan biaya dan performa yang terbatas.

Lihat Mengirimkan Aplikasi Cepat dan Ringan dengan Hemat Data untuk mempelajari lebih lanjut.

Smart pemuatan berdasarkan informasi jaringan

Sebaiknya periksa navigator.connection.type sebelum melakukan pramuat. Jika ditetapkan ke cellular, Anda dapat mencegah pramuat dan memberi tahu pengguna bahwa operator jaringan seluler mereka mungkin mengenakan biaya untuk bandwidth tersebut, dan hanya memulai pemutaran otomatis konten yang disimpan dalam cache sebelumnya.

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

Lihat contoh Informasi Jaringan untuk mempelajari cara bereaksi juga terhadap perubahan jaringan.

Melakukan pra-cache beberapa segmen pertama

Bagaimana jika saya ingin melakukan pramuat beberapa konten media tanpa mengetahui bagian media mana yang pada akhirnya akan dipilih pengguna? Jika pengguna berada di halaman web yang berisi 10 video, kita mungkin memiliki cukup memori untuk mengambil satu file segmen dari setiap file, tetapi kita tidak boleh membuat 10 elemen <video> tersembunyi dan 10 objek MediaSource dan mulai memasukkan data tersebut.

Contoh dua bagian di bawah ini menunjukkan cara melakukan pra-cache beberapa segmen pertama video menggunakan Cache API yang canggih dan mudah digunakan. Perhatikan bahwa hal serupa juga dapat dicapai dengan IndexedDB. Kami belum menggunakan pekerja layanan karena Cache API juga dapat diakses dari objek window.

Pengambilan dan cache

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

Perhatikan bahwa jika saya menggunakan permintaan Range HTTP, saya harus membuat ulang objek Response secara manual karena Cache API belum mendukung respons Range. Perhatikan bahwa memanggil networkResponse.arrayBuffer() akan mengambil seluruh konten respons sekaligus ke dalam memori perender, itulah sebabnya Anda mungkin ingin menggunakan rentang kecil.

Sebagai referensi, saya telah mengubah bagian dari contoh di atas untuk menyimpan permintaan Rentang HTTP ke precache video.

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

Putar video

Saat pengguna mengklik tombol putar, kita akan mengambil segmen pertama video yang tersedia di Cache API sehingga pemutaran akan segera dimulai jika tersedia. Jika tidak, kita akan mengambilnya dari jaringan. Perlu diingat bahwa browser dan pengguna dapat memutuskan untuk menghapus Cache.

Seperti yang terlihat sebelumnya, kami menggunakan MSE untuk memberikan feed ke segmen pertama video tersebut ke elemen video.

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

Membuat respons Range dengan pekerja layanan

Bagaimana jika Anda telah mengambil seluruh file video dan menyimpannya di Cache API? Saat browser mengirim permintaan Range HTTP, Anda tentu tidak ingin memasukkan seluruh video ke dalam memori perender karena Cache API belum mendukung respons Range belum.

Jadi, akan saya tunjukkan cara mencegat permintaan ini dan menampilkan respons Range yang disesuaikan dari pekerja layanan.

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

Penting untuk diperhatikan bahwa saya menggunakan response.blob() untuk membuat ulang respons yang diiris ini karena tindakan ini hanya memberi saya handle ke file, sementara response.arrayBuffer() membawa seluruh file ke dalam memori perender.

Header HTTP X-From-Cache kustom saya dapat digunakan untuk mengetahui apakah permintaan ini berasal dari cache atau dari jaringan. Ini dapat digunakan oleh pemain seperti ShakaPlayer untuk mengabaikan waktu respons sebagai indikator kecepatan jaringan.

Lihat Sample Media App resmi dan khususnya file ranged-response.js untuk solusi lengkap terkait cara menangani permintaan Range.