传统直播技术,大多使用RTMP通过Flash进行传输。随着HTML5的逐渐实现,等媒体标签的浏览器支持,
很多视频逐渐向HTML5靠拢。Youtube
等视频网站纷纷开始使用HTML5播放器,然而纵观当前的直播网站,大多
还是依赖Flash。直播为何不采用HTML5呢?
目前的HTML5直播思路有以下几种。一是使用js调用WebGL渲染视频,用websocket/XHR传输,比如jsmpeg项目,
实现了一个MPEG1的js解析器,该项目存在很多bug,另外由于MPEG1的效能极低(比GIF好不了多少),传输的视频
质量较差,而且js解析消耗很高CPU,这种方案不是很理想。二是使用Native的解码方案,使用MediaSourceExtension。
MSE支持一些格式,比如H264,VP8,VP9等(因浏览器而异),这里就VP8/VP9进行讨论。VP8/VP9的容器一般是webm,
然而chrome对webm的解析貌似不正确(很多网友说很多webm视频能够在Firefox上面使用MSE播放,但是不能在chrome上面
播放,ffmpeg压制出来的视频就是无法被chrome播放的)。webm格式大致可以分割为两部分,启动信息和媒体片段:
<EBMLHeader type="list" offset="0">
<EBMLVersion type="uint" value="1"/>
<EBMLReadVersion type="uint" value="1"/>
<EBMLMaxIDLength type="uint" value="4"/>
<EBMLMaxSizeLength type="uint" value="8"/>
<DocType type="string" value="webm"/>
<DocTypeVersion type="uint" value="2"/>
<DocTypeReadVersion type="uint" value="2"/>
EBMLHeader>
<Segment type="list" offset="43">
<SeekHead type="list" offset="55">
<Seek type="list" offset="61">
<SeekID type="uint" id_name="Info" value="357149030"/>
<SeekPosition type="uint" value="229"/>
Seek>
<Seek type="list" offset="75">
<SeekID type="uint" id_name="Tracks" value="374648427"/>
<SeekPosition type="uint" value="291"/>
Seek>
<Seek type="list" offset="90">
<SeekID type="uint" id_name="Cues" value="475249515"/>
<SeekPosition type="uint" value="340702"/>
Seek>
SeekHead>
<Void type="binary" size="169"/>
<Info type="list" offset="284">
<TimecodeScale type="uint" value="1000000"/>
<MuxingApp type="string" value="Lavf57.65.100"/>
<WritingApp type="string" value="Lavf57.65.100"/>
<Duration type="float" value="9269.000000"/>
Info>
<Tracks type="list" offset="346">
<TrackEntry type="list" offset="358">
<TrackNumber type="uint" value="1"/>
<TrackUID type="uint" value="1"/>
<FlagLacing type="uint" value="0"/>
<Language type="string" value="und"/>
<CodecID type="string" value="V_VP8"/>
<TrackType type="uint" value="1"/>
<DefaultDuration type="uint" value="33333333"/>
<Video type="list" offset="402">
<PixelWidth type="uint" value="1536"/>
<PixelHeight type="uint" value="864"/>
<FlagInterlaced type="uint" value="2"/>
<DisplayUnit type="uint" value="4"/>
Video>
TrackEntry>
<TrackEntry type="list" offset="426">
<TrackNumber type="uint" value="2"/>
<TrackUID type="uint" value="2"/>
<FlagLacing type="uint" value="0"/>
<Language type="string" value="und"/>
<CodecID type="string" value="A_VORBIS"/>
<TrackType type="uint" value="2"/>
<Audio type="list" offset="465">
<Channels type="uint" value="2"/>
<SamplingFrequency type="float" value="48000.000000"/>
<BitDepth type="uint" value="32"/>
Audio>
<CodecPrivate type="binary" size="3950"/>
TrackEntry>
Tracks>
<Cluster type="list" offset="4445">
<Timecode type="uint" value="0"/>
<SimpleBlock type="binary" size="44" trackNum="2" timecode="0" presentationTimecode="0" flags="80"/>
<SimpleBlock type="binary" size="33554" trackNum="1" timecode="3" presentationTimecode="3" flags="80"/>
<SimpleBlock type="binary" size="244" trackNum="2" timecode="3" presentationTimecode="3" flags="80"/>
<SimpleBlock type="binary" size="238" trackNum="2" timecode="15" presentationTimecode="15" flags="80"/>
<SimpleBlock type="binary" size="243" trackNum="2" timecode="36" presentationTimecode="36" flags="80"/>
<SimpleBlock type="binary" size="240" trackNum="2" timecode="58" presentationTimecode="58" flags="80"/>
<SimpleBlock type="binary" size="3543" trackNum="1" timecode="70" presentationTimecode="70" flags="0"/>
...
Cluster>
...
启动信息中包含了轨道数量,音视频轨道的编码,颜色,持续时间等等信息,后面的媒体信息里面包含了混杂的轨道流。MSE的一般模式
如下:
var video = document.querySelector('video');
var assetURL = 'frag_bunny.webm';
var mimeCodec = 'video/webm; codecs="vp9, vorbis"';
if ('MediaSource' in window && MediaSource.isTypeSupported(mimeCodec)) {
var mediaSource = new MediaSource();
//console.log(mediaSource.readyState); // closed
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
console.error('Unsupported MIME type or codec: ', mimeCodec);
}
function sourceOpen (_) {
//console.log(this.readyState); // open
var mediaSource = this;
var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
fetchAB(assetURL, function (buf) {
sourceBuffer.addEventListener('updateend', function (_) {
mediaSource.endOfStream();
video.play();
//console.log(mediaSource.readyState); // ended
});
sourceBuffer.appendBuffer(buf);
});
};
function fetchAB (url, cb) {
console.log(url);
var xhr = new XMLHttpRequest;
xhr.open('get', url);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
cb(xhr.response);
};
xhr.send();
};
基本套路是先创建一个MediaSource,然后将MediaSource的URL传给Video标签,然后MediaSource会open,在sourceopen
事件中创建一个特定编码的SourceBuffer,然后将webm流填入这个SourceBuffer。但是这种方案需要准备好的webm,这种
webm的启动信息里面包含了视频长度,每个偏移在哪里找等等信息。对于直播流来说,因为实时性要求,不可能在头部回写视频
流的信息,所以有了一种DASH的解决方案,DASH.js。这种方案
将webm的启动信息和媒体信息分离,并且将视频和音频轨道分离:
通过HTTP传输,然后在浏览器端使用MSE解析。DASH会为每个轨道(视频/音频)创建一个SourceBuffer,先填入启动信息(头),
然后持续填入媒体片段(chunk)。但是MSE默认会从0开始播放,但是对于直播流,用户开始看的时候可能直播流已经进行了一段时间,
这时候如果向SourceBuffer填入当前时间(比如61.25s)的片段,Video标签并不会播放,这时候通过设置Video的currentTime可以
解决,通过SourceBuffer的buffered属性可以获得已经解析好的视频片段的开始时间和结束时间。但是实践发现,少于2s的chunk
极容易被解析失败,如果要求较小粒度的传输控制,可能要在前端使用js做buffer缓冲。
编程时需要注意的有,SourceBuffer的appendBuffer接受的buffer的时间范围是SourceBuffer的appendWindowStart和appendWindowEnd
控制的,在这个范围外的片段不会被接受。MSE的操作都是异步操作,SourceBuffer在解析的时候,不能再放入片段,所以需要监听updateend
事件,用队列处理异步请求。最后PS:VP9的码流非常清晰,500K就可以达到很好的效果。
参考阅读: