HTML5直播技术探究

传统直播技术,大多使用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的启动信息和媒体信息分离,并且将视频和音频轨道分离:

HTML5直播技术探究_第1张图片

通过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就可以达到很好的效果。

参考阅读:

  1. WebM VOD Baseline format
  2. MDN MediaSource
  3. W3C MediaSourceExtensions
  4. Instructions to do WebM live streaming via DASH

你可能感兴趣的:(后端,前端)