关于 video 播放的新探索

前端同学要使用 HTML5 播放器视频,必然会使用 video 标签,不过大多数同学只是使用了较简单的功能,其实它本身拥有不凡之力有待我们发现。

首先,我们先来看下 video 最基础的用法:

  • 使用 src 属性

    <video src="http://v2v.cc/~j/theora_testsuite/320x240.ogg" controls>
    你的浏览器不支持 <code>video</code> 标签。
    </video>
  • 使用 source 标签

    <video controls>
      <source src="foo.ogg" type="video/ogg">
      <source src="foo.mp4" type="video/mp4">
      Your browser does not support the <code>video</code> element.
    </video>

这是 MDN 关于 video 给出的基本用例。在这里我们简单介绍下两种方法的不同,src 只能赋予 video 一个播放地址,当浏览器不支持这种视频格式的解码时就会出现错误,导致视频播放失败。为了解决这个问题才有了 source 标签,利用多个 source 标签引入不同格式的视频,从上到下解析直到遇到看上述代码当浏览器不支持 ogg 格式时,浏览器会自动播放 foo.mp4。

实用技巧

我们会发现使用 video:src 属性播放视频的时候会经常出现播放失败,我们该怎么做能提升视频播放的质量?

这种情况一般都使用 cdn,为了更保险一般还会将不同的 cdn 厂商分为主用 cdn 和备用 cdn。那么问题来了,我们怎么利用 video 本身的特性并结合 cdn 保证我们的视频播放质量?

<video controls>
  <source src="//a.com/main.mp4" type="video/mp4">
  <source src="//b.com/backup.mp4" type="video/mp4">
  Your browser does not support the <code>video</code> element.
</video>

不难看出,source 标签不仅支持不同视频格式的自动切换,也适用于相同视频格式的失败切换,即 main.mp4 因网络问题无法获取时浏览器会自动切换到 backup.mp4 。

高阶技能

如果大家浏览视频的时候,可以发现很多网站的 video 是这个样子的:

<video src="blob:http://abc.com/d0823f0f-2b2a-4fd6-a93a-e4c82173c107">
</video>

如果直接访问 blob 这个地址发现并不存在。

这个地址就是映射 Blob 对象的 DOMString,其实 video 属性 src 支持 Blob 的,不过新的标准是使用 srcObject 属性来替代这一个功能,目前代码可以这样写:

const mediaSource = new MediaSource();
const video = document.createElement('video');
try {
  video.srcObject = mediaSource;
} catch (error) {
  video.src = URL.createObjectURL(mediaSource);
}

在这段代码中除了 Blob 对象,还有 MediaSource 对象,让 video 拥有不凡之力的主要因素是浏览器对 MediaSource 对象的支持,才让 JavaScript 对 video 有了更大的操纵空间。关于如何使用 MediaSource 不在本文讲述,大家可以自行查阅,我们要说的是 video 结合 MediaSource 可以做哪些事情?

清晰度无缝切换

点播领域里 mp4 是最普遍、兼容性最好的视频容器,不过 mp4 也有它的局限性,比如常见的清晰度切换,我们是无法像youtube那样做到无缝切换的。我们可以看下普通的mp4播放的网络请求和youtube视频播放的网络请求的区别。

图片描述

图1.1 普通mp4的下载请求过程

图片描述

图1.2 Youtube视频下载请求过程

这两张图不难看出,在默认情况下 mp4 使用一次 http 请求所有的视频数据,Youtube 则分次请求。当然这个描述很不专业,但确实形象。造成这种差异的是 video 不支持流式的视频数据,Youtube 采用的是流式的视频容器 webm,而 mp4 是非流式的。那如何解释清楚流式的视频数据呢,从专业的角度三言两语很难说清楚,但用大白话翻译过来就是流式的视频数据支持分段独立播放,非流式的不可以。换句话说一个10M的视频文件,流式的视频可以把0~1M的数据请求回来单独播放,但是非流式的不可以。

上面我们描述了视频格式的不同,接下来我们要说的是第一张图中的视频加载是浏览器来控制的,通过给 video 的 src 属性配置视频地址,触发播放之后浏览器就会开始下载了,JS干涉不了。而 Youtube 的视频加载是通过JS来控制的,各位可以再次看下第二张图的网络请求类型:xhr,足以证明这一点。

上面两点搞清楚之后我们就该说下清晰度切换的事情了。这个需求大家都不陌生,但是直接使用 mp4 格式做无缝清晰度切换,难度还挺大的。先解释下“无缝清晰度切换”的概念:从播放一个分辨率的视频到另一个分辨率且保证画面、声音不停顿的平滑切换过程。了解了这个概念,大家应该知道了用 video 无缝切换 mp4 有多难。一方面,video 是不支持流式的视频格式的,一方面,video 的加载是不受JS控制的。通过切换 video 的 src 属性,必然会导致画面中断、重新请求视频数据等。有的同学想到说利用两个 video 再结合 z-index 来搞,但是当你生成另一个video去加载视频的时候,无法保证两个画面是严格一致的,即使将原来的画面暂停到一个时刻,用另一个视频通过 currentTime 属性与之同步,切换仍然看到画面闪烁,基本无法和 Youtube 无缝切换的体验匹敌。而且还会造成更多流量的浪费,背后的原因大家可以研究下 mp4 容器和 webm 容器的异同,也可以看下视频解码相关的文章。

还有一种方法就是将 mp4 格式统统转码到流式的视频格式比如 hls、webm 等。不过这种看上去可行的方式实际上会带来很大的成本开销,如将大量视频做转码会消耗高昂的机器资源、双倍存储的费用、CDN的双倍费用等等。其实我们也是在这种背景下研究出来新的技术问题解决清晰度无缝切换的。

首先,我们改变对 mp4 视频的播放流程,不再直接使用 video 的 src 来播放,因为我们没有任何可以操作的空间。video不仅支持 src 属性还支持 Blob 对象,我们就是利用后者。播放的流程如下:

图片描述
图1.3 mp4 视频新播放流程

  • 来请求 mp4 视频数据,这样可以结合视频 Range 服务,做到精确加载。
  • 编写解析器将加载回来的部分 mp4 视频数据进行解复用
  • 将解复用的视频数据转成 fmp4 格式并传递给 MediaSource
  • 使用 video 进行解码完成播放

然后在做清晰度切换的时候流程如下:

图片描述
图1.4 mp4视频清晰度切换原理示意图

  • 来请求 mp4 视频数据,这样可以结合视频 Range 服务,做到精确加载。
  • 编写解析器将加载回来的部分 mp4 视频数据进行解复用
  • 将解复用的视频数据转成 fmp4 格式并传递给 MediaSource
  • 使用 video 进行解码完成播放

    然后在做清晰度切换的时候流程如下:

    图片描述
    图1.5 mp4视频清晰度切换流程示意图

    这个过程看上去比较繁琐,但是所有的操作都是在浏览器端完成,也就是说都是JS来实现的。这样之前说的所有成本问题都不存在,还能做到youtube相同体验的无缝切换。如果大家也想使用这个功能不需要自己再去实现一遍上述流程,可以使用如下代码:

    import Player from 'xgplayer';
    import 'xgplayer-mp4';
    
    let player = new Player({
          el:document.querySelector('#mse'),
          url: [{src:'/mp4/',type:"video/mp4"},{src:'/mp5/',type:'video/mp4'}]
    });
    
    player.emit('resourceReady', [{name: '高清', url: '/mp4/',cname:'高清'}, {name: '超清', url: '/mp5/',cname:'超清'}]);

如果对这段代码有什么疑惑或者想深入了解下它背后是如何实现的可以参考 文档 或者 Github

节省视频流量

我们平时直接使用video加载视频,大概是这样的:

图片描述
图2.1 video默认下载截图

我随便找了个视频,大家看下视频总长度是 02:08,在播放到 00:05 的时候,浏览器已经下载到 01:30 了,如果用户终止观看,下载的视频就这样被浪费掉了。当然,如果不断的 seek 也会造成较多的流量浪费。按照我们之前的统计在短视频领域,用户 seek 的频率在 80%,所以这部分流量是可以节省掉的。具体原理如下:

图片描述
图2.2 播放器加载视频原理

  • 设置每次加载的数据包大小
  • 设置预加载时长
  • 开启加载队列,完成第一次数据包下载,判断缓冲时间和预加载时长是否满足,不满足请求下一个数据包

具体实现代码如下:

  import Player from 'xgplayer';
  import 'xgplayer-mp4';
  const player = new Player({
    id:'vs',
    url:'//abc.com/a/mp4',
    preloadTime:10
  });

这样就实现了视频在播放过程中永远只预加载10秒的数据,进而保证节省流量。

所有代码来自带解析器、能节省流量的西瓜播放器,Github


铁狮子
在积累中沉淀,在学习中总结,在探索中尝试,只为做到专业

喜欢不断挑战的前端实践者

1.8k 声望
527 粉丝
0 条评论
推荐阅读
领略原生 JavaScript ES6~ES10 的魅力
作为前端开发工程师,盲目追逐框架似乎有点舍本逐末,要知道基本功才是硬核。JavaScript 的语法这几年一直在更新,不管我们是框架的核心开发者还是业务重塑者,学习下最新的 JavaScript 语法和能力是非常有好处的...

快乐动起来46阅读 3.5k

ESlint + Stylelint + VSCode自动格式化代码(2023)
安装插件 ESLint,然后 File -&gt; Preference-&gt; Settings(如果装了中文插件包应该是 文件 -&gt; 选项 -&gt; 设置),搜索 eslint,点击 Edit in setting.json

谭光志34阅读 20.8k评论 9

安全地在前后端之间传输数据 - 「3」真的安全吗?
在「2」注册和登录示例中,我们通过非对称加密算法实现了浏览器和 Web 服务器之间的安全传输。看起来一切都很美好,但是危险就在哪里,有些人发现了,有些人嗅到了,更多人却浑然不知。就像是给门上了把好锁,还...

边城32阅读 7.3k评论 5

封面图
涨姿势了,有意思的气泡 Loading 效果
今日,群友提问,如何实现这么一个 Loading 效果:这个确实有点意思,但是这是 CSS 能够完成的?没错,这个效果中的核心气泡效果,其实借助 CSS 中的滤镜,能够比较轻松的实现,就是所需的元素可能多点。参考我们...

chokcoco24阅读 2.3k评论 3

你可能不需要JS!CSS实现一个计时器
CSS现在可不仅仅只是改一个颜色这么简单,还可以做很多交互,比如做一个功能齐全的计时器?样式上并不复杂,主要是几个交互的地方数字时钟的变化开始、暂停操作重置操作如何仅使用 CSS 来实现这样的功能呢?一起...

XboxYan25阅读 1.7k评论 1

封面图
在前端使用 JS 进行分类汇总
最近遇到一些同学在问 JS 中进行数据统计的问题。虽然数据统计一般会在数据库中进行,但是后端遇到需要使用程序来进行统计的情况也非常多。.NET 就为了对内存数据和数据库数据进行统一地数据处理,发明了 LINQ (L...

边城17阅读 2k

封面图
过滤/筛选树节点
又是树,是我跟树杠上了吗?—— 不,是树的问题太多了!🔗 相关文章推荐:使用递归遍历并转换树形数据(以 TypeScript 为例)从列表生成树 (JavaScript/TypeScript) 过滤和筛选是一个意思,都是 filter。对于列表来...

边城18阅读 7.9k评论 3

封面图

喜欢不断挑战的前端实践者

1.8k 声望
527 粉丝
宣传栏