全面进阶 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 条评论
登录 后参与评论

相关文章

来自专栏desperate633

设计模式之中介者模式(mediator模式)引入中介者模式中介者模式的实例中介者模式分析

大家想象一下有十个人要共同完成一个工作,他们要互相合作和沟通,并且根据对方的通知可能要改变自己的状态,但这通常会带来很多问题,流程过于复杂,使得每个人不仅要专注...

673
来自专栏Android机动车

混合开发之WebView秘笈

注意: 每个 Application 只调用一次 WebSettings.setAppCachePath(),WebSettings.setAppCacheMa...

1053
来自专栏移动端开发

AVFoundation 框架初探究(二)

接着第一篇总结 ----       系列第一篇地址:AVFoundation 框架初探究(一)       在第一篇的文章中,我们总结了主要有下面几个点的知识...

3514
来自专栏前端小吉米

冲顶大会有前端什么事吗?

2675
来自专栏xx_Cc的学习总结专栏

六天完成一个简单iOS App - 第三天

28211
来自专栏c#开发者

Easyui DataGrid DateRange Filter 漂亮实用的日期区间段筛选功能

自定义扩展Jquery easyui datagrid filter组件实现对日期类型区间段的筛选功能。显示效果如一下 ? 是不是非常实用 引用的jquery ...

3597
来自专栏知道一点点

js倒计时,秒倒计时,天倒计时

距某某开幕式还有 [<script language="JavaScript" type="text/javascript">djs()</script>] 天...

731
来自专栏IMWeb前端团队

用Flux实现TodoMVC

本文通过实现一个 TodoMVC 应用来说明一个 Flux 应用的结构是怎样的。本文会告诉你如何一步一步地实现这个应用,完整的源代码可以从 Github 下载。...

1775
来自专栏前端小吉米

无 Flash 时代,让直播拥抱 H5(MSE篇)

2454
来自专栏GuZhenYin

脑洞大开之采用HTML5+SignalR2.0(.Net)实现原生Web视频

前言  - -,我又来了,今天废话不多说,我们直接来实现Web视频聊天. 采用的技术如下: HTML5 WebRTC SignalR2.2.0 localRes...

2045

扫码关注云+社区