https://zhuanlan.zhihu.com/p/26374202

使用 MediaSource 搭建流式播放器

Starkwang

JavaScript 话题的优秀回答者

137 人赞同了该文章

好久好久没有写文章了呀,感觉再不写点啥的话这个专栏就要掉粉了=。=

马上就要正式毕业了,毕业设计是做自适应流媒体相关的优化(说白了就是弄个算法更先进的、码率根据网速自适应的、基于现代浏览器的流式播放器),正好最近这块开始步入正轨了,就介绍一下现在浏览器已经普及的 MediaSource Extension,以及一些实践的细节吧。

一、背景

Media Source Extensions(媒体源扩展)大大地扩展了浏览器的媒体播放功能,提供允许JavaScript 生成媒体流。这可以用于自适应流(adaptive streaming,也是我毕设的研究方向)及随时间变化的视频直播流(live streaming)等应用场景。

在这之前,浏览器提供的媒体播放功能(音频、视频)都是相当简陋的,一个 video 或者 audio 标签再加上相对应的数据 url 就搞定了。

<video src="/xxxx.mp4"></video>

但这缺少了诸如视频分段加载、视频码率切换、部分加载等等现代播放器应该有的功能,所以绝大部分的浏览器视频播放器过去都是基于 Flash 开发的,这也是为什么直到现在2017年中旬,Flash 这个老古董依然在各大视频网站上活着的原因。

如果你是一位经常上 B 站的朋(shēn)友(shì),特别是用 mac 的,一定会注意到 B 站在去年就上线了 HTML5 的播放器,大大缓解了 mac 看 B 站时发热严重的问题。

用 HTML5 播放器替代 Flash,是现在的一大趋势,Flash 过去占据的版图正在逐渐被现代浏览器吞噬,身为前端工程师要做的,就是给 Flash 的坟墓上再加一铲子土吧……

二、MediaSource 的简单使用

在浏览器里,首先我们要判断是否支持 MediaSource:

var supportMediaSource = 'MediaSource' in window

然后就可以新建一个 MediaSource 对象,并且把 mediaSource 作为 objectURL 附加到 video 标签上上:

var mediaSource = new MediaSource()
var video = document.querySelector('video')
video.src = URL.createObjectURL(mediaSource)

接下来就可以监听 mediaSource 上的 sourceOpen 事件,并且设置一个回调:

mediaSource.addEventListener('sourceopen', sourceOpen);
function sourceOpen {// todo...
}

接下来会用到一个叫 SourceBuffer 的对象,这个对象提供了一系列接口,这里用到的是 appendBuffer 方法,可以动态地向 MediaSource 中添加视频/音频片段(对于一个 MediaSource,可以同时存在多个 SourceBuffer)

function sourceOpen () {// 这个奇怪的字符串后面再解释var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'// 新建一个 sourceBuffervar sourceBuffer = mediaSource.addSourceBuffer(mime);// 加载一段 chunk,然后 append 到 sourceBuffer 中fetchBuffer('/xxxx.mp4', buffer => {sourceBuffer.appendBuffer(buffer)})
}// 以二进制格式请求某个url
function fetchBuffer (url, callback) {var xhr = new XMLHttpRequest;xhr.open('get', url);xhr.responseType = 'arraybuffer';xhr.onload = function () {callback(xhr.response);};xhr.send();
}

上面这些代码基本上就是一个最简化的流程了,加载了一段视频 chunk,然后把它『喂』到播放器中。

可以参考:

MediaSource

构建简单的 MPEG-DASH 流媒体播放器

二、实践中的一些坑

1、mime 字符串

mime 字符串指的就是下面这个东西,会在新建 SourceBuffer 中使用到:

var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'
var sourceBuffer = mediaSource.addSourceBuffer(mime);

这个神奇的字符串是什么意思呢?

首先,前面的 video/mp4 代表这是一段 mp4 格式封装的视频,同理也存在类似 video/webmaudio/mpegaudio/mp4 这样的 mime 格式。一般情况下,可以通过 canPlayType 这个方法来判断浏览器是否支持当前格式。

后面的这一段 codecs="...." 比较特别,以逗号相隔,分为两段:

第一段,'avc1.42E01E',即它用于告诉浏览器关于视频编解码的一些重要信息,诸如编码方式、分辨率、帧率、码率以及对解码器解码能力的要求。

在这个例子中,'avc1' 代表视频采用 H.264 编码,随后是一个分隔点,之后是 3 个两位的十六进制的数,这 3 个十六进制数分别代表:

  1. AVCProfileIndication(42
  2. profile_compability(E0
  3. AVCLevelIndication(1E

第一个用于标识 H.264 的 profile,后两个用于标识视频对于解码器的要求。

对于一个 mp4 视频,可以使用 mp4file 这样的命令行工具:

mp4file --dump xxx.mp4

找到 avcC Box 后,就可以看到这三个值:

mp4file --dump movie.mp4
...type avcC (moov.trak.mdia.minf.stbl.stsd.avc1.avcC) // avc1configurationVersion = 1 (0x01)AVCProfileIndication = 66 (0x42)    // 42profile_compatibility = 224 (0xe0)  // E0AVCLevelIndication = 30 (0x1e)      // 1E
...

有一处要注意,后面两个值(profile_compability、AVCLevelIndication)只是浏览器用于判断自身的解码能力能否满足需求,所以不需要和视频完全对应,更高也是可以的。

下面来看 codecs 的第二段 'mp4a.40.2',这一段信息是关于音频部分的,代表视频的音频部分采用了 AAC LC 标准:

'mp4a' 代表此视频的音频部分采用 MPEG-4 压缩编码。

随后是一个分隔点,和一个十六进制数(40),这是 ObjectTypeIndication,40 对应的是 Audio ISO/IEC 14496-3 标准。(不同的值具有不同的含义,详细可以参考官方文档)

然后又是一个分隔点,和一个十进制数(2),这是 MPEG-4 Audio Object Type,维基百科中的解释是 "MPEG-4 AAC LC Audio Object Type is based on the MPEG-2 Part 7 Low Complexity profile (LC) combined with Perceptual Noise Substitution (PNS) (defined in MPEG-4 Part 3 Subpart 4)",具体是什么意思就不翻译了,其实就是一种 H.264 视频中常用的音频编码规范。

这一整段 codecs 都有完善的官方文档,可以参考:

The 'Codecs' and 'Profiles' Parameters for "Bucket" Media Types

2、如何转码出符合标准的视频

目前有很多开源的视频处理工具,比如 FFMPEG、HandBrake,我用的后者转码,前者切割。

转码其实很简单,HandBrake 打开后,加入想要处理的视频(mp4 格式),窗口下半部分 video 标签,H.264 Profile 选择 "Baseline",level 选择 "3.0";audio 标签,选择 Encoder 为 "AAC"。

然后就是把视频切割为比较小的 chunk,ffmpeg 就可以很方便地切割:

ffmpeg -ss 00:00:00 -i source2.mp4 -c copy -t 00:00:05 xxxx.mp4

上面这段命令就切出了视频的第 0 秒到第 5 秒。

注意一个问题,ffmpeg 在切割视频的时候无法做到时间绝对准确,因为视频编码中关键帧(I帧)和跟随它的B帧、P帧是无法分割开的,否则就需要进行重新帧内编码,会让视频体积增大。所以,如果切割的位置刚好在两个关键帧中间,那么 ffmpeg 会向前/向后切割,所以最后切割出的 chunk 长度总是会大于等于应有的长度。

3、向 SourceBuffer 中添加多个 chunk

第一部分的范例中只是请求了一段 chunk 然后加入到播放器里,如果视频很长,存在多个chunk 的话,就需要不停地向 SourceBuffer 中加入新的 chunk。

这里就需要注意一个问题了,即 appendBuffer 是异步执行的,在完成前,不能 append 新的 chunk:

sourceBuffer.appendBuffer(buffer1)
sourceBuffer.appendBuffer(buffer2)// Uncaught DOMException: Failed to set the 'timestampOffset' property on 'SourceBuffer': This SourceBuffer is still processing an 'appendBuffer' or 'remove' operation.

而是应该监听 SourceBuffer 上的 updateend 事件,确定空闲后,再加入新的 chunk:

sourceBuffer.addEventListener('updateend', () => {// 这个时候才能加入新 chunk// 先设定新chunk加入的位置,比如第20秒处sourceBuffer.timestampOffset = 20// 然后加入sourceBuffer.append(newBuffer)
}

4、码率自适应算法

对于随时变化的网络情况,我们会根据情况加载不同码率的视频,这里就需要一些控制算法决定当前加载哪个码率的视频。

很容易就能想到一种简单的算法:上一段 chunk 加载完后,计算出加载速度,从而决定下一个 chunk 的码率。

但这种朴素的算法有很严重的问题,即它假设网络是相当稳定的,我们可以根据当前的信息预测出未来的网速。但这已经被大量的统计数据证明是不现实的,换句话说,我们没办法预测未来的网络环境。

所以学术界提出了一系列新的算法,比如最常见的 Buffer-Based 算法,即根据当前缓冲区里视频的长度,决定下一个 chunk 的码率。如果有很长的缓冲,那么就加载高码率的视频。这在相当于一个积分控制器,把时刻变化的无法预测的网络环境在时间维度上积分,以获得一个更平缓更能预测的函数。

但是 Buffer-Based 算法依然有问题,在视频起步阶段,缓冲区里的视频很短,导致无论网络环境如何,起步阶段的码率都是很低的。所以 Buffer-Based 算法只适用于视频 startup 后的稳态阶段。在起步阶段,依然有很多优化的空间,这也不是本文的重点,具体就不再详述了。

编辑于 2017-04-16

前端开发

多媒体播放器(软件)

​赞同 137​​11 条评论

​分享

​收藏

使用 MediaSource 搭建流式播放器相关推荐

  1. 搭建webassembly网页播放器(五)---网页播放器开发

    在前面的章节中,我们解决emcc环境以及使用emcc来编译ffmpeg得到网页开发中可以使用的js库,本章节,我们就来实现一个简单的播放器. 视频课程以及源码下载: https://edu.csdn. ...

  2. 搭建webassembly网页播放器(六)---websocket后台服务程序

    这里我们主要介绍后台搭建技术,前端搭建好后,需要后端配置展现,实现的效果就是 :网页连接上我们的 websocket服务后,我们就从H264文件中不断的提取出H264帧,然后传递给前端,前端调用web ...

  3. 搭建webassembly网页播放器(三)---emcc编译ffmpeg编译库

    经过前面的2个部分,我们完成了环境的搭建,接下来我们使用emcc编译ffmpeg库,注意同样由于软链接的缘故,我们不能在windows和linux的共享文件夹里使用emcc编译ffmpeg, 这里我们 ...

  4. 搭建webassembly网页播放器(二)---emcc环境搭建

    emcc全称 emscripten,最重要的功能就是让网页js调用c/c++ 成为可能,是我们基于webassembly搭建网页播放器必须依赖的编译工具. emcc官网的搭建教程较为简单,安装过程中最 ...

  5. 流式断言器AssertJ介绍

    本文来自网易云社区 作者:范旭斐 大家在使用testng.junit做自动化测试的过程中,经常会用到testng.junit自带的断言器,有时候对一个字符串.日期.列表进行断言很麻烦,需要借助到jdk ...

  6. 魔坊APP项目-27-直播、客户端中调整窗口大小、能播放rtmp格式直播流的播放器模块

    直播 客户端中直播的界面调整和当前窗口一致 live_list.html <!DOCTYPE html> <html lang="en"> <head ...

  7. H5流式播放(FMP4转封装与mediaSource)

    mediaSource接口的介绍 W3C上有明确关于mediaSource 扩展接口的文档.mediaSource 扩展文档中是这么定义的, 它允许JS脚本动态构建媒体流用于和,允许JS传送媒体块到H ...

  8. 用QT搭建简单的播放器外壳

    用QT来搭建一个简易的播放器的外壳,除了一个框框用来显示视频之外,前进按钮,快退按钮,播放/暂停按钮,停止按钮,和一个选择文件的按钮. 没有什么太重点的,主要就是熟悉一下QT的基本操作,在选择文件上比 ...

  9. 我使用的IJK拉流直播播放器

    无意间发现有个群主(小路飞)写了一个直播的sdk(可自定义界面) 548545202 下载好sdk先导入项目,至于过程就不过多的描述. 导入完之后在项目里面的build.grable配置下面的信息(里 ...

最新文章

  1. 智能边缘计算:计算模式的再次轮回
  2. java二个整数相减_Java-消息框显示两整数加减乘除
  3. jpa配置映射包_JPA – Hibernate –包级别的类型映射
  4. fceux模拟器linux,超强FC模拟器fceux-2.2.3最新版
  5. PostgreSQL 中的递归查询 与oracle 的比较
  6. 【14:00开播】国际顶会强力加持,算法“视”界杯巅峰之战拉开帷幕!
  7. 程序员如何从0到1搭建自己的技术博客
  8. 白板推导系列Pytorch实现-感知机算法
  9. 真格基金王强:判断一个项目好坏,我会做五个思考!
  10. ModBus RTU和ModBus ASC
  11. 状态方程simulink仿真_推荐几本关于制冷仿真的书籍|压缩机|制冷剂|热泵
  12. To prevent a memory leak the JDBC Driver has been forcibly
  13. 实现点击桌面的一个图标打开多个选择画面
  14. python3GUI--微博图片爬取工具V1.5 By:PyQt5(附源码)
  15. Python 写入csv中文乱码问题
  16. 使用Nexus添加jar包到私服里
  17. SQLServer按照每小时、半小时分组
  18. 高维向量的相似度判定-距离产生了,美还在吗
  19. thingsboard物联网平台编译安装一步到位
  20. 零基础制作【武林外传】辅助工具(一)

热门文章

  1. 儿童机器人课程的学习
  2. 第一天:Hbase 概述
  3. 复杂美入选2022中国产业区块链企业100强
  4. Codeforces 940E Cashback
  5. 【Github】使用github
  6. border-image-slice
  7. Qt4.8.5——QWSServer
  8. Vue 路由导航守卫(全局守卫、路由独享守卫、组件内守卫)详解
  9. 40岁以上的程序员该怎么办?年龄不是问题
  10. 最新Java设计模式之美