Lucius's Blog

音视频学习-MSE基础原理篇

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 APIMediaSource接口表示HTMLMediaElement对象的媒体数据源。MediaSource对象可以附加到HTMLMediaElement在客户端中播放。

状态

1
2
3
4
5
6
7
8
enum ReadyState {
// 说明媒体源当前未附加到媒体元素(HTMLMediaElement)
"closed",
// 媒体源已被媒体元素打开,并准备好将数据添加到sourceBuffers中的SourceBuffer对象
"open",
// 媒体源仍然附加到一个媒体元素上,但endOfStream()已被调用
"ended"
};

错误

1
2
3
4
enum EndOfStreamError {
"network", // 网络错误
"decode" // 解码错误
};

基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Constructor]
interface MediaSource : EventTarget {
readonly attribute SourceBufferList sourceBuffers;
readonly attribute SourceBufferList activeSourceBuffers;
readonly attribute ReadyState readyState;
attribute unrestricted double duration;
attribute EventHandler onsourceopen;
attribute EventHandler onsourceended;
attribute EventHandler onsourceclose;
SourceBuffer addSourceBuffer(DOMString type);
void removeSourceBuffer(SourceBuffer sourceBuffer);
void endOfStream(optional EndOfStreamError error);
void setLiveSeekableRange(double start, double end);
void clearLiveSeekableRange();
static boolean isTypeSupported(DOMString type);
};

属性

  • 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:只读,返回ReadyStateMediaSource对象的当前状态。当创建媒体源时,必须将“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:添加新的SourceBuffersourceBuffers

    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.resource
  • endOfStream:表示媒体流结束。

    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_algorithm
  • setLiveSeekableRange:更新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_range
  • clearLiveSeekableRange:顾名思义。

    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sourceopen  | Event | readyState = 'closed' => 'open' / 'ended' => 'open'
// sourceopen
let mediaSource = new MediaSource();
videoElement.src = URL.createObjectURL(mediaSource);
---------------------------------------------------
sourceend | Event | readyState = 'open' => 'ended'
// sourceend
mediaSource.endOfStream()
---------------------------------------------------
sourceclose | Event | readyState = 'open' => 'closed' / 'ended' => 'closed'
// sourcrclose
mediaSource.readyState = 'closed';
mediaSource.duration = NaN;
mediaSource.removeSourceBuffer(activeSourceBuffers);
mediaSource.removeSourceBuffer(sourceBuffers);

SourceBuffer

添加模式

1
2
3
4
5
6
enum AppendMode {
// media segment中的时间戳决定了编码帧的位置,即按 pts 来排序。
"segments",
// media segment的时间戳跟编码帧的位置无关。新的media segment的编码帧会追加到上一段的media segment的编码帧后面,假如之间存在间隔,则timestampOffset会被重新设置用来弥补这段间隔。因此timestampOffset可以将一段media segment放在任何一个位置。
"sequence"
};

基础结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
};

属性

  • 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_mode
  • updating:只读,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_timestampOffset
  • audioTracks,videoTracks,textTracks:分别返回AudioTrackListVideoTrackListTextTrackList

  • 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_algorithm
  • abort:中止并重置解析器。

    1
    .abort(): void

    假如方法被调用,则会有以下的执行步骤:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    if 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 = Infinity
  • remove:删除特定时间段的媒体。参数中的单位为s。

    1
    .remove(start: double, end: unrestricted double): void

    假如方法被调用,则会有以下的执行步骤:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    if 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
2
3
4
5
6
7
8
9
updatestart  | Event | updating = false => true
---------------------------------------------------
update | Event | updating = true => false // append或remove成功执行
---------------------------------------------------
updateend | Event | // append或remove执行到最后
---------------------------------------------------
error | Event | updating = true => false // 在append的时候发生错误
---------------------------------------------------
abort | Event | updating = true => false // append或remove时调用abort()方法

其他的就不多说了。基本上都是可以从官方文档上找得到答案的。

总结

总结就是如下的操作顺序,但实际情况远不止如此,还复杂很多,得依据场景来分析。

  1. 在页面的 HTML 部分中定义 HTMLMediaElement 元素。

    1
    <video id='vEle' autoplay/>
  2. 使用JavaScript创建 MediaSource 对象。

    1
    var mediaSource = new window.MediaSource()
  3. 使用 URL.createObjectURL 创建 BlobURL,并将 MediaSource 对象作为源。

    1
    var url = URL.createObjectURL(mediaSource);
  4. 将虚拟 URL 分配到视频元素的 src 属性。

    1
    2
    v = document.getElementById('vEle');
    v.src = url;
  5. 使用 addSourceBuffer 创建 SourceBuffer,包含你添加的 MIME 类型的视频。

    1
    var videoSource = mediaSource.addSourceBuffer('video/mp4');
  6. 从媒体文件联机获取视频initialization segment,并使用 appendBuffer 将其添加到 SourceBuffer 中。而initialization segment就是相当于fmp4中的 ftyp + moov,有trackId, duration, width, height等基础信息。

    1
    videoSource.appendBuffer(initialization_segment)
  7. 从媒体文件获取视频media segment,并使用 appendBuffer 将其附加到 SourceBuffer 中。而media segment就相当于fmp4中的moof + mdat,是实实在在的音视频数据。

    1
    videoSource.appendBuffer(media_segment)
  8. 在 video 元素上调用 play 方法。

    1
    2
    3
    4
    5
    6
    7
    var playPromise = v.play()

    if (playPromise !== undefined) {
    playPromise.then(_ => {
    v.pause()
    }).catch(error => { })
    }
  9. 重复步骤 7 直到完成。

  10. 清除。
    1
    videoSource.remove(start, end)
我只是试下能不能被赞赏😳