Cara mempercepat pemutaran media dengan memuat ulang resource secara aktif.
Memulai pemutaran lebih cepat berarti lebih banyak orang yang menonton video atau mendengarkan audio Anda. Itu adalah fakta umum. Dalam artikel ini, saya akan membahas teknik yang dapat Anda gunakan untuk mempercepat pemutaran audio dan video dengan secara aktif memuat ulang resource, bergantung pada kasus penggunaan Anda.
Saya akan menjelaskan tiga metode pramuat file media, mulai dari kelebihan dan kekurangannya.
Bagus... | Namun... | |
---|---|---|
Atribut pramuat video | Mudah digunakan untuk file unik yang dihosting di server web. | Browser mungkin mengabaikan atribut tersebut sepenuhnya. |
Pengambilan resource dimulai saat dokumen HTML dimuat dan diuraikan sepenuhnya. | ||
Ekstensi Sumber Media (MSE) mengabaikan atribut preload pada elemen media karena aplikasi bertanggung jawab untuk
menyediakan media ke MSE.
|
||
Preload link |
Memaksa browser membuat permintaan untuk resource video tanpa memblokir
peristiwa onload dokumen.
|
Permintaan Rentang HTTP tidak kompatibel. |
Kompatibel dengan MSE dan segmen file. | Hanya boleh digunakan untuk file media kecil (<5 MB) saat mengambil resource lengkap. | |
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 kepada browser tentang jumlah informasi atau konten yang akan dimuat sebelumnya. Artinya, Ekstensi Sumber Media
(MSE) tidak kompatibel dengan preload
.
Pengambilan resource hanya akan dimulai jika 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.
Menetapkan atribut preload
ke metadata
menunjukkan bahwa pengguna tidak
diharapkan memerlukan video, tetapi pengambilan metadatanya (dimensi, daftar
jalur, durasi, dan sebagainya) diinginkan. Perhatikan bahwa mulai Chrome
64, nilai default untuk preload
adalah metadata
. (Sebelumnya adalah 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>
Menetapkan atribut preload
ke auto
menunjukkan bahwa browser dapat meng-cache
data yang cukup sehingga pemutaran dapat dilakukan 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 hal yang perlu diperhatikan. Karena ini hanyalah petunjuk, browser dapat sepenuhnya
mengabaikan atribut preload
. Pada saat penulisan, berikut beberapa aturan
yang diterapkan di Chrome:
- Saat Penghemat Data diaktifkan, Chrome akan memaksa nilai
preload
menjadinone
. - Di Android 4.3, Chrome memaksa nilai
preload
kenone
karena Bug Android. - Pada koneksi seluler (2G, 3G, dan 4G), Chrome memaksa nilai
preload
menjadimetadata
.
Tips
Jika situs Anda berisi banyak resource video di domain yang sama, sebaiknya
tetapkan nilai preload
ke metadata
atau tentukan atribut poster
dan tetapkan preload
ke none
. Dengan begitu, Anda akan menghindari
jumlah maksimum koneksi HTTP ke domain yang sama (6 menurut spec HTTP 1.1) yang dapat menggantung pemuatan resource. Perhatikan bahwa hal ini juga dapat
meningkatkan kecepatan halaman jika video bukan bagian dari pengalaman pengguna inti Anda.
Pramuat link
Seperti yang dibahas dalam artikel lain, pramuat link adalah pengambilan deklaratif yang
memungkinkan Anda memaksa browser untuk membuat permintaan resource tanpa
memblokir peristiwa load
dan saat halaman didownload. Resource
yang dimuat melalui <link rel="preload">
disimpan secara lokal di browser, dan
secara efektif tidak aktif hingga dirujuk 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.). Ini harus digunakan untuk memanaskan cache browser untuk sesi saat ini.
Memuat video lengkap secara otomatis
Berikut 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 sudah 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 yang dimuat sebelumnya akan digunakan oleh elemen video dalam
contoh, nilai link pramuat as
adalah video
. Jika elemennya adalah elemen
audio, elemen tersebut akan menjadi as="audio"
.
Memuat segmen pertama secara otomatis
Contoh di bawah menunjukkan cara memuat segmen pertama video dengan <link
rel="preload">
dan menggunakannya dengan Ekstensi Sumber Media. Jika Anda tidak memahami
MSE JavaScript API, lihat dasar-dasar MSE.
Untuk memudahkan, mari kita asumsikan bahwa 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 membahas Cache API dan pekerja layanan, mari kita lihat cara 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. Perhatikan bahwa beberapa library middleware seperti Shaka Player
Google, 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 sekarang mengontrol seluruh pengalaman buffering media, sebaiknya pertimbangkan level baterai perangkat, preferensi pengguna "Mode Penghemat Data", dan informasi jaringan saat mempertimbangkan pramuat.
Kesadaran baterai
Pertimbangkan level baterai perangkat pengguna sebelum memikirkan pramuat video. Tindakan ini akan menghemat masa pakai baterai saat level daya rendah.
Nonaktifkan pramuat atau setidaknya pramuat video beresolusi lebih rendah saat baterai perangkat hampir habis.
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 mengirimkan aplikasi yang cepat dan ringan kepada pengguna yang telah memilih mode "penghematan data" di browser mereka. Dengan mengidentifikasi header permintaan ini, aplikasi Anda dapat menyesuaikan dan
memberikan pengalaman pengguna yang dioptimalkan kepada pengguna yang dibatasi biaya dan performa.
Lihat Menghadirkan Aplikasi Cepat dan Ringan dengan Hemat-Data untuk mempelajari lebih lanjut.
Pemuatan cerdas 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, dan hanya memulai
pemutaran otomatis konten yang di-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 merespons perubahan jaringan juga.
Menyimpan beberapa segmen pertama ke dalam cache terlebih dahulu
Sekarang, bagaimana jika saya ingin melakukan pramuat spekulatif untuk 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 memori yang cukup untuk mengambil satu file segmen dari setiap video, tetapi kita tidak boleh membuat 10 elemen <video>
tersembunyi dan 10 objek MediaSource
, lalu mulai memasukkan data tersebut.
Contoh dua bagian di bawah 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. Kita belum menggunakan pekerja layanan karena
Cache API juga dapat diakses dari objek window
.
Mengambil dan meng-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 sebagian contoh di atas untuk menyimpan permintaan Rentang HTTP 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 sehingga pemutaran segera dimulai jika tersedia. Jika tidak, kita cukup mengambilnya dari jaringan. Perlu diingat bahwa browser dan pengguna dapat memutuskan untuk menghapus Cache.
Seperti yang telah kita lihat sebelumnya, kita menggunakan MSE untuk memasukkan segmen video pertama 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 Rentang 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 belum
mendukung respons Range
secara.
Jadi, mari kita lihat 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 dipotong ini karena hal ini hanya memberi saya handle ke file saat
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 cache atau dari jaringan. Ini dapat digunakan oleh pemain seperti
ShakaPlayer untuk mengabaikan waktu respons sebagai indikator
kecepatan jaringan.
Lihat Contoh Aplikasi Media resmi dan khususnya file
ranged-response.js untuk solusi lengkap tentang cara menangani permintaan
Range
.