全面进阶 H5 直播(下)

本文作者:ivweb villainthr

接 《 全面进阶 H5 直播(上)

Media Source Extensions

在没有 MSE 出现之前,前端对 video 的操作,仅仅局限在对视频文件的操作,而并不能对视频流做任何相关的操作。现在 MSE 提供了一系列的接口,使开发者可以直接提供 media stream。

那 MSE 是如何完成视频流的加载和播放呢?

入门实例

这可以参考 google 的 MSE 简介

 var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log("The Media Source Extensions API is not supported.")
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp9"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function(e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

可以从上面的代码看出,一套完整的执行代码,不仅需要使用 MSE 而且,还有一下这些相关的 API。

  • HTMLVideoElement.getVideoPlaybackQuality()
  • SourceBuffer
  • SourceBufferList
  • TextTrack.sourceBuffer
  • TrackDefault
  • TrackDefaultList
  • URL.createObjectURL()
  • VideoPlaybackQuality
  • VideoTrack.sourceBuffer

我们简单讲解一下上面的流程。根据 google 的阐述,整个过程可以为:

  • 第一步,通过异步拉取数据。
  • 第二步,通过 MediaSource 处理数据。
  • 第三步,将数据流交给 audio/video 标签进行播放。

而中间传递的数据都是通过 Buffer的形式来进行传递的。

中间有个需要注意的点,MS 的实例通过 URL.createObjectURL() 创建的 url 并不会同步连接到 video.src。换句话说,URL.createObjectURL() 只是将底层的流(MS)和 video.src 连接中间者,一旦两者连接到一起之后,该对象就没用了。

那么什么时候 MS 才会和 video.src 连接到一起呢?

创建实例都是同步的,但是底层流和 video.src 的连接时异步的。MS 提供了一个sourceopen 事件给我们进行这项异步处理。一旦连接到一起之后,该 URL object 就没用了,处于内存节省的目的,可以使用 URL.revokeObjectURL(vidElement.src) 销毁指定的

URL object。 
mediaSource.addEventListener('sourceopen', sourceOpen);

function sourceOpen(){
    URL.revokeObjectURL(vidElement.src)
}

MS 对流的解析

MS 提供了我们对底层音视频流的处理,那一开始我们怎么决定以何种格式进行编解码呢?

这里,可以使用addSourceBuffer(mime)来设置相关的编码器:

var mime = 'video/webm; codecs="opus, vp9"';  
  var sourceBuffer = mediaSource.addSourceBuffer(mime);

然后通过,异步拉取相关的音视频流:

fetch(url)
.then(res=>{
    return res.arrayBuffer();
})
.then(buffer=>{
    sourceBuffer.appendBuffer(buffer);
})

如果视频已经传完了,而相关的 Buffer 还在占用内存,这时候,就需要我们显示的中断当前的 Buffer 内容。那么最终我们的异步处理结果变为:

fetch(url)
.then(res=>{
    return res.arrayBuffer();
})
.then(function(arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function(e) {
      // 是否有持续更新的流
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
        // 没有,则中断连接
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });

上面我们大致了解了一下关于 Media Source Extensions 的大致流程,但里面的细节我们还没有细讲。接下来,我们来具体看一下 MSE 一篮子的生态技术包含哪些内容。首先是,MediaSource

MediaSource

MS(MediaSource) 可以理解为多个视频流的管理工具。以前,我们只能下载一个清晰度的流,并且不能平滑切换低画质或者高画质的流,而现在我们可以利用 MS 实现这里特性。我们先来简单了解一下他的 API。

MS 的创建

创建一个 MS: var mediaSource = new MediaSource();

相关方法

addSourceBuffer()

该是用来返回一个具体的视频流,接受一个 mimeType 表示该流的编码格式。例如:

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

sourceBuffer 是直接和视频流有交集的 API。例如:

function sourceOpen (_) {
  var mediaSource = this;
  var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
  fetchAB(assetURL, function (buf) {
    sourceBuffer.addEventListener('updateend', function (_) {
      mediaSource.endOfStream();
      video.play();
    });
    // 通过 fetch 添加视频 Buffer
    sourceBuffer.appendBuffer(buf);
  });
};

它通过appendBuffer直接添加视频流,实现播放。不过,在使用 addSourceBuffer 创建之前,还需要保证当前浏览器是否支持该编码格式。

removeSourceBuffer()

用来移除某个 sourceBuffer。移除也主要是考虑性能原因,将不需要的流移除以节省相应的空间,格式为: mediaSource.removeSourceBuffer(sourceBuffer);

endOfStream()

用来表示接受的视频流的停止,注意,这里并不是断开,相当于只是下好了一部分视频,然后你可以进行播放。此时,MS 的状态变为:ended。例如:

 var mediaSource = this;
  var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
  fetchAB(assetURL, function (buf) {
    sourceBuffer.addEventListener('updateend', function (_) {
      mediaSource.endOfStream(); // 结束当前的接受
      video.play(); // 可以播放当前获得的流
    });
    sourceBuffer.appendBuffer(buf);
  });

isTypeSupported()

该是用来检测当前浏览器是否支持指定视频格式的解码。格式为: var isItSupported = mediaSource.isTypeSupported(mimeType); // 返回值为 Boolean

mimeType 可以为 type 或者 type + codec。

例如:

 // 不同的浏览器支持不一样,不过基本的类型都支持。
MediaSource.isTypeSupported('audio/mp3'); // false,这里应该为 audio/mpeg 
MediaSource.isTypeSupported('video/mp4'); // true
MediaSource.isTypeSupported('video/mp4; codecs="avc1.4D4028, mp4a.40.2"'); // true

这里有一份具体的 mimeType 参考列表。

MS 的状态

当 MS 从创建开始,都会自带一个readyState 属性,用来表示其当前打开的状态。MS 有三个状态:

  • closed: 当前 MS 没有和 media element(比如:video.src) 相关联。创建时,MS 就是该状态。
  • open: source 打开,并且准备接受通过 sourceBuffer.appendBuffer 添加的数据。
  • ended: 当 endOfStream() 执行完成,会变为该状态,此时,source 依然和 media element 连接。 var mediaSource = new MediaSource; mediaSource.readyState; // 默认为 closed 当由 closed 变为 open 状态时,需要监听 sourceopen 事件。 video.src = URL.createObjectURL(mediaSource); mediaSource.addEventListener('sourceopen', sourceOpen); MS 针对这几个状态变化,提供了相关的事件:sourceopensourceendedsourceclose
  • sourceopen: 当 "closed" to "open" 或者 "ended" to "open" 时触发。
  • sourceended: 当 "open" to "ended" 时触发。
  • sourceclose: 当 "open" to "closed" 或者 "ended" to "closed" 时触发。

MS 还提供了其他的监听事件 sourceopen,sourceended,sourceclose,updatestart,update,updateend,error,abort,addsourcebuffer,removesourcebuffer. 这里主要选了比较重要的,其他的可以参考官方文档。

MS 属性

比较常用的属性有: duration,readyState。

  • duration: 获得当前媒体播放的时间,既可以设置(get),也可以获取(set)。单位为 s(秒) mediaSource.duration = 5.5; // 设置媒体流播放的时间 var myDuration = mediaSource.duration; // 获得媒体流开始播放的时间 在实际应用中为: sourceBuffer.addEventListener('updateend', function (_) { mediaSource.endOfStream(); mediaSource.duration = 120; // 设置当前流播放的时间 video.play(); });
  • readyState: 获得当前 MS 的状态。取值上面已经讲过了:closedopenendedvar mediaSource = new MediaSource; //此时的 mediaSource.readyState 状态为 closed 以及: sourceBuffer.addEventListener('updateend', function (_) { mediaSource.endOfStream(); // 调用该方法后结果为:ended video.play(); }); 除了上面两个属性外,还有 sourceBuffersactiveSourceBuffers这两个属性。用来返回通过 addSourceBuffer() 创建的 SourceBuffer 数组。这没啥过多的难度。

接下来我们就来看一下靠底层的sourceBuffer

SourceBuffer

SourceBuffer 是由mediaSource 创建,并直接和 HTMLMediaElement接触。简单来说,它就是一个流的容器,里面提供的 append()remove()来进行流的操作,它可以包含一个或者多个 media segments。同样,接下来,我们再来看一下该构造函数上的基本属性和内容。

基础内容

前面说过 sourceBuffer 主要是一个用来存放流的容器,那么,它是怎么存放的,它存放的内容是啥,有没有顺序等等。这些都是 sourceBuffer 最最根本的问题。OK,接下来,我们来看一下的它的基本架构有些啥。

参考 W3C,可以基本了解到里面的内容为:

 interface SourceBuffer : EventTarget {
             attribute AppendMode          mode;
    readonly attribute boolean             updating;
    readonly attribute TimeRanges          buffered;
             attribute double              timestampOffset;
    readonly attribute AudioTrackList      audioTracks;
    readonly attribute VideoTrackList      videoTracks;
    readonly attribute TextTrackList       textTracks;
             attribute double              appendWindowStart;
             attribute unrestricted double appendWindowEnd;
             attribute EventHandler        onupdatestart;
             attribute EventHandler        onupdate;
             attribute EventHandler        onupdateend;
             attribute EventHandler        onerror;
             attribute EventHandler        onabort;
    void appendBuffer(BufferSource data);
    void abort();
    void remove(double start, unrestricted double end);
};

上面这些属性决定了其 sourceBuffer 整个基础。

首先是 mode。上面说过,SB(SourceBuffer) 里面存储的是 media segments(就是你每次通过 append 添加进去的流片段)。SB.mode 有两种格式:

  • segments: 乱序排放。通过 timestamps 来标识其具体播放的顺序。比如:20s的 buffer,30s 的 buffer 等。
  • sequence: 按序排放。通过 appendBuffer 的顺序来决定每个 mode 添加的顺序。timestamps 根据 sequence 自动产生。

那么上面两个哪个是默认值呢?

看情况,讲真,没骗你。

media segments 天生自带timestamps,那么 mode 就为 segments ,否则为 sequence。所以,一般情况下,我们是不用管它的值。不过,你可以在后面,将 segments 设置为 sequence 这个是没毛病的。反之,将 sequence 设置为 segments 就有问题了。

var bufferMode = sourceBuffer.mode;
if (bufferMode == 'segments') {
  sourceBuffer.mode = 'sequence';
}

然后另外两个就是 buffered 和 updating。

  • buffered:返回一个 timeRange 对象。用来表示当前被存储在 SB 中的 buffer。
  • updating: 返回 Boolean,表示当前 SB 是否正在被更新。例如: SourceBuffer.appendBuffer(), SourceBuffer.appendStream(), SourceBuffer.remove() 调用时。

另外还有一些其他的相关属性,比如 textTracks,timestampOffset,trackDefaults,这里就不多说了。实际上,SB 是一个事件驱动的对象,一些常见的处理,都是在具体的事件中完成的。那么它又有哪些事件呢?

事件触发

在 SB 中,相关事件触发包括:

  • updatestart: 当 updating 由 false 变为 true。
  • update:当 append()/remove() 方法被成功调用完成时,updating 由 true 变为 false。
  • updateend: append()/remove() 已经结束
  • error: 在 append() 过程中发生错误,updating 由 true 变为 false。
  • abort: 当 append()/remove() 过程中,使用 abort() 方法废弃时,会触发。此时,updating 由 true 变为 false。

注意上面有两个事件比较类似:updateupdateend。都是表示处理的结束,不同的是,update 比 updateend 先触发。

sourceBuffer.addEventListener('updateend', function (e) {
    // 当指定的 buffer 加载完后,就可以开始播放
      mediaSource.endOfStream();
      video.play();
    });

相关方法

SB 处理流的方法就是 +/- : appendBuffer, remove。另外还有一个中断处理函数 abort()

  • appendBuffer(ArrayBuffer):用来添加 ArrayBuffer。该 ArrayBuffer 一般是通过 fetch 的 response.arrayBuffer(); 来获取的。
  • remove(start, end): 用来移除具体某段的 media segments。 @param start/end: 都是时间单位(s)。用来表示具体某段的 media segments 的范围。
  • abort(): 用来放弃当前 append 流的操作。不过,该方法的业务场景也比较有限。它只能用在当 SB 正在更新流的时候。即,此时通过 fetch,已经接受到新流,并且使用 appendBuffer 添加,此为开始的时间。然后到 updateend 事件触发之前,这段时间之内调用abort()。有一个业务场景是,当用户移动进度条,而,此时 fetch 已经获取前一次的 media segments,那么可以使用 abort放弃该操作,转而请求新的 media segments。具体可以参考:abort 使用

上面主要介绍了处理音视频流需要用的 Web 技术,后面章节,我们接入实战,具体来讲一下,如何做到使用 MSE 进行 remux 和 demux。

原文链接:http://www.ivweb.io/topic/58de6dcb4813a86006c9052a

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Jerry的SAP技术分享

CRM WebClient UI和Hybris里工作中心跳转的url生成逻辑

把Work center的navigation target在client side不可见:在Chrome development tool里看不到,而是点击了...

3554
来自专栏aoho求索

由Consul谈到Raft

在前一篇文章consul配置与实战中,介绍了consul的一些内幕及consul配置相关,并对项目中的一些实际配置进行展示。这篇文章重点介绍consul中所涉及...

3938
来自专栏游戏杂谈

设置Images.xcassets后启动仍旧黑屏的问题

换了XCode6之后,发现以前写Icon-76.png这样很麻烦,就使用了Image.xcassets,公司所有测试机跑了一下,发现没什么问题。唯独有一台设置有...

1112
来自专栏林德熙的博客

win10 uwp 使用油墨输入 保存,修改,加载inkUWP 手写清理笔画手写识别无法识别手写语音

现在很多人还是使用笔和纸来记录,那么可以在电脑输入方式和之前使用的方式一样,很多用户觉得会方便。在win10 我们有一个简单的方法去让用户输入,InkCanva...

1801
来自专栏Java帮帮-微信公众号-技术文章全总结

学Java-Spring使用Quartz任务调度定时器

睁开眼看一看窗外的阳光,伸一个懒腰,拿起放在床一旁的水白开水,甜甜的味道,晃着尾巴东张西望的猫猫,在窗台上舞蹈。你向生活微笑,生活也向你微笑。 请你不要询问我...

3053
来自专栏北京马哥教育

LaTeXila:Linux 的多语言 LaTeX 编辑器简介

豌豆贴心提醒,本文阅读时间7分钟 LaTeXila 是一个多语言 LaTeX 编辑器,专为那些偏爱 GTK+ 外观的 Linux 用户设计。这个软件除了操作简...

3489
来自专栏JavaEdge

JDK7新特性概览JSR292:支持动态类型语言(InvokeDynamic)G1 垃圾回收器(Garbage-First Collector)JSR334:小的语言改进(Project Coin)核

61310
来自专栏coding

听说,撸代码,ide与vim更配哦vim折腾记vim常用命令

在选择编辑器上面,我是一个纠结的人,曾经年少的我执着地追求一款万能的编辑器,可以支持所有编辑语言,灵活可定制,可纯粹用键盘操作。符合这种条件的编辑器,非vim莫...

922
来自专栏前端知识分享

第151天:网页中插入百度地图方法(不需要密钥)

 以上就是网页中插入百度地图的方法,做企业站,总是要插入百度地图,以后再也不用愁了。

5282
来自专栏哲学驱动设计

产品前端重构(TypeScript、MVC框架设计)

最近两周完成了对公司某一产品的前端重构,本文记录重构的主要思路及相关的设计内容。 公司期望把某一管理类信息系统从项目代码中抽取、重构为一个可复用的产品。该系统的...

2408

扫码关注云+社区