美文网首页
DASH研究

DASH研究

作者: 请叫我小飞鹅 | 来源:发表于2021-06-27 11:33 被阅读0次

    MP4格式研究

    mp4解析在线地址:mp4parser

    一个mp4就是一个容器: 每个mp4是由多个box组成,每个box可以嵌套子box,这个box称为container box。
    • Box:每个Box由Header和Data组成。
    • Header:包含了整个Box的长度size和类型type。当size==0时,代表这是文件中最后一个Box;当size==1时,意味着Box长度需要更多bits来描述,在后面会定义一个64bits的largesize描述Box的长度;当type是uuid时,代表Box中的数据是用户自定义扩展类型。
    • Data:是Box的实际数据,可以是纯数据也可以是更多的子Boxes。
    mp4分有传统的regular mp4, 和为适应流媒体的fragmented Mp4(fMp4)。
    • regular mp4 主要由ftyp,moov/mdat,mdat/moov等box组成。
    • ftypbox,在文件的开始位置,描述的文件的版本、兼容协议等;
    • moovbox,这个box中不包含具体媒体数据,但包含本文件中所有媒体数据的宏观描述信息,moov box下有mvhd和trak box。
    • mvhd中记录了创建时间、修改时间、时间度量标尺、可播放时长等信息。
    • trak中的一系列子box描述了每个媒体轨道的具体信息。

    在fMp4格式中包含一系列的segments(moof+mdat的组合),这些segments可以被独立的request(利用byte-range request),这有利于在不同质量级别的码流之间做码率切换操作

    在regular mp4中,如果我们要在两个码流之间做码率切换,就需要找到两个码流中对应时间点的byte position,然而这时候我们只有一个巨大的mdat box,要在这里面找到一个具体的byte position无疑是复杂的。而且,在regular mp4中,有时moov会在巨大的mdat box之后,这也会影响起播的速度。
    moofbox(此box存在于fmp4中),这个box是视频分片的描述信息。并不是MP4文件必须的部分,但在我们常见的可在线播放的MP4格式文件中确是重中之重。

    • mdatbox,实际媒体数据。我们最终解码播放的数据都在这里面。
    • 其他box
      • Free Space Box(free或skip) “free”中的内容是无关紧要的,可以被忽略。该box被删除后,不会对播放产生任何影响。
      • sidx box是segment index box, 是fmp4的分片索引box。开发DASH时,需要解析这里的box,定位到对应的视频数据。

      sidx解析规则:
      box的header部分,分别为4B的size大小,4B的type类型(Unicode 值,需要转码成字符串)。
      box的data部分,关于DASH最有用的是referenced_size(单位:字节)和subsegment_duration(单位:毫秒)。个数就是fmp4的分片数目,数目为:type|size|duration * reference_count
      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
    解析代码如下
    
    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播放原理

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

    其他技术知识点

    • 传统上,服务器通过 AJAX 操作只能返回文本数据,即responseType属性默认为text。XMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob。若果设置为arraybuffer,就可以直接获得arraybuffer对象。blob还需要通过FileReader转成arraybuffer。
    • cors请求,当设置了header头时,会触发浏览器的预检option请求。
    • 播放器全屏:
      • 由于video标签使用requestFullScreen,会漏出shadow dom。而自定义的控制栏被隐藏。所以应该使用video外层的节点进行全屏。
      • 退出全屏需要特殊处理一下esc键,需要通过document.fullscreenEnabled || window.fullScreen || document.webkitIsFullScreen ||
      • document.msFullscreenEnabled检测下当前是否在全屏状态下。否则在退出全屏并检测esc时,浏览器会吧esc键给屏蔽掉。 理解有误,最新理解是退出全屏需要特殊处理一下esc键,需要检测下当前是否在全屏状态下。否则在退出全屏并检测esc时,浏览器会吧esc键给屏蔽掉。
      • document.fullscreenElement: 当前处于全屏状态的元素 element。
      • document.fullscreenEnabled: 标记 fullscreen 当前是否可用。
      • 当进入/退出 全屏模式时,会触发 fullscreenchange 事件。
        代码如下:
    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();
                }
            }
    };
    
    
    • 浏览器兼容问题:

      • safari浏览器video标签,视频资源地址必须添加明确的视频扩展名,或者添加source标签,明确指定type。否则播放不了。-
    • 视频网站使用的流媒体技术方案

      • youtube DASH video/webm ajax get url后加range CORS请求分片
      • 腾讯视频 HLS m3u8+ts CORS请求
      • 爱奇艺 RTMP f4v url后加range CORS请求分片
      • bilibili DASH m4s 在请求体内加range 触发options来请求分片
      • 优酷 HLS m3u8+ts url后加参数 CORS请求分片 但是切换分辨率有缝隙

    相关文章

      网友评论

          本文标题:DASH研究

          本文链接:https://www.haomeiwen.com/subject/obfxultx.html