Pemutaran cepat dengan pramuat audio dan video

Cara mempercepat pemutaran media dengan melakukan pramuat resource secara aktif.

François Beaufort
François Beaufort

Waktu mulai pemutaran yang lebih cepat berarti lebih banyak orang yang menonton video atau mendengarkan audio. Itu fakta yang sudah diketahui. Dalam artikel ini, saya akan mempelajari teknik yang dapat digunakan untuk mempercepat pemutaran audio dan video dengan melakukan pramuat resource bergantung pada kasus penggunaan Anda.

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

Saya akan menjelaskan tiga metode untuk melakukan pramuat file media, dimulai dengan profesionalnya dan kontra.

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

Atribut pramuat video

Jika sumber video adalah file unik yang di-{i>host<i} di server web, Anda mungkin ingin gunakan atribut preload video untuk memberikan petunjuk ke browser tentang bagaimana banyak informasi atau konten untuk dipramuat. Ini berarti Ekstensi Sumber Media (MSE) tidak kompatibel dengan preload.

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

Menyetel atribut preload ke metadata menunjukkan bahwa pengguna tidak mengharapkan kebutuhan video, tetapi kemudian mengambil metadatanya (dimensi, trek, seperti daftar, durasi, dan lain-lain) sesuai keinginan. Perhatikan bahwa mulai Chrome 64, nilai default untuk preload adalah metadata. (Saat itu auto sebelumnya).

<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 mungkin menyimpan cache cukup data sehingga pemutaran dapat dilakukan tanpa perlu berhenti untuk mengalami 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 demikian, ada beberapa hal yang harus diwaspadai. Karena ini hanyalah petunjuk, browser mungkin sepenuhnya abaikan atribut preload. Pada saat menulis artikel ini, berikut beberapa aturan diterapkan di Chrome:

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

Tips

Jika situs Anda berisi banyak referensi video di domain yang sama, kami akan sebaiknya tetapkan nilai preload ke metadata atau tentukan poster dan tetapkan preload ke none. Dengan begitu, Anda akan menghindari memukul jumlah maksimum koneksi HTTP ke domain yang sama (6 sesuai dengan spesifikasi HTTP 1.1) yang dapat menghentikan pemuatan resource. Perhatikan bahwa ini mungkin juga meningkatkan kecepatan halaman jika video bukan bagian dari pengalaman pengguna utama Anda.

Seperti yang dibahas dalam artikel lain, pramuat link adalah pengambilan deklaratif yang memungkinkan Anda memaksa browser untuk membuat permintaan untuk sumber daya tanpa memblokir peristiwa load dan saat halaman diunduh. Sumber daya yang dimuat melalui <link rel="preload"> disimpan secara lokal di browser, dan secara efektif tidak berfungsi hingga mereka direferensikan secara eksplisit 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.). Alat ini harus digunakan untuk memanaskan {i> cache browser<i} untuk sesi.

Pramuat video lengkap

Berikut ini cara melakukan pramuat video lengkap di situs Anda agar saat JavaScript meminta untuk mengambil konten video, konten dibaca dari cache sebagai resource mungkin telah di-cache oleh browser. Jika permintaan pramuat belum selesai, maka 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 yang dipramuat akan digunakan oleh elemen video di contohnya, nilai link pramuat as adalah video. Jika konten tersebut adalah audio , variabel tersebut adalah 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 tidak familier dengan MSE JavaScript API, lihat dasar-dasar MSE.

Agar lebih mudah, anggap saja 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 ini:

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 membahas Cache API dan pekerja layanan, mari kita lihat cara melakukan buffer video secara manual dengan MSE. Contoh di bawah ini mengasumsikan bahwa server mendukung HTTP Range permintaan tapi ini akan sangat mirip dengan segmen. Perlu diketahui bahwa beberapa library middleware seperti Shaka Google Pemutar, JW Player, dan Video.js dibuat untuk menangani hal ini.

<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 sekarang dapat mengontrol seluruh pengalaman buffering media, sebaiknya Anda pertimbangkan level baterai perangkat, yakni "Mode Hemat Data" preferensi pengguna dan informasi jaringan saat memikirkan tentang pramuat.

Awareness baterai

Mempertimbangkan tingkat daya baterai pengguna sebelum berpikir 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 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 iklan dengan cepat dan ringan aplikasi kepada pengguna yang telah ikut serta dalam "penghematan data" mode di browser. Dengan mengidentifikasi header permintaan ini, aplikasi Anda bisa menyesuaikan dan memberikan pengalaman pengguna yang dioptimalkan dengan biaya dan performa yang terbatas pelanggan.

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

Smart pemuatan berdasarkan informasi jaringan

Sebaiknya periksa navigator.connection.type sebelum melakukan pramuat. Kapan disetel ke cellular, Anda dapat mencegah pramuat dan memberi tahu pengguna bahwa operator jaringan seluler mereka mungkin mengenakan biaya {i>bandwidth<i}, dan mulai secara otomatis untuk konten yang sebelumnya di-cache.

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 terhadap jaringan perubahan juga.

Melakukan pra-cache beberapa segmen pertama

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

Contoh dua bagian di bawah ini menunjukkan cara melakukan {i>pre-cache<i} beberapa segmen pertama dari video menggunakan Cache API yang canggih dan mudah digunakan. Perhatikan bahwa hal serupa juga dapat dicapai dengan IndexedDB. Kita belum menggunakan pekerja layanan sebagai Cache API juga dapat diakses dari objek window.

Ambil 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 menggunakan permintaan Range HTTP, saya harus membuat ulang secara manual objek Response karena Cache API belum mendukung respons Range. Membuat perlu diingat bahwa memanggil networkResponse.arrayBuffer() akan mengambil seluruh konten respons sekaligus ke dalam memori perender, karena itulah Anda sebaiknya menggunakan rentang kecil.

Sebagai referensi, saya telah mengubah bagian dari contoh di atas untuk menyimpan Rentang HTTP permintaan ke pra-cache 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 agar pemutaran segera dimulai jika tersedia. Jika tidak, kita hanya akan mengambilnya dari jaringan. Ingatlah bahwa browser dan pengguna dapat memutuskan untuk menghapus Cache.

Seperti yang terlihat sebelumnya, kita menggunakan MSE untuk memasukkan segmen pertama video ke 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

Sekarang 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 tidak mendukung respons Range belum.

Jadi, saya akan menunjukkan cara menangkap permintaan ini dan menampilkan Range yang disesuaikan respons yang sama 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 irisan ini karena memberikan handle ke file sembari response.arrayBuffer() memasukkan seluruh file ke dalam memori perender.

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

Lihat Aplikasi Media Contoh resmi dan khususnya File ranged-response.js untuk solusi lengkap cara menangani Range permintaan.