预加载视频实现快速播放

原文地址: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请求的完整解决方案。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341