ribbon image search rewind fast-forward speech-bubble pie-graph star

hls.js 源码解读【3】

这一篇主要分析一些辅助类函数的代码实现,扩展一些对 buffer 操作,web worker 等内容的学习。

performance

playlist-loader.js 中我们可以看到这行代码

stats.tload = performance.now();  

performance 提供了一个高精度的时间访问 API。它能够查看到 Navigation Timing,Resource Timing这些准确的时间结果。

Navigation Timing API能够帮助网站开发者检测真实用户数据(RUM),例如带宽、延迟或主页的整体页面加载时间。

其中 performance.now()可以得到距离页面开始请求到现在相差的毫秒数。

web worker

Web worker, Javascript是单线程的,所以如果页面中的Javascript有大量计算的话,很容易阻塞页面的动画或者交互响应。HTML5中的Web Worker就使Javascript的多线程编程成为可能。

demux/demuxer.js 中我们可以看到这么一段代码:

import work from 'webworkify-webpack';  
//...
const vendor = navigator.vendor;  
    if (config.enableWorker && (typeof(Worker) !== 'undefined')) {
        logger.log('demuxing in webworker');
        let w;
        try {
          w = this.w = work(require.resolve('../demux/demuxer-worker.js'));
          this.onwmsg = this.onWorkerMessage.bind(this);
          w.addEventListener('message', this.onwmsg);
          w.onerror = function(event) { hls.trigger(Event.ERROR, {type: ErrorTypes.OTHER_ERROR, details: ErrorDetails.INTERNAL_EXCEPTION, fatal: true, event : 'demuxerWorker', err : { message : event.message + ' (' + event.filename + ':' + event.lineno + ')' }});};
          w.postMessage({cmd: 'init', typeSupported : typeSupported, vendor : vendor, id : id, config: JSON.stringify(config)});
        } catch(err) {
          logger.error('error while initializing DemuxerWorker, fallback on DemuxerInline');
          if (w) {
            // revoke the Object URL that was used to create demuxer worker, so as not to leak it
            URL.revokeObjectURL(w.objectURL);
          }
          this.demuxer = new DemuxerInline(observer,typeSupported,config,vendor);
          this.w = undefined;
        }
      } else {
        this.demuxer = new DemuxerInline(observer,typeSupported,config, vendor);
      }
  }

这段代码主要判断宿主环境是否支持 web worker, 然后使用 webworkify-webpack 来调用 webpack bundle 模块,开启 worker.

它非常易于使用,尤其我们环境在 webpack 的环境下:

import work from 'webworkify-webpack';

let w = work(require.resolve('./worker.js'));  
w.addEventListener('message', event => {  
    console.log(event.data);
});

w.postMessage(4);  

而在 hls.js 中,针对 buffer 的 remux 和 demux ,都是在 worker 中完成。

视频编解码

其实我们在拿到请求文件的内容的时候我们需要去判断当前文件的类型,然后从而去用对应的合流器。

static probe(data) {  
    const syncOffset = TSDemuxer._syncOffset(data);
    if (syncOffset < 0)  {
      return false;
    } else {
      if (syncOffset) {
        logger.warn(`MPEG2-TS detected but first sync word found @ offset ${syncOffset}, junk ahead ?`);
      }
      return true;
    }
  }

  static _syncOffset(data) {
    // scan 1000 first bytes
    const scanwindow  = Math.min(1000,data.length - 3*188);
    let i = 0;
    while(i < scanwindow) {
      // 关于 TS 的分流解码可以查看 https://zh.wikipedia.org/wiki/MPEG2-TS
      // 一个 TS 帧 包含 3 TS包, a PAT, a PMT, and one PID, each starting with 0x47
      if (data[i] === 0x47 && data[i+188] === 0x47 && data[i+2*188] === 0x47) {
        return i;
      } else {
        i++;
      }
    }
    return -1;
  }

上面代码就是判断 是否为 MPEG2-TS 的原理,是一种传输和存储包含音效、视频与通信协议各种数据的标准格式,用于数字电视广播系统,如DVB、ATSC、IPTV等等。 TS的解码方式是从PID为0的TS packet内,解析出PAT table,然后PAT table找到各个program源的PID。解码器根据PMT table的ES streaming的PID,将TS流上的packet进行区分,并按不同的ES流进行解码。TS--Transport Streams(传输流)由定长的TS包组成(188字节),而TS包是对PES的一种重新封装(到这里,ES经过了两层封装)。PES包的包头信息依然存在于TS包中

XHR range

如果您的服务器文件支持对文件进行片段的获取,那么这个时候我们可以设置 Xhr 的 header range来控制请求文件的大小。

xhr-loader.js 文件中:

if (context.rangeEnd) {  
      xhr.setRequestHeader('Range','bytes=' + context.rangeStart + '-' + (context.rangeEnd-1));
    }

我们只需要通过设置 header range 的 value 来控制请求的字节大小。


简单的参考代码

var xhr = new XMLHttpRequest();

xhr.onreadystatechange = function () {  
  if (xhr.readyState != 4) {
    return;
  }
  alert(xhr.status);
};

xhr.open('GET', 'http://fiddle.jshell.net/img/logo.png', true);  
xhr.setRequestHeader('Range', 'bytes=100-200'); // the bytes (incl.) you request  
xhr.send(null);  

Buffer

helper/buffer-helper.js 中我们看到对 buffer 的一些常用函数: 我们可以通过 video.buffered拿到当前 video 中缓存的字节数据。

isBuffered : function(media,position) {  
    if (media) {
      let buffered = media.buffered;
      // 我们可以通过 end 和 start 来判断时间差
      for (let i = 0; i < buffered.length; i++) {
        if (position >= buffered.start(i) && position <= buffered.end(i)) {
          return true;
        }
      }
    }
    return false;
  },

这段函数就是可以很好的判断当前想要seek的位置是否在缓存的内容中。

还有一段函数式获取传入 buffer 的一些数据信息。

bufferedInfo : function(buffered,pos,maxHoleDuration) {  
    var buffered2 = [],

        bufferLen,bufferStart, bufferEnd,bufferStartNext,i;
    // 首先我们根据时间开始的先后进行排序
    buffered.sort(function (a, b) {
      var diff = a.start - b.start;
      if (diff) {
        return diff;
      } else {
        return b.end - a.end;
      }
    });
    // 我们需要将持续时间小于最大 duration给 排除掉
    for (i = 0; i < buffered.length; i++) {
      var buf2len = buffered2.length;
      if(buf2len) {
        var buf2end = buffered2[buf2len - 1].end;
        // if small hole (value between 0 or maxHoleDuration ) or overlapping (negative)
        if((buffered[i].start - buf2end) < maxHoleDuration) {
          // 需要合并结束时间点小于目前最长的时间点
          if(buffered[i].end > buf2end) {
            buffered2[buf2len - 1].end = buffered[i].end;
          }
        } else {
          // big hole
          buffered2.push(buffered[i]);
        }
      } else {
        // first value
        buffered2.push(buffered[i]);
      }
    }
    for (i = 0, bufferLen = 0, bufferStart = bufferEnd = pos; i < buffered2.length; i++) {
      var start =  buffered2[i].start,
          end = buffered2[i].end;
      if ((pos + maxHoleDuration) >= start && pos < end) {
        // 位置在当前的片段里面
        bufferStart = start;
        bufferEnd = end;
        bufferLen = bufferEnd - pos;
      } else if ((pos + maxHoleDuration) < start) {
        bufferStartNext = start;
        break;
      }
    }
    return {len: bufferLen, start: bufferStart, end: bufferEnd, nextStart : bufferStartNext};
  }

系列文章

扩展阅读

You Can Speak "Hi" to Me in Those Ways