如何通过主动预加载资源来加快媒体播放速度。
<ph type="x-smartling-placeholder">
更快的开始播放速度意味着有更多人观看你的视频或收听你的音乐 音频。这是众所周知的事实。在本文中,我将为您介绍 您可以采用什么技巧来通过主动 根据您的用例预加载资源。
我将说明三种预加载媒体文件的方法,首先介绍它们的优点 和缺点
很棒... | 但是... | |
---|---|---|
视频预加载属性 | 易于使用,用于托管在网络服务器上的唯一文件。 | 浏览器可能会完全忽略该属性。 |
当 HTML 文档完全加载并 已解析。 | ||
Media Source Extensions (MSE) 会忽略媒体元素上的 preload 属性,因为应用需负责
向 MSE 提供媒体
|
||
链接预加载 |
强制浏览器在不屏蔽的情况下请求视频资源
触发文档的 onload 事件。
|
HTTP 范围请求不兼容。 |
与 MSE 和文件段兼容。 | 应仅用于获取完整资源时小型媒体文件 (<5 MB)。 | |
手动缓冲 | 完全控制 | 复杂的错误处理由网站负责。 |
视频预加载属性
如果视频源是托管在网络服务器上的唯一文件,您可能需要将
使用视频 preload
属性来提示浏览器如何
需要预加载的信息或内容。这意味着 Media Source Extensions 扩展
(MSE) 与 preload
不兼容。
只有在创建了初始 HTML 文档后,系统才会开始提取资源
已完全加载并解析(例如,DOMContentLoaded
事件已触发)
而在调用资源时会触发完全不同的load
事件
实际已被提取的内容
将 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;
const 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;
const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
console.log(`${bufferedSeconds} seconds of video are ready to play.`);
});
</script>
不过,有几点需要注意。由于这只是一种提示
并忽略 preload
属性。在撰写本文时,请注意以下规则
已在 Chrome 中应用:
- 启用流量节省程序后,Chrome 会强制将
preload
值设为none
。 - 在 Android 4.3 中,由于 Android
preload
none
bug。 - 使用移动网络连接(2G、3G 和 4G)时,Chrome 会强制
preload
值metadata
。
提示
如果您的网站包含同一个网域中的许多视频资源,
建议您将 preload
值设置为 metadata
,或定义 poster
属性,并将 preload
设置为 none
。这样,您就可以避免
到同一网域的 HTTP 连接数上限(根据
HTTP 1.1 规范),这可能会挂起资源加载。请注意,这也可以
如果视频并非核心用户体验的一部分,可以提高网页加载速度。
链接预加载
<ph type="x-smartling-placeholder">如其他文章中所述,链接预加载是一种声明式提取,
您可以强制浏览器请求资源
在网页下载期间屏蔽 load
事件。资源
通过 <link rel="preload">
加载的文件会存储在本地浏览器中,
直到在 DOM、JavaScript 和
或 CSS 提供商。
预加载与预提取的不同之处在于,它侧重于当前的导航和 根据资源类型(脚本、样式、字体、 视频、音频等)。它应该用于为当前的 会话。
预加载完整视频
下面介绍了如何在您的网站上预加载完整视频, 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>
由于预加载的资源将由 Google Cloud 中的
在此示例中,as
预加载链接的值为 video
。如果是音频内容
元素,则属性为 as="audio"
。
预加载第一个片段
以下示例展示了如何使用 <link
rel="preload">
预加载视频的第一段,并将其与 Media Source Extensions 搭配使用。如果您不熟悉
使用 MSE JavaScript API,请参阅 MSE 基础知识。
为简单起见,我们假设整个视频
file_1.webm
、file_2.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>
支持
<ph type="x-smartling-placeholder">您可以使用as
<link rel=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
但使用 .zip 文件
细分。请注意,有些中间件库如 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!
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>
注意事项
由于您现在可以控制整个媒体缓冲体验,因此建议您 考虑设备的电池电量、“流量节省程序模式”用户偏好设置和 在考虑预加载时需要考虑哪些网络信息
电池感知
考虑用户设备的电池电量然后再考虑 如何预加载视频这将在电池电量达到 30% 时延长电池续航时间 较低。
在以下情况下,停用预加载或至少预加载 设备电量耗尽
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.
}
});
}
检测“流量节省程序”
使用 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 个隐藏 <video>
元素和 10 个 MediaSource
对象,并开始馈送这些数据。
下面的两部分示例展示了如何预缓存
了解如何使用功能强大且易于使用的 Cache API。请注意,
也可以使用 IndexedDB 来实现。我们尚未使用 Service Worker,
您还可以通过 window
对象访问 Cache API。
提取和缓存
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 范围,以供参考 向视频预缓存发送多个请求
...
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;
});
播放视频
当用户点击播放按钮时,我们会提取视频的第一段 ,以便立即开始播放(如果可用)。 否则,我们将直接从网络中提取。请注意 且用户可以决定清除缓存。
如前所述,我们使用 MSE 将视频的第一段馈送到视频中 元素。
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.
});
}
});
}
使用 Service Worker 创建范围响应
如果您已经将整个视频文件
并将其保存在
Cache API?当浏览器发送 HTTP Range
请求时,您肯定不会
需要将整个视频放入渲染器内存中,因为 Cache API 不需要
目前支持 Range
响应。
我来展示一下如何拦截这些请求并返回自定义的 Range
从 Service Worker 返回。
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;
}
}
请务必注意,我使用 response.blob()
重新创建了此 Slice
因为这仅会为我提供一个文件句柄
response.arrayBuffer()
将整个文件引入渲染器内存。
我的自定义 X-From-Cache
HTTP 标头可用于了解此请求是否
来自缓存或网络。可供其他玩家使用
ShakaPlayer 忽略响应时间作为指标
网络速度
已了解官方示例媒体应用,尤其是其
ranged-response.js 文件,获取有关如何处理 Range
的完整解决方案
请求。