Media Source Extensions播放h264

310 阅读9分钟

1.什么是MSE

    MSE(Media Source Extensions),即媒体源扩展,是一项 W3C 规范,其提供了实现无插件且基于 Web 的流媒体的功能。通过 MSE,媒体串流能够通过 JavaScript 创建,并且可以使用 HTML5 的 <audio> 和 <video> 标签进行播放。

2.为什么用MSE

  • 单纯使用 <video>/<audio> 无法做到无缝衔接切换分辨率、动态切换语音等复杂功能
  • <video>/<audio> 自带解封装和解码功能。
  • 由于大多数现代浏览器会通过硬件加速来优化媒体解码和渲染,MSE利用了浏览器底层的媒体解码器和GPU加速,以确保高效的媒体播放和渲染,因此性能上也会之前使用webgl加载视频好非常多。
  • MSE允许将视频和音频分成小的片段进行加载和缓冲,而不是等待整个文件下载完成后再进行播放。这种分段加载和缓冲控制可以提供更快的初始播放启动时间,并使用户能够在网络不稳定的情况下更好地处理数据流
  • MSE并不限制特定的音频或视频编码格式,因此它可以适应各种常见的编码格式。这种灵活性使开发者可以选择适合其需求的最佳编码格式,并根据需要进行调整,以获得最佳性能和兼容性。

3.局限

    尽管MSE为流媒体播放提供了更多的控制能力,但在编码格式支持、浏览器兼容性、安全限制和视频加密方面存在一些局限性。

  • 编码格式支持有限:MSE在不同的浏览器中对编码格式的支持可能不一致。一些旧版本的浏览器可能不支持最新的音频和视频编码格式,导致无法播放特定类型的媒体。
    • 旧编码格式支持:某些旧版本的浏览器可能不支持最新的音频和视频编码格式。例如,一些较旧的浏览器可能不支持较新的视频编码标准如H.265(HEVC)或VP9,或者可能不支持较新的音频编码标准如Opus。
    • 浏览器特定的编码支持:不同浏览器在支持编码格式上存在一些差异。某些编码格式可能在某些浏览器中得到很好的支持,而在其他浏览器中可能没有完全支持或存在问题。因此,在选择编码格式时,需要考虑目标用户的浏览器偏好以及广泛支持的编码格式。
    • 潜在的硬件加速限制:MSE还依赖于浏览器底层的解码器来对媒体进行解码。一些浏览器在硬件加速方面的支持程度可能有所不同,可能会影响对某些编码格式的支持或性能。
  • 浏览器兼容性问题:尽管MSE是一个W3C推荐标准,但在不同的浏览器和设备上实现的方式可能有所不同。这可能导致在某些平台或浏览器上需要额外的工作来确保正常播放。如:
    • 火狐浏览器超过一定倍数会导致视频锐化(白屏)
    • 当前预览为追求低延时,在延后超过一秒情况下会主动进行追赶跳帧,跳帧在每个浏览器上的表现也有所不同,这就要求需要选取一个适中的预留帧数或针对不同浏览器做不同处理。如Safari对播放流畅度较高,要求较为充足的预留帧数;
  • 低帧播放流畅性问题:MSE在低帧情况下无法很流畅播放,特别是1/2帧。
  • 安全性限制:由于安全性考虑,浏览器会对跨域请求进行限制,这可能导致在使用MSE时出现跨域访问的问题。为了解决这个问题,通常需要进行服务器配置或使用跨域资源共享(CORS)策略。
  • 视频加密支持有限:在某些情况下,MSE可能无法直接处理受数字版权管理(DRM)保护的视频内容。为了实现视频加密和内容保护,可能需要额外的技术支持,如使用Encrypted Media Extensions (EME)。

4.MSE一些常用Api

MediaSource 属性

方法描述
sourceBuffers返回包含 MediaSource 所有 SourceBuffer 的 SourceBufferList 对象
duration获取或者设置当前媒体展示的时长,负数或 NaN 时抛出 InvalidAccessError,readyState 不为 open 或 SourceBuffer.updating 属性为 true 时抛出 InvalidStateError
readyState表示 MediaSource 的当前状态。open:已附着到一个 media 元素并准备好接收 SourceBuffer 对象;close:未附着到一个 media 元素上;ebded:已附着到一个 media 元素,但流已被 MediaSource.endOfStream() 结束

MediaSource 方法

方法描述
addSourceBuffer(mime)根据给定 MIME 类型创建一个新的 SourceBuffer 对象,将它追加到 MediaSource 的 SourceBuffers 列表中
removeSourceBuffer(sourceBuffer)除 MediaSource 中指定的 SourceBuffer。如果不存在则抛出 NotFoundError 异常

MediaSource 事件

方法描述
sourceopenreadyState 从 closed 或 ended 到 open
sourceclosereadyState 从 open 或 ended 到 closed
sourceendedreadyState 从 open 或 ended

其余API可以参考MDN MediaSource

SourceBuffer 属性

方法描述
modesegments以及sequence。控制处理媒体片段序列,segments 片段时间戳决定播放顺序,sequence 添加顺序决定播放顺序,在 MediaSource.addSourceBuffer() 中设置初始值,如果媒体片段有时间戳设置为 segments,否则 sequence。自己设置时只能从 segments 设置为 sequence,不能反过来,它意味着播放顺序将被固定,并会生成新的时间戳。 如果为实时视频,可设置为sequence。
updating是否正在更新,比如 appendBuffer() 或 remove() 方法还在处理中,在进行appendBuffer或者remove前需先判断下!updating。
buffered返回当前缓冲的 TimeRanges 对象,可以通过start(i)以及end(i)获取当前视频段的起始时间和结束时间。如果视频数据时间不流畅或缓存空间已满就容易分段。
audioTracks返回当前包含的 AudioTrack 的 AudioTrackList 对象。
videoTracks返回当前包含的 VideoTrack 的 VideoTrackList 对象。

SourceBuffer 方法

方法描述
appendBuffer(source)添加媒体数据片段(ArrayBuffer 或 ArrayBufferView)到 SourceBuffer。
abort中断当前片段,重置段解析器,可以让 updating 变成 false
remove(start, end)移除指定范围的媒体数据。播放过程中可以在合适的时机移除已播放的视频数据,释放缓存空间。

SourceBuffer 事件

方法描述
updateendappend 或 remove 已经结束,在 update 之后触发。一般在该事件中对视频数据进行特殊处理,如跳帧,丢弃缓存数据等
abort中断当前片段,重置段解析器,可以让 updating 变成 false
remove(start, end)移除指定范围的媒体数据。播放过程中可以在合适的时机移除已播放的视频数据,释放缓存空间。

其余API可以参考MDN SourceBuffer

5.如何使用MSE

    整体流程:将 video/audio 的 src 设置为 MediaSource 对象,然后通过 HTTP 请求获取数据,然后传给 MeidaSource 中的 SourceBuffer 来实现视频播放。 image.png

一个简单的MSE初始化加载过程:获取码流数据,进行初始化,使用appendBuffer推入mediasourc

// 创建video元素
const videoElement = document.createElement('video');
document.body.appendChild(videoElement);
// 创建媒体源对象
const mediaSource = new MediaSource();
// 当媒体源打开时的处理函数
mediaSource.addEventListener('sourceopen', function() {
    // 创建媒体缓冲区对象
    const sourceBuffer = mediaSource.addSourceBuffer('video/mp4');
    const curMode = sourceBuffer.mode
    if (curMode === 'segments') {
        sourceBuffer.mode = 'sequence'
    }
    // 创建WebSocket连接
    const socket = new WebSocket('XXXX');
    // 监听WebSocket消息事件
    socket.addEventListener('message', function(event) {
        // 接收到数据后将其追加到缓冲区
        sourceBuffer.appendBuffer(event.data);
    });
});
// 将媒体源URL设置为视频元素的src属性
videoElement.src = URL.createObjectURL(mediaSource);

addSourceBuffer 方法会根据给定的 MIME 类型创建一个新的 SourceBuffer 对象,然后会将它追加到 MediaSource 的 SourceBuffers 列表中。


一个简单的MSE卸载过程*

//注销src
window.URL.revokeObjectURL(videoElement.src);
videoElement.src = "";
//移除sourcebuffer
for (let i = 0; i < mediaSource.sourceBuffers.length; i++) {
    mediaSource.removeSourceBuffer(mediaSource.sourceBuffers[i]);
}

实时视频跳帧追赶过程示例

//时间分段处理
const SKIP_COUNT = 5
function handleTimeUpdate() {
    const buffered = sourceBuffer.buffered
    // 是否分段
    if (buffered.length==0 || currentSegmentIndex == buffered.length - 1) {
        return
    }
    if (buffered.length && currentSegmentIndex >= buffered.length) {
        currentSegmentIndex = buffered.length - 1
        return
    }
    const currentTime = videoElement.currentTime
    const nextSegmentIndex = currentSegmentIndex + 1
    const currentStart = buffered.start(currentSegmentIndex)
    const currentEnd = buffered.end(currentSegmentIndex)
    const nextStart = buffered.start(nextSegmentIndex)
    const nextEnd = buffered.end(nextSegmentIndex)
    // 如果当前时间已经超过了下一段的起始时间,把当前的缓存删掉,之后开始播放下一段视频,并且重新设置追赶次数
    currentSegmentIndex += 1
    videoElement.currentTime = nextStart
    removeOffset = 0;
    sourceBuffer.remove(0, currentEnd)
    videoElement.play()
    skipDistance = SKIP_COUNT
}
sourceBuffer.addEventListener('updateend', () => {
    if (sourceBuffer != null && mediaSource?.readyState == 'open') {
        handleTimeUpdate()
        let end = sourceBuffer.buffered.end(currentSegmentIndex)
        let currentTime = videoElement.currentTime
        if (end - currentTime >= 1 && skipDistance == 0) {
            skipDistance = SKIP_COUNT
        }
        // 在尝试下预留0.3s-0.4s左右的帧数能较好保证视频播放的流畅度
        // 单次连续进行五次跳帧,之后正常播放,因为浏览器播放也会根据数据自动最终
        if (end - currentTime >= 0.5 && skipDistance) {
            videoElement.currentTime = end - 0.4
            skipDistance--;
        }
        // 清除已播放数据,留存10s缓存,保证流畅
        if (!sourceBuffer.updating && currentTime - removeOffset >= 20) {
            sourceBuffer.remove(removeOffset, currentTime - 10);
            removeOffset = currentTime - 10
        }
    }
})

非实时视频不建议追赶,容易造成卡顿。追赶时预留的时间需根据不同情况而定,也需要考虑浏览器兼容性。如果sourceBuffer.mode为segments, 文档中介绍分段后是在buffered的尾部插入,但实际情况下可能不一定,预计跟时间戳有一定关系

6.问题分析及解决方案

  • 无法正常流畅播放可能原因

    • duration无法与实际对应,注意单位
    • 需要以I帧为第一帧,否则无法成像,容易造成无法播放,特别是safari浏览器
  • safari出现倒帧

    追赶时间进度而进行跳帧,但由于safari对流畅度要求较高,当预留帧数不足是就会出现跳帧,具体预留多少秒帧数视情况而定

  • 播放超过一定秒数会暂停卡顿,无法继续播放

    数据分段,currentTime未能准确跳转

  • MSE有延迟甚至卡顿暂停(高帧)

    缓存内数据过多,浏览器解析数据需要时间

  • 火狐放大倍数过大会导致白屏

    放大倍数过大导致像素点锐化造成的白屏

  • 低帧播放会一直卡顿问题(5帧以下)

    频繁跳帧导致,并非每次超过1s都需要追赶,浏览器在播放过程中也会根据当前appendBuffer的片段的duration去进行追赶。总体需要保持一个动态平衡的追赶过程,因此设置哨兵(超过1.5s或者1s,视情况而定),单次只连续追赶5次,避免频繁跳帧。

  • 火狐浏览器全屏放大会闪屏

    width与height一次性变化过大导致。有以下两种方案:

    • 增加一个可动态更新的dom节点带动video标签变化即可, 如:画布
    • transform: scale() 也有些用(但也会有闪屏)
  • 低帧情况下无法成像

    对I帧进行复制为相同的多帧,使其成像

7.调试工具

  • chrome://media-internals/: 是 chrome 浏览器用来调试多媒体的工具,直接在地址栏输入
  • chrome控制台Media(媒体)调试工具