DASH研究

MP4格式研究

  1. mp4解析在线地址:mp4parser
  2. 一个mp4就是一个容器。
  3. 每个mp4是由多个box组成,每个box可以嵌套子box,这个box称为:container box。
  4. Box,每个Box由Header和Data组成。
  5. Header,包含了整个Box的长度size和类型type。当size==0时,代表这是文件中最后一个Box;当size==1时,意味着Box长度需要更多bits来描述,在后面会定义一个64bits的largesize描述Box的长度;当type是uuid时,代表Box中的数据是用户自定义扩展类型。
  6. Data,是Box的实际数据,可以是纯数据也可以是更多的子Boxes。
  7. mp4分有传统的regular mp4, 和为适应流媒体的fragmented Mp4(fMp4)。
  8. regular mp4 主要由ftyp,moov/mdat,mdat/moov等box组成。
    1. ftypbox,在文件的开始位置,描述的文件的版本、兼容协议等;
    2. moovbox,这个box中不包含具体媒体数据,但包含本文件中所有媒体数据的宏观描述信息,moov box下有mvhd和trak box。
    3. mvhd中记录了创建时间、修改时间、时间度量标尺、可播放时长等信息。
    4. trak中的一系列子box描述了每个媒体轨道的具体信息。
  9. 在fMp4格式中包含一系列的segments(moof+mdat的组合),这些segments可以被独立的request(利用byte-range request),这有利于在不同质量级别的码流之间做码率切换操作
  10. 在regular mp4中,如果我们要在两个码流之间做码率切换,就需要找到两个码流中对应时间点的byte position,然而这时候我们只有一个巨大的mdat box,要在这里面找到一个具体的byte position无疑是复杂的。而且,在regular mp4中,有时moov会在巨大的mdat box之后,这也会影响起播的速度。
    1. moofbox(此box存在于fmp4中),这个box是视频分片的描述信息。并不是MP4文件必须的部分,但在我们常见的可在线播放的MP4格式文件中确是重中之重。
    2. mdatbox,实际媒体数据。我们最终解码播放的数据都在这里面。
  11. 其他box
    1. Free Space Box(free或skip) “free”中的内容是无关紧要的,可以被忽略。该box被删除后,不会对播放产生任何影响。
  12. sidx box是segment index box, 是fmp4的分片索引box。开发DASH时,需要解析这里的box,定位到对应的视频数据。
  13. sidx解析规则:
    1. box的header部分,分别为4B的size大小,4B的type类型(Unicode 值,需要转码成字符串)。
    2. box的data部分,关于DASH最有用的是referenced_size(单位:字节)和subsegment_duration(单位:毫秒)。个数就是fmp4的分片数目,数目为:type|size|duration * reference_count
    3. sidx内容规则如下图:
sidx 名字 大小
header size Uint32
header type Int32
header larsesize(if size==1) Uint64
data version Uint8
data none Uint24
data reference_ID Uint32
data timescale Uint32
data earliest_presentation_time Uint32
data first_offset Uint32
data reserved Uint16
data reference_count Uint16
data type|size|duration * reference_count Uint32
data SAP * reference_count Uint32
  1. 解析代码如下:

const mp4 = {
    
    
  parseSidx (dataView, offset) {
    
    
      /* 
          注意:
          此方法只能解析sidx -> dash只需解析sidx就行
          以及header里面size不等于1和0的情况 -> 这里需要将来支持一下
          以及data部分version为0的情况 
       */

      let hex2a = function (hex) {
    
    
          var str = '';
          for (var i = 0; i < hex.length; i += 2)
              str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
          return str;
      };
      let trim1 = function (str) {
    
    
          return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
      };
      const msg = {
    
    };
      msg.len = dataView.getUint32(offset); offset += 4;//获取header头size
      let type = dataView.getInt32(offset); offset += 4;
      msg.type = trim1(hex2a(type.toString(16)));//获取header头type

      msg.version = dataView.getUint8(offset); offset += 4;//获取version
      msg.reference_ID = dataView.getUint32(offset);offset += 4;//获取reference_ID
      msg.timescale = dataView.getUint32(offset);offset += 4;//timescale
      msg.earliest_presentation_time = dataView.getUint32(offset); offset += 4;//earliest_presentation_time
      msg.first_offset = dataView.getUint32(offset);  offset += 4;//first_offset
      msg.reserved = dataView.getUint16(offset) ;offset += 2;//reserved
      msg.reference_count = dataView.getUint16(offset) ;offset += 2;//reference_count
      msg.entries = [];
      let reference_count = msg.reference_count;
      while (reference_count--) {
    
    
          let entry = {
    
    };
          let fourBytes = dataView.getUint32(offset); offset += 4;
          entry['reference_type'] = (fourBytes >> 31) & 1;
          entry['referenced_size'] = (fourBytes & 0x7fffffff);
          entry['subsegment_duration'] = dataView.getUint32(offset);offset += 4;
          fourBytes = dataView.getUint32(offset);offset += 4;
          entry['starts_with_SAP'] = (fourBytes >> 31) & 1;
          entry['SAP_type'] = (fourBytes >> 29) & 7;
          entry['SAP_delta_time'] = (fourBytes & 0x0fffffff);
          msg.entries.push(entry);
      }
      return msg;
  }
};

export {
    
    mp4};

video播放原理

  1. 原生video标签是不支持流媒体播放的,那么它怎么在不完全加载全部视频文件的情况下,就开始播放视频的呢?原理是,首先(请求range: bytes=0-)加载mp4头部的一小部分(chunk),解析出ftyp和moov,用来定位一帧的位置,在发出对应的range请求,来实现定位播放,以及提前播放。是最终在内存中下载开始播放位置的整个视频。这个视频后端需要提供range请求服务。
  2. 为了让video支持流媒体播放,需要使用MSE,MSE原理是开辟一块内存,提供存放流媒体的原始数据,用以video标签能够播放fmp4视频。
  3. 为了让video标签能够播放本地blob数据,有两种方法:
    1. 使用data uri 格式的数据,可以使用FileReader对象将blob数据转成data uri。格式为:data:[<MIME type>][;charset=<charset>][;base64],<encoded data>
    2. 使用URL对象,指向blob对象。格式为:blob:same origin/pointer
    3. 使用data uri的话,编码会转成base64,会增大文件size,并且是直接打在页面上,有损性能,使用URL对象的话,解决如上问题
    4. URL对象可以使用URL.createObjectURL(blob)生成。
    5. URL对象可以指向硬盘或者内存空间中的文件,以URL的形式赋给video,audio或者img等标签,来使得浏览器获取本地文件,减少http请求数量。

其他技术知识点

  1. 传统上,服务器通过 AJAX 操作只能返回文本数据,即responseType属性默认为text。XMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob。若果设置为arraybuffer,就可以直接获得arraybuffer对象。blob还需要通过FileReader转成arraybuffer。
  2. cors请求,当设置了header头时,会触发浏览器的预检option请求。
  3. 播放器全屏:
    1. 由于video标签使用requestFullScreen,会漏出shadow dom。而自定义的控制栏被隐藏。所以应该使用video外层的节点进行全屏。
    2. 退出全屏需要特殊处理一下esc键,需要通过document.fullscreenEnabled || window.fullScreen || document.webkitIsFullScreen || document.msFullscreenEnabled检测下当前是否在全屏状态下。否则在退出全屏并检测esc时,浏览器会吧esc键给屏蔽掉。 理解有误,最新理解是退出全屏需要特殊处理一下esc键,需要检测下当前是否在全屏状态下。否则在退出全屏并检测esc时,浏览器会吧esc键给屏蔽掉。
    3. document.fullscreenElement: 当前处于全屏状态的元素 element。
    4. document.fullscreenEnabled: 标记 fullscreen 当前是否可用。
    5. 当进入/退出 全屏模式时,会触发 fullscreenchange 事件。
    6. 代码如下:
    const api = {
          
          
           requestFullScreen : () => {
          
          
                const el = this.model.BaseNode;
                const rfs = el.requestFullScreen || el.webkitRequestFullScreen || el.mozRequestFullScreen || el.msRequestFullscreen;      
                if(typeof rfs != "undefined" && rfs) {
          
          
                    rfs.call(el);
                    utils.evt.addEvent(window, 'resize', this.evtHandler.fullWindowOnEscKey);
                };
                return;
            },
            exitFullScreen : () => {
          
          
                if (document.exitFullscreen) {
          
            
                    document.exitFullscreen();  
                }  
                else if (document.mozCancelFullScreen) {
          
            
                    document.mozCancelFullScreen();  
                }  
                else if (document.webkitCancelFullScreen) {
          
            
                    document.webkitCancelFullScreen();  
                }  
                else if (document.msExitFullscreen) {
          
            
                    document.msExitFullscreen();  
                } 
                utils.evt.removeEvent(window, 'resize', this.evtHandler.fullWindowOnEscKey);
            },
            checkFull : () => {
          
          
                //let isFUll = document.fullscreenEnabled || window.fullScreen || document.webkitIsFullScreen || document.msFullscreenEnabled || false;
                let isFull = document.fullscreenElement  || document.webkitCurrentFullScreenElement || document.mozFullScreenElement || null;
                return isFUll;
            },
    };
    const evtHandler = {
          
          
            fullWindowOnEscKey : (evt) => {
          
          
                if (!this.tools.checkFull()) {
          
          
                    utils.dom.removeClassName(this.model.BaseNode, 'krv-fullscreen');
                    this.tools.exitFullScreen();
                }
            }
    };
    
  4. 浏览器兼容问题:
    1. safari浏览器video标签,视频资源地址必须添加明确的视频扩展名,或者添加source标签,明确指定type。否则播放不了。
  5. 视频网站使用的流媒体技术方案
    1. youtube DASH video/webm ajax get url后加range CORS请求分片
    2. 腾讯视频 HLS m3u8+ts CORS请求
    3. 爱奇艺 RTMP f4v url后加range CORS请求分片
    4. bilibili DASH m4s 在请求体内加range 触发options来请求分片
    5. 优酷 HLS m3u8+ts url后加参数 CORS请求分片 但是切换分辨率有缝隙

猜你喜欢

转载自blog.csdn.net/sinat_25259461/article/details/84941465