https://stackoverflow.com/questions/24102075/mediasource-error-this-sourcebuffer-has-been-removed-from-the-parent-media-sour
https://blog.csdn.net/panqisheng/article/details/51470624
isTypeSupported
判断是否支持要解码播放的视频文件编码和类型。
MediaSource.isTypeSupported('video/webm; codecs=“vorbis,vp8”’);//是否支持webm
MediaSource.isTypeSupported('video/mp4; codecs=“avc1.42E01E,mp4a.40.2”’);//是否支持mp4
MediaSource.isTypeSupported('video/mp2t; codecs=“avc1.42E01E,mp4a.40.2”’);//是否支持ts
1、mime 字符串
mime 字符串指的就是下面这个东西,会在新建 SourceBuffer 中使用到:
var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'
var sourceBuffer = mediaSource.addSourceBuffer(mime);
这个神奇的字符串是什么意思呢?
首先,前面的 video/mp4 代表这是一段 mp4 格式封装的视频,同理也存在类似 video/webm、audio/mpeg、audio/mp4 这样的 mime 格式。一般情况下,可以通过 canPlayType 这个方法来判断浏览器是否支持当前格式。
后面的这一段 codecs="...." 比较特别,以逗号相隔,分为两段:
第一段,'avc1.42E01E',即它用于告诉浏览器关于视频编解码的一些重要信息,诸如编码方式、分辨率、帧率、码率以及对解码器解码能力的要求。
在这个例子中,'avc1' 代表视频采用 H.264 编码,随后是一个分隔点,之后是 3 个两位的十六进制的数,这 3 个十六进制数分别代表:
第一个用于标识 H.264 的 profile,后两个用于标识视频对于解码器的要求。
对于一个 mp4 视频,可以使用 mp4file 这样的命令行工具:
mp4file --dump xxx.mp4
找到 avcC Box 后,就可以看到这三个值:
mp4file --dump movie.mp4
...
type avcC (moov.trak.mdia.minf.stbl.stsd.avc1.avcC) // avc1
configurationVersion = 1 (0x01)
AVCProfileIndication = 66 (0x42) // 42
profile_compatibility = 224 (0xe0) // E0
AVCLevelIndication = 30 (0x1e) // 1E
...
有一处要注意,后面两个值(profile_compability、AVCLevelIndication)只是浏览器用于判断自身的解码能力能否满足需求,所以不需要和视频完全对应,更高也是可以的。
下面来看 codecs 的第二段 'mp4a.40.2',这一段信息是关于音频部分的,代表视频的音频部分采用了 AAC LC 标准:
'mp4a' 代表此视频的音频部分采用 MPEG-4 压缩编码。
随后是一个分隔点,和一个十六进制数(40),这是 ObjectTypeIndication,40 对应的是 Audio ISO/IEC 14496-3 标准。(不同的值具有不同的含义,详细可以参考官方文档)
然后又是一个分隔点,和一个十进制数(2),这是 MPEG-4 Audio Object Type,维基百科中的解释是 "MPEG-4 AAC LC Audio Object Type is based on the MPEG-2 Part 7 Low Complexity profile (LC) combined with Perceptual Noise Substitution (PNS) (defined in MPEG-4 Part 3 Subpart 4)",具体是什么意思就不翻译了,其实就是一种 H.264 视频中常用的音频编码规范。
这一整段 codecs 都有完善的官方文档,可以参考:
The 'Codecs' and 'Profiles' Parameters for "Bucket" Media Types
2、如何转码出符合标准的视频
目前有很多开源的视频处理工具,比如 FFMPEG、HandBrake,我用的后者转码,前者切割。
转码其实很简单,HandBrake 打开后,加入想要处理的视频(mp4 格式),窗口下半部分 video 标签,H.264 Profile 选择 "Baseline",level 选择 "3.0";audio 标签,选择 Encoder 为 "AAC"。
然后就是把视频切割为比较小的 chunk,ffmpeg 就可以很方便地切割:
ffmpeg -ss 00:00:00 -i source2.mp4 -c copy -t 00:00:05 xxxx.mp4
上面这段命令就切出了视频的第 0 秒到第 5 秒。
注意一个问题,ffmpeg 在切割视频的时候无法做到时间绝对准确,因为视频编码中关键帧(I帧)和跟随它的B帧、P帧是无法分割开的,否则就需要进行重新帧内编码,会让视频体积增大。所以,如果切割的位置刚好在两个关键帧中间,那么 ffmpeg 会向前/向后切割,所以最后切割出的 chunk 长度总是会大于等于应有的长度。
3、向 SourceBuffer 中添加多个 chunk
第一部分的范例中只是请求了一段 chunk 然后加入到播放器里,如果视频很长,存在多个chunk 的话,就需要不停地向 SourceBuffer 中加入新的 chunk。
这里就需要注意一个问题了,即 appendBuffer 是异步执行的,在完成前,不能 append 新的 chunk:
sourceBuffer.appendBuffer(buffer1)
sourceBuffer.appendBuffer(buffer2)
// Uncaught DOMException: Failed to set the 'timestampOffset' property on 'SourceBuffer': This SourceBuffer is still processing an 'appendBuffer' or 'remove' operation.
而是应该监听 SourceBuffer 上的 updateend 事件,确定空闲后,再加入新的 chunk:
sourceBuffer.addEventListener('updateend', () => {
// 这个时候才能加入新 chunk
// 先设定新chunk加入的位置,比如第20秒处
sourceBuffer.timestampOffset = 20
// 然后加入
sourceBuffer.append(newBuffer)
}
4、码率自适应算法
对于随时变化的网络情况,我们会根据情况加载不同码率的视频,这里就需要一些控制算法决定当前加载哪个码率的视频。
很容易就能想到一种简单的算法:上一段 chunk 加载完后,计算出加载速度,从而决定下一个 chunk 的码率。
但这种朴素的算法有很严重的问题,即它假设网络是相当稳定的,我们可以根据当前的信息预测出未来的网速。但这已经被大量的统计数据证明是不现实的,换句话说,我们没办法预测未来的网络环境。
所以学术界提出了一系列新的算法,比如最常见的 Buffer-Based 算法,即根据当前缓冲区里视频的长度,决定下一个 chunk 的码率。如果有很长的缓冲,那么就加载高码率的视频。这在相当于一个积分控制器,把时刻变化的无法预测的网络环境在时间维度上积分,以获得一个更平缓更能预测的函数。
但是 Buffer-Based 算法依然有问题,在视频起步阶段,缓冲区里的视频很短,导致无论网络环境如何,起步阶段的码率都是很低的。所以 Buffer-Based 算法只适用于视频 startup 后的稳态阶段。在起步阶段,依然有很多优化的空间,这也不是本文的重点,具体就不再详述了。