原文地址:https://developers.google.com/web/fundamentals/media/fast-playback-with-video-preload
在以往的項(xiàng)目中寂恬,只要有視頻的存在,那么就會(huì)是個(gè)讓人費(fèi)神的項(xiàng)目闰挡。且不說(shuō)對(duì)它的適配兼容問(wèn)題举娩,只說(shuō)它的加載問(wèn)題就能說(shuō)上半天了析校。本文作者從視頻預(yù)加載的各種方法入手,討論了如何讓視頻播放速度更快的解決辦法铜涉。
眾所周知勺良,能更快速的播放視頻意味著會(huì)有更的多人觀看到你的視頻。在本文中骄噪,我將探索通過(guò)用戶主動(dòng)觸發(fā)預(yù)加載資源來(lái)加速視頻播放的技術(shù)尚困。
注意: 除非另有說(shuō)明,否則本文也適用于audio元素链蕊。
視頻地址:https://developers.google.com/web/fundamentals/media/fast-playback-with-video-preload
致謝:版權(quán)所有Blender Foundation | www.blender.org 事甜。
TL; DR
這很棒…但…
視頻preload屬性易用于Web服務(wù)器上托管的唯一文件。瀏覽器可能完全忽略該屬性滔韵。
HTML文檔完全加載和解析后逻谦,資源才開(kāi)始獲取。
當(dāng)應(yīng)用程使用MSE擴(kuò)展媒體時(shí)陪蜻,MSE會(huì)忽略媒體元素上的preload屬性邦马。
Link preload強(qiáng)制瀏覽器發(fā)出視頻資源請(qǐng)求,但不會(huì)阻止文檔的onload事件宴卖。HTTP Range請(qǐng)求不兼容滋将。
兼容MSE和文檔片斷。獲取完整資源時(shí)症昏,文件只能是小型媒體(< 5MB)随闽。
手動(dòng)緩沖完全控制復(fù)雜的錯(cuò)誤需要網(wǎng)頁(yè)來(lái)處理。
視頻預(yù)加載(preload)屬性
如果視頻資源是托管在Web服務(wù)器上的唯一文件肝谭,您可能會(huì)使用 video 標(biāo)簽的?preload屬性來(lái)提示瀏覽器預(yù)加載的信息或內(nèi)容量掘宪。 但這意味著Media Source Extensions(MSE)與?preload?將不兼容蛾扇。
資源的獲取將僅在HTML文檔初始加載和解析完成后啟動(dòng)(例如,?DOMContentLoaded事件已觸發(fā))魏滚,而實(shí)際上在獲取資源時(shí)將觸發(fā)完全不同的?window.onload事件镀首。
將?preload屬性設(shè)置為?metadata表示用戶不想馬上加載視頻,但是需要預(yù)先獲取其元數(shù)據(jù)(尺寸鼠次,軌道列表蘑斧,時(shí)長(zhǎng)等)。 請(qǐng)注意须眷,從Chrome 64開(kāi)始,?preload的默認(rèn)值是?metadata(以前是?auto?)沟突。
<video?id="video"?preload="metadata"?src="file.mp4"?controls></video>
????<script>
????????video.addEventListener('loadedmetadata',?function()?{
????????if?(video.buffered.length?===?0)?return;
????????????var?bufferedSeconds?=?video.buffered.end(0)?-?video.buffered.start(0);
????????????console.log(bufferedSeconds?+?'?seconds?of?video?are?ready?to?play!');
????????});
????</script>
將?preload屬性設(shè)置為?auto表示瀏覽器將緩存整個(gè)視頻花颗,無(wú)需暫停緩沖,可以支持完整播放惠拭。
<video?id="video"?preload="auto"?src="file.mp4"?controls></video>
????<script>
????????video.addEventListener('loadedmetadata',?function()?{
????????if?(video.buffered.length?===?0)?return;
????????????var?bufferedSeconds?=?video.buffered.end(0)?-?video.buffered.start(0);
????????????console.log(bufferedSeconds?+?'?seconds?of?video?are?ready?to?play!');
????????});
????</script>
由于?preload屬性只是一個(gè)提示扩劝,瀏覽器可能會(huì)完全忽略?preload屬性。寫(xiě)到這职辅,請(qǐng)注意以下Chrome中的一些應(yīng)用規(guī)則:
? 啟用Data Saver后 棒呛,Chrome 會(huì)強(qiáng)制設(shè)置?preload值為?none?。
? 在Android 4.3中域携,由于Android的bug簇秒,Chrome 會(huì)強(qiáng)制設(shè)置?preload值為?none。
? 在蜂窩連接(2G秀鞭,3G和4G)時(shí)趋观,Chrome 會(huì)強(qiáng)制設(shè)置?preload值為?metadata?。
提示
如果您的網(wǎng)站在同一個(gè)域中包含多個(gè)視頻資源锋边,我建議您將?preload值設(shè)置為?metadata或定義?poster屬性并將?preload設(shè)置為?none?皱坛。 這樣,可以避免在同一域名中HTTP連接數(shù)達(dá)到最大時(shí)導(dǎo)致資源加載掛起(根據(jù)HTTP 1.1規(guī)范6)豆巨。 請(qǐng)注意剩辟,如果視頻不屬于您的核心用戶體驗(yàn),這樣做也會(huì)提高網(wǎng)頁(yè)加載速度往扔。
Link preload
正如其他文章所述 贩猎,link preload是一種聲明性資源獲取,允許您強(qiáng)制瀏覽器在不阻止?window.onload事件和頁(yè)面加載的情況下發(fā)出資源請(qǐng)求萍膛。 通過(guò)?<linkrel="preload">預(yù)加載的資源在DOM融欧、JavaScript或CSS沒(méi)有明確引用之前,被存儲(chǔ)在本地瀏覽器中卦羡。
預(yù)加載preload與預(yù)獲取prefetch不同之處在于它側(cè)重于當(dāng)前導(dǎo)航并根據(jù)其類型的優(yōu)先級(jí)(腳本噪馏,樣式麦到,字體,視頻欠肾,音頻等)獲取資源瓶颠。它通常用于為當(dāng)前會(huì)話預(yù)熱瀏覽器緩存。
預(yù)加載完整視頻
以下示例講述了是如何在您的網(wǎng)站上預(yù)加載完整視頻刺桃,以便當(dāng)您的JavaScript請(qǐng)求獲取視頻內(nèi)容時(shí)粹淋,它會(huì)從緩存中讀取,因?yàn)橐曨l資源可能已被瀏覽器緩存瑟慈。 如果預(yù)加載請(qǐng)求尚未完成桃移,則將進(jìn)行常規(guī)網(wǎng)絡(luò)獲取。
<link rel="preload" as="video" >
<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>
注意: 我建議僅將其用于小型媒體文件(<5MB)葛碧。
由于link預(yù)加載的?as屬性值為?video借杰,所以預(yù)加載資源將由例子中的視頻元素使用。如果它是一個(gè)音頻元素进泼,它將是?as="audio"蔗衡。
預(yù)加載第一個(gè)片段
下面的示例顯示了如何用?<linkrel="preload">來(lái)預(yù)加載視頻的第一段內(nèi)容,并將其與Media Source Extensions一起使用乳绕。 如果您不熟悉 MSE Javascript API绞惦,請(qǐng)參閱MSE基礎(chǔ)知識(shí)。
為簡(jiǎn)單起見(jiàn)洋措,我們假設(shè)整個(gè)視頻已被拆分為若干較小的文件,如“file1.webm”济蝉,“file2.webm”,“file_3.webm”等菠发。
<link rel="preload" as="fetch" >
<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>
警告: 對(duì)于跨域問(wèn)題堆生,請(qǐng)確保正確設(shè)置了CORS請(qǐng)求頭。 由于我們無(wú)法使用fetch(videoFileUrl, { mode: ‘no-cors’ })檢索未知響應(yīng)所創(chuàng)建的緩存數(shù)組雷酪,因此我們無(wú)法將其提供給視頻或音頻元素淑仆。
支持
由于link preload 尚未在每個(gè)瀏覽器中得到支持。您可以使用下面的代碼檢測(cè)其可用性哥力,以調(diào)整您的展現(xiàn)效果蔗怠。
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');
}
手動(dòng)緩沖
在我們深入了解Cache API和Service Worker之前,讓我們看看如何使用MSE手動(dòng)緩沖視頻吩跋。 下面的例子模擬了支持HTTP Range請(qǐng)求的Web服務(wù)器寞射,但這種方法與緩存文件片段非常相似。 請(qǐng)注意锌钮,一些插件庫(kù)如Google的Shaka Player 桥温,JW Player和Video.js都可以為您處理此問(wèn)題。
<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!
var 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>
注意事項(xiàng)
由于您現(xiàn)在已有控制緩沖整個(gè)媒體的能力梁丘,我建議您在預(yù)加載時(shí)考慮下使用設(shè)備的電池電量侵浸、用戶的“Data-Saver 模式”首選項(xiàng)和網(wǎng)絡(luò)信息等因素旺韭。
電池意識(shí)
在考慮預(yù)加載視頻之前,請(qǐng)考慮用戶設(shè)備的電池電量掏觉。 這將在電量較低時(shí)保持電池壽命区端。
當(dāng)設(shè)備電池電量快耗盡時(shí),禁用預(yù)加載或預(yù)加載分辨率較低的視頻澳腹。
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.
}
});
}
檢測(cè)“Data-Saver”
使用?Save-Data客戶端提示請(qǐng)求頭為在瀏覽器中啟動(dòng)“流量節(jié)省”模式的用戶提供快速輕便的應(yīng)用程序织盼。通過(guò)識(shí)別此請(qǐng)求頭,您的應(yīng)用程序可以通過(guò)自定義限制成本和限制性能的方法為用戶提供更好的用戶體驗(yàn)酱塔。
通過(guò)閱讀?使用Save-Data提供快速和輕量級(jí)應(yīng)用程序?全文沥邻,了解更多信息 。
基于網(wǎng)絡(luò)信息的智能加載
您可以在預(yù)加載之前檢查?navigator.connection.type?羊娃。當(dāng)它設(shè)置為?cellular?時(shí)唐全,您可以阻止預(yù)加載并提示用戶他們的移動(dòng)網(wǎng)絡(luò)運(yùn)營(yíng)商可能正在為帶寬收費(fèi),并且只自動(dòng)回放以前緩存的內(nèi)容迁沫。
if ('connection' in navigator) {
if (navigator.connection.type == 'cellular') {
// TODO: Prompt user before preloading video
} else {
// TODO: Preload the first segment of a video.
}
}
查看?網(wǎng)絡(luò)信息示例?了解如何對(duì)網(wǎng)絡(luò)更改做出反應(yīng)。
預(yù)緩存多個(gè)第一片段
如果我們想在不知道用戶最終將選擇哪一個(gè)視頻進(jìn)行播放的情況下捌蚊,預(yù)先加載一些視頻集畅,那該如何操作呢。假設(shè)用戶在瀏覽一個(gè)具有10個(gè)視頻的網(wǎng)頁(yè)缅糟,我們有足夠的內(nèi)存來(lái)緩存每個(gè)視頻文件挺智,但我們肯定不會(huì)去創(chuàng)建10個(gè)隱藏的video標(biāo)簽和10個(gè)?MediaSource對(duì)象以及它們的數(shù)據(jù)。
下面的兩個(gè)部分示例向您展示了如何使用功能強(qiáng)大且易用的Cache API來(lái)預(yù)緩存多個(gè)第一視頻片段窗宦。需要注意的是赦颇,使用IndexedDB也可以實(shí)現(xiàn)類似的功能。這里我們沒(méi)有使用Service Workers赴涵,是因?yàn)镃ache API也可以從Window對(duì)象中訪問(wèn)媒怯。
Fetch和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;
});
});
}
請(qǐng)注意,如果我要使用 HTTP Range 請(qǐng)求髓窜,則必須手動(dòng)重新創(chuàng)建?Response對(duì)象扇苞,因?yàn)镃ache API尚不支持Range響應(yīng)。 還要注意的是寄纵,在調(diào)用?networkResponse.arrayBuffer()時(shí)會(huì)立即響應(yīng)鳖敷,并將獲取到的全部?jī)?nèi)容渲染器內(nèi)存中,這也是您可能希望使用小范圍的原因程拭。
作為參考定踱,我修改了上面例子的部分代碼,將HTTP Range請(qǐng)求的視頻保存到預(yù)緩存中恃鞋。
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;
});
播放視頻
當(dāng)用戶點(diǎn)擊播放按鈕時(shí)崖媚,我們將獲取Cache API中可用的第一段視頻亦歉,以便在它可用時(shí)能立即開(kāi)始播放。否則至扰,我們需要從網(wǎng)絡(luò)中獲取它鳍徽。 需要注意的是,瀏覽器和用戶可能會(huì)清除緩存 敢课。
如前所述阶祭,我們使用MSE將視頻的第一片段傳給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.
});
}
});
}
警告: 對(duì)于跨域問(wèn)題直秆,請(qǐng)確保正確設(shè)置了CORS請(qǐng)求頭濒募。 由于我們無(wú)法使用fetch(videoFileUrl, { mode: ‘no-cors’ })檢索未知響應(yīng)所創(chuàng)建的緩存數(shù)組技俐,因此我們無(wú)法將其提供給視頻或音頻元素载迄。
使用Service Worker創(chuàng)建Range響應(yīng)
現(xiàn)在崖咨,如果您已獲取整個(gè)視頻文件并將其保存在Cache API中古掏。 當(dāng)瀏覽器發(fā)送 HTTP Range請(qǐng)求時(shí)拒炎,您肯定不希望將整個(gè)視頻存入渲染器內(nèi)存硅堆,因?yàn)?Cache API尚不支持 Range 響應(yīng)沪曙。
那么痘括,讓我演示下如何攔截這些請(qǐng)求并從service worker返回自定義的Range響應(yīng)歇竟。
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.
});
}
});
}
重點(diǎn)是要注意我使用?response.blob()重新創(chuàng)建了這個(gè)切片響應(yīng)挥唠,因?yàn)檫@只是讓我可以( 在Chrome中 )處理文件,而?response.arrayBuffer()會(huì)將整個(gè)文件存入渲染器內(nèi)存焕议。
我的自定義?X-From-CacheHTTP 響應(yīng)頭可用于判斷此請(qǐng)求是來(lái)自緩存還是來(lái)自網(wǎng)絡(luò)宝磨。也可以用于像ShakaPlayer等播放器用它來(lái)忽略作為網(wǎng)絡(luò)速度指示的響應(yīng)時(shí)間。
視頻地址:https://developers.google.com/web/fundamentals/media/fast-playback-with-video-preload
這里有一個(gè)官方媒體應(yīng)用程序的視頻例子 盅安,特別是它的ranged-response.js文件唤锉,講解了如何處理Range請(qǐng)求的完整解決方案。