原文地址:https://developers.google.com/web/fundamentals/media/fast-playback-with-video-preload
在以往的项目中,只要有视频的存在,那么就会是个让人费神的项目。且不说对它的适配兼容问题,只说它的加载问题就能说上半天了。本文作者从视频预加载的各种方法入手,讨论了如何让视频播放速度更快的解决办法。
众所周知,能更快速的播放视频意味着会有更的多人观看到你的视频。在本文中,我将探索通过用户主动触发预加载资源来加速视频播放的技术。
注意: 除非另有说明,否则本文也适用于audio元素。
视频地址:https://developers.google.com/web/fundamentals/media/fast-playback-with-video-preload
致谢:版权所有Blender Foundation | www.blender.org 。
TL; DR
这很棒…但…
视频preload属性易用于Web服务器上托管的唯一文件。浏览器可能完全忽略该属性。
HTML文档完全加载和解析后,资源才开始获取。
当应用程使用MSE扩展媒体时,MSE会忽略媒体元素上的preload属性。
Link preload强制浏览器发出视频资源请求,但不会阻止文档的onload事件。HTTP Range请求不兼容。
兼容MSE和文档片断。获取完整资源时,文件只能是小型媒体(< 5MB)。
手动缓冲完全控制复杂的错误需要网页来处理。
视频预加载(preload)属性
如果视频资源是托管在Web服务器上的唯一文件,您可能会使用 video 标签的 preload属性来提示浏览器预加载的信息或内容量。 但这意味着Media Source Extensions(MSE)与 preload 将不兼容。
资源的获取将仅在HTML文档初始加载和解析完成后启动(例如, DOMContentLoaded事件已触发),而实际上在获取资源时将触发完全不同的 window.onload事件。
将 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;
var 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;
var bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
console.log(bufferedSeconds + ' seconds of video are ready to play!');
});
</script>
由于 preload属性只是一个提示,浏览器可能会完全忽略 preload属性。写到这,请注意以下Chrome中的一些应用规则:
• 启用Data Saver后 ,Chrome 会强制设置 preload值为 none 。
• 在Android 4.3中,由于Android的bug,Chrome 会强制设置 preload值为 none。
• 在蜂窝连接(2G,3G和4G)时,Chrome 会强制设置 preload值为 metadata 。
提示
如果您的网站在同一个域中包含多个视频资源,我建议您将 preload值设置为 metadata或定义 poster属性并将 preload设置为 none 。 这样,可以避免在同一域名中HTTP连接数达到最大时导致资源加载挂起(根据HTTP 1.1规范6)。 请注意,如果视频不属于您的核心用户体验,这样做也会提高网页加载速度。
Link preload
正如其他文章所述 ,link preload是一种声明性资源获取,允许您强制浏览器在不阻止 window.onload事件和页面加载的情况下发出资源请求。 通过 <linkrel="preload">预加载的资源在DOM、JavaScript或CSS没有明确引用之前,被存储在本地浏览器中。
预加载preload与预获取prefetch不同之处在于它侧重于当前导航并根据其类型的优先级(脚本,样式,字体,视频,音频等)获取资源。它通常用于为当前会话预热浏览器缓存。
预加载完整视频
以下示例讲述了是如何在您的网站上预加载完整视频,以便当您的JavaScript请求获取视频内容时,它会从缓存中读取,因为视频资源可能已被浏览器缓存。 如果预加载请求尚未完成,则将进行常规网络获取。
<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>
注意: 我建议仅将其用于小型媒体文件(<5MB)。
由于link预加载的 as属性值为 video,所以预加载资源将由例子中的视频元素使用。如果它是一个音频元素,它将是 as="audio"。
预加载第一个片段
下面的示例显示了如何用 <linkrel="preload">来预加载视频的第一段内容,并将其与Media Source Extensions一起使用。 如果您不熟悉 MSE Javascript API,请参阅MSE基础知识。
为简单起见,我们假设整个视频已被拆分为若干较小的文件,如“file1.webm”,“file2.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>
警告: 对于跨域问题,请确保正确设置了CORS请求头。 由于我们无法使用fetch(videoFileUrl, { mode: ‘no-cors’ })检索未知响应所创建的缓存数组,因此我们无法将其提供给视频或音频元素。
支持
由于link 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 Worker之前,让我们看看如何使用MSE手动缓冲视频。 下面的例子模拟了支持HTTP Range请求的Web服务器,但这种方法与缓存文件片段非常相似。 请注意,一些插件库如Google的Shaka Player ,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!
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>
注意事项
由于您现在已有控制缓冲整个媒体的能力,我建议您在预加载时考虑下使用设备的电池电量、用户的“Data-Saver 模式”首选项和网络信息等因素。
电池意识
在考虑预加载视频之前,请考虑用户设备的电池电量。 这将在电量较低时保持电池寿命。
当设备电池电量快耗尽时,禁用预加载或预加载分辨率较低的视频。
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对象以及它们的数据。
下面的两个部分示例向您展示了如何使用功能强大且易用的Cache API来预缓存多个第一视频片段。需要注意的是,使用IndexedDB也可以实现类似的功能。这里我们没有使用Service Workers,是因为Cache API也可以从Window对象中访问。
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;
});
});
}
请注意,如果我要使用 HTTP Range 请求,则必须手动重新创建 Response对象,因为Cache API尚不支持Range响应。 还要注意的是,在调用 networkResponse.arrayBuffer()时会立即响应,并将获取到的全部内容渲染器内存中,这也是您可能希望使用小范围的原因。
作为参考,我修改了上面例子的部分代码,将HTTP Range请求的视频保存到预缓存中。
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将视频的第一片段传给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.
});
}
});
}
警告: 对于跨域问题,请确保正确设置了CORS请求头。 由于我们无法使用fetch(videoFileUrl, { mode: ‘no-cors’ })检索未知响应所创建的缓存数组,因此我们无法将其提供给视频或音频元素。
使用Service Worker创建Range响应
现在,如果您已获取整个视频文件并将其保存在Cache API中。 当浏览器发送 HTTP Range请求时,您肯定不希望将整个视频存入渲染器内存,因为 Cache API尚不支持 Range 响应。
那么,让我演示下如何拦截这些请求并从service worker返回自定义的Range响应。
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.
});
}
});
}
重点是要注意我使用 response.blob()重新创建了这个切片响应,因为这只是让我可以( 在Chrome中 )处理文件,而 response.arrayBuffer()会将整个文件存入渲染器内存。
我的自定义 X-From-CacheHTTP 响应头可用于判断此请求是来自缓存还是来自网络。也可以用于像ShakaPlayer等播放器用它来忽略作为网络速度指示的响应时间。
视频地址:https://developers.google.com/web/fundamentals/media/fast-playback-with-video-preload
这里有一个官方媒体应用程序的视频例子 ,特别是它的ranged-response.js文件,讲解了如何处理Range请求的完整解决方案。