MSE 可以说W3C最重要的一个媒体源扩展标准。假如没有 MSE 的存在,那么什么hls.js,flv.js,dash.js都是空谈。因为 MSE,才使得媒体流能根据需要进行更精准地控制。这是w3c的文档Media Source Extensions™。
介绍
早先的媒体流还是一直依赖Flash,通过rtmp或者http-flv等协议进行视频串流,再到 Flash 播放器。
然而有了MSE(media souce extensions)之后,情况就不一样了。我们可以引用MediaSource
对象,你可以把它看成一个容器,一个引用多个SourceBuffer
对象的容器。那么在上面我们就提到,MSE能够根据需要对媒体流进行更精准的控制,例如根据网络情况进行切码率不同的媒体流,或是根据内存占用情况释放之前播放过的媒体流等等。
这是一张Media Source的结构图:
MediaSource
Media Source Extensions API
的MediaSource
接口表示HTMLMediaElement
对象的媒体数据源。MediaSource
对象可以附加到HTMLMediaElement
在客户端中播放。
状态
1 | enum ReadyState { |
错误
1 | enum EndOfStreamError { |
基本结构
1 | [Constructor] |
属性
sourceBuffers:只读,返回
SourceBufferList
。包含与此MediaSource
关联的SourceBuffer
对象的列表。1
2
3
4
5
6// 伪代码
if mediaSource.readyState === 'close'
return SourceBufferList(0)
else if mediaSource.readyState === 'open'
// length = mediaSource.addSourceBuffer(SourceBuffer).length
return SourceBufferList(length);activeSourceBuffers:只读,返回
SourceBufferList
。包含了所选中的video track,启用中的audio track以及显示或隐藏的text track的sourceBuffers
的子集。此列表中的SourceBuffer对象必须按照它们出现在sourceBuffers
属性 中的顺序出现; 例如,如果只有sourceBuffers [0]和sourceBuffers [3]在activeSourceBuffers
中,则activeSourceBuffers [0]必须等于sourceBuffers [0],activeSourceBuffers [1]必须等于sourceBuffers [3]。readyState:只读,返回
ReadyState
。MediaSource
对象的当前状态。当创建媒体源时,必须将“readyState”设置为“close”。duration:返回double。当
MediaSource
对象创建时,duration的初始化值为NaN。获取时,执行以下的步骤:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 伪代码
// getter
if mediaSource.readyState === 'close'
return NaN
return dutation
// setter
if new_duration < 0 ||isNaN(new_duration)
throw TypeError
else if mediaSource.readyState !== 'open'
throw InvalidStateError
else if anyOneSourceBuffer(sourceBuffers).updating === true
throw InvalidStateError
else
run duration_change_algorithm
// 注意:
1、如果当前buffer编码帧有较高的结束时间,则duration_change_algorithm也会将duration调整得相对比较高;
2、appendBuffer()和endOfStream()可以在某些情况下更新持续时间。onsourceopen,onsourceended,onsourceclose:
EventHandler
类型。
方法
addSourceBuffer:添加新的
SourceBuffer
到sourceBuffers
。1
.addSourceBuffer(type: DOMString): SourceBuffer
假如方法被调用,则会有以下的执行步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 伪代码
if type === ''
throw TypeError
else if isNotSupportedMIME(type)
throw NotSupportedError
else if canNotHandlerMoreSourceBuffer ||
(sb = addSourceBuffer(type) &&
isNotSupportedSourceBufferConfiguration(sb))
// SourceBuffer Configuration:1、具有1个audio track和/或1个video track的单个SourceBuffer;2、两个SourceBuffers,一个处理单个audio track,另一个处理单个video track
// 举个例子,`HTMLMediaElement`中的`readyState`为[HAVE_METADATA](https://www.w3.org/TR/html51/semantics-embedded-content.html#dom-htmlmediaelement-have_metadata),假如浏览器不支持在播放过程中添加更多的track,那么则有可能会抛出这种异常。
throw QuotaExceededError
else if mediaSource.readyState !== 'open'
throw InvalidStateError
sb = new SourceBuffer
init generate_timestamps_flag
if generate_timestamps_flag === true
sb.mode = 'sequence'
else
sb.mode = 'segments'
sourceBuffers.addSourceBuffer(sb)
sourceBuffers.fireEvent('sourceBuffers');removeSourceBuffer:从
sourceBuffsers
删除SourceBuffer
。执行步骤:1
.removeSourceBuffer(sourceBuffer: SourceBuffer): void
假如方法被调用,则会有以下的执行步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 伪代码
if sourceBuffer not in sourceBuffers
throw NotFoundError
else if sourceBuffer.updating === true
if buffer_append_algorithm.isRunning === true return
sourceBuffer.updating = false
sourceBuffer.fireEvent('abort')
sourceBuffer.fireEvent('updateend')
sourceBufferAudioTracks = sourceBuffer.audioTracks
sourceBufferVideoTracks = sourceBuffer.videoTracks
sourceBufferTextTracks = sourceBuffer.textTracks
if sourceBufferAudioTracks.length > 0 || sourceBufferVideoTracks.length > 0 || sourceBufferTextTracks.length > 0
for i in sourceBufferAudioTracks/sourceBufferVideoTracks/sourceBufferTextTracks
sourceBufferAudioTracks[i].sourceBuffer = null
sourceBufferVideoTracks[i].sourceBuffer = null
sourceBufferTextTracks[i].sourceBuffer = null
HTMLMediaElement.removeTracks(sourceBufferAudioTracks[i], sourceBufferVideoTracks[i], sourceBufferTextTracks[i])
sourceBuffer.removeTracks(sourceBufferAudioTracks[i], sourceBufferVideoTracks[i], sourceBufferTextTracks[i])
if sourceBuffer in activeSourceBuffers
activeSourceBuffers.remove(sourceBuffer)
activeSourceBuffers.fireEvent('removesourcebuffer')
destroy sourceBuffer.resourceendOfStream:表示媒体流结束。
1
.endOfStream(error?: EndOfStreamError): void
假如方法被调用,则会有以下的执行步骤:
1
2
3
4
5
6
7// 伪代码
if mediaSource.readyState !== 'open'
throw InvalidStateError
else if anyOneSourceBuffer(sourceBuffers).updating === true
throw InvalidStateError
run end_of_stream_algorithmsetLiveSeekableRange:更新
HTMLMediaElement Extensions
中的可seek范围,从而达到修改HTMLMediaElement.seekable
。1
.setLiveSeekableRange(start: double, end: double): void
假如方法被调用,则会有以下的执行步骤:
1
2
3
4
5
6
7
8// 伪代码
if mediaSource.readyState !== 'open'
throw InvalidStateError
else if start < 0 || start > end
throw TypeError
live_seekable_range = new TimeRanges(end - start)
HTMLMediaElement.seekable = live_seekable_rangeclearLiveSeekableRange:顾名思义。
1
.clearLiveSeekableRange(): void
假如方法被调用,则会有以下的执行步骤:
1
2
3
4
5// 伪代码
if mediaSource.readyState !== 'open'
throw InvalidStateError
new TimeRanges()isTypeSupported:static。检查
MediaSource
是否能够为指定的MIME类型创建SourceBuffer
对象。可以通过Media MIME Support来查看当前使用的浏览器对不同的MIME支持程序。或者通过ffmpeg -i test.mp4
得到的信息去匹配Video type parameters - WHATWG Wiki。1
MediaSource.isTypeSupported(type: DOMString): void
事件
1 | sourceopen | Event | readyState = 'closed' => 'open' / 'ended' => 'open' |
SourceBuffer
添加模式
1 | enum AppendMode { |
基础结构
1 | interface SourceBuffer : EventTarget { |
属性
mode:当使用
MediaSource.addSourceBuffer()
创建SourceBuffer
时,会给mode设置初始值。如果media segment已经存在时间戳,则该值将被设置为‘segments’
,反之则sequence
。当设置时,会执行以下步骤:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 伪代码如下
if sourceBuffer not in mediaSource.sourceBuffers
throw InvalidStateError
else if updating === true
throw InvalidStateError
else if generate_timestamps_flag === true && newMode === "segments"
throw TypeError
else if mediaSource.readyState === 'ended'
mediaSource.readyState = 'open'
mediaSource.dispatch('sourceopen')
else if append_state === PARSING_MEDIA_SEGMENT
throw InvalidStateError
else if new_mode === "sequence"
group_start_timestamp = group_end_timestamp
else
sourceBuffer.mode = new_modeupdating:只读,boolean类型。当执行异步操作
appendBuffer()
或remove()
时,返回true;反之false;buffered:只读,返回
TimeRanges
。表明了SourceBuffer
中缓冲了哪些TimeRanges
。创建该SourceBuffer
对象时,该属性最初设置为空的TimeRanges
对象。timestampOffset:double类型。主要是控制media segment片段之间的偏移量,也可以说是上一个的duration,初始值为0。当设置时,会执行一下步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 伪代码如下
if sourceBuffer not in mediaSource.sourceBuffers
throw InvalidStateError
else if sourceBuffer.updating === true
throw InvalidStateError
else if mediaSource.readyState === 'ended'
mediaSource.readyState = 'open'
mediaSource.dispatch('sourceopen')
else if append_state === PARSING_MEDIA_SEGMENT
throw InvalidStateError
else if mode === "sequence"
group_start_timestamp = new_timestampOffset
sourceBuffer.timestampOffset = new_timestampOffsetaudioTracks,videoTracks,textTracks:分别返回
AudioTrackList
,VideoTrackList
和TextTrackList
。onupdatestart,onupdate,onupdateend,onerror,onabort:
EventHandler
类型。
方法
appendBuffer:将
BufferSource
中的数据添加到source buffer。1
.appendBuffer(data: BufferSource): void
假如方法被调用,则会有以下的执行步骤:
1
2
3
4
5
6// 伪代码
run prepare_append_algorithm
inputBuffer.push(data)
sourceBuffer.updating = true
sourceBuffer.fireEvent('updatestart')
async run buffer_append_algorithmabort:中止并重置解析器。
1
.abort(): void
假如方法被调用,则会有以下的执行步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15if sourceBuffer not in mediaSource.sourceBuffers
throw InvalidStateError
else if mediaSource.readyState !== 'open'
throw InvalidStateError
else if range_removal_algorithm.isRunning === true
throw InvalidStateError
else if sourceBuffer.updating === true
if buffer_append_algorithm.isRunning === true
buffer_append_algorithm.abort()
sourceBuffer.updating = false
sourceBuffer.fireEvent('abort')
sourceBuffer.fireEvent('updateend')
run reset_parser_state_algorithm
sourceBuffer.ptsStart = sourceBuffer.appendWindowStart
sourceBuffer.appendWindowEnd = Infinityremove:删除特定时间段的媒体。参数中的单位为s。
1
.remove(start: double, end: unrestricted double): void
假如方法被调用,则会有以下的执行步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15if sourceBuffer not in mediaSource.sourceBuffers
throw InvalidStateError
else if sourceBuffer.updating === true
throw InvalidStateError
else if isNaN(sourceBuffer.duration)
throw TypeError
else if start < 0 || start > sourceBuffer.duration
throw TypeError
else if end <= start || is(NaN)
throw TypeError
else if mediaSource.readySate === 'ended'
mediaSource.readySate = 'open'
mediaSource.fireEvent('sourceopen')
run range_removal_algorithm(start, end)
Track Buffers
Track Buffer为每一个独立的track储存对应的描述和编码帧。将 track buffer 更新为初始化片段,并将媒体片段添加到 SourceBuffer 。
每一个Track都拥有以下属性:
- last decode timestamp:储存着当前解码帧组里面的最后一帧的dts。当未添加到SourceBuffer时,该值为空。
- last frame duration:储存着当前解码帧组里面的最后一帧的帧时长。当未添加到SourceBuffer时,该值为空。
- highest end timestamp:储存着当前解码帧组中已被添加到SourceBuffer的所有编码帧中最高的结束时间戳。同样初始值未空。
- need random access point flag:该变量初始设置为true,表示在有什么添加到track buffer之前,都需要随机访问点编码帧。
- track buffer ranges:表示当前存储在track buffer中的编码帧占用的pts范围。这个我在数据转化的音视频同步原理那里其实有说过,其中的duration就是该值。
事件
1 | updatestart | Event | updating = false => true |
其他的就不多说了。基本上都是可以从官方文档上找得到答案的。
总结
总结就是如下的操作顺序,但实际情况远不止如此,还复杂很多,得依据场景来分析。
在页面的 HTML 部分中定义
HTMLMediaElement
元素。1
<video id='vEle' autoplay/>
使用JavaScript创建
MediaSource
对象。1
var mediaSource = new window.MediaSource()
使用
URL.createObjectURL
创建 BlobURL,并将 MediaSource 对象作为源。1
var url = URL.createObjectURL(mediaSource);
将虚拟 URL 分配到视频元素的 src 属性。
1
2v = document.getElementById('vEle');
v.src = url;使用 addSourceBuffer 创建 SourceBuffer,包含你添加的 MIME 类型的视频。
1
var videoSource = mediaSource.addSourceBuffer('video/mp4');
从媒体文件联机获取视频
initialization segment
,并使用appendBuffer
将其添加到SourceBuffer
中。而initialization segment
就是相当于fmp4中的ftyp + moov
,有trackId, duration, width, height等基础信息。1
videoSource.appendBuffer(initialization_segment)
从媒体文件获取视频
media segment
,并使用appendBuffer
将其附加到SourceBuffer
中。而media segment
就相当于fmp4中的moof + mdat
,是实实在在的音视频数据。1
videoSource.appendBuffer(media_segment)
在 video 元素上调用 play 方法。
1
2
3
4
5
6
7var playPromise = v.play()
if (playPromise !== undefined) {
playPromise.then(_ => {
v.pause()
}).catch(error => { })
}重复步骤 7 直到完成。
- 清除。
1
videoSource.remove(start, end)