video-segment-stream.js 5.39 KB
/**
 * Constructs a single-track, ISO BMFF media segment from H264 data
 * events. The output of this stream can be fed to a SourceBuffer
 * configured with a suitable initialization segment.
 * @param track {object} track metadata configuration
 * @param options {object} transmuxer options object
 * @param options.alignGopsAtEnd {boolean} If true, start from the end of the
 *        gopsToAlignWith list when attempting to align gop pts
 */
'use strict';

var Stream = require('../utils/stream.js');
var mp4 = require('../mp4/mp4-generator.js');
var trackInfo = require('../mp4/track-decode-info.js');
var frameUtils = require('../mp4/frame-utils');
var VIDEO_PROPERTIES = require('../constants/video-properties.js');

var VideoSegmentStream = function(track, options) {
  var
    sequenceNumber = 0,
    nalUnits = [],
    frameCache = [],
    // gopsToAlignWith = [],
    config,
    pps,
    segmentStartPts = null,
    segmentEndPts = null,
    gops,
    ensureNextFrameIsKeyFrame = true;

  options = options || {};

  VideoSegmentStream.prototype.init.call(this);

  this.push = function(nalUnit) {
    trackInfo.collectDtsInfo(track, nalUnit);
    if (typeof track.timelineStartInfo.dts === 'undefined') {
      track.timelineStartInfo.dts = nalUnit.dts;
    }

    // record the track config
    if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) {
      config = nalUnit.config;
      track.sps = [nalUnit.data];

      VIDEO_PROPERTIES.forEach(function(prop) {
        track[prop] = config[prop];
      }, this);
    }

    if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' &&
        !pps) {
      pps = nalUnit.data;
      track.pps = [nalUnit.data];
    }

    // buffer video until flush() is called
    nalUnits.push(nalUnit);
  };

  this.processNals_ = function(cacheLastFrame) {
    var i;

    nalUnits = frameCache.concat(nalUnits);

    // Throw away nalUnits at the start of the byte stream until
    // we find the first AUD
    while (nalUnits.length) {
      if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') {
        break;
      }
      nalUnits.shift();
    }

    // Return early if no video data has been observed
    if (nalUnits.length === 0) {
      return;
    }

    var frames = frameUtils.groupNalsIntoFrames(nalUnits);

    if (!frames.length) {
      return;
    }

    // note that the frame cache may also protect us from cases where we haven't
    // pushed data for the entire first or last frame yet
    frameCache = frames[frames.length - 1];

    if (cacheLastFrame) {
      frames.pop();
      frames.duration -= frameCache.duration;
      frames.nalCount -= frameCache.length;
      frames.byteLength -= frameCache.byteLength;
    }

    if (!frames.length) {
      nalUnits = [];
      return;
    }

    this.trigger('timelineStartInfo', track.timelineStartInfo);

    if (ensureNextFrameIsKeyFrame) {
      gops = frameUtils.groupFramesIntoGops(frames);

      if (!gops[0][0].keyFrame) {
        gops = frameUtils.extendFirstKeyFrame(gops);

        if (!gops[0][0].keyFrame) {
          // we haven't yet gotten a key frame, so reset nal units to wait for more nal
          // units
          nalUnits = ([].concat.apply([], frames)).concat(frameCache);
          frameCache = [];
          return;
        }

        frames = [].concat.apply([], gops);
        frames.duration = gops.duration;
      }
      ensureNextFrameIsKeyFrame = false;
    }

    if (segmentStartPts === null) {
      segmentStartPts = frames[0].pts;
      segmentEndPts = segmentStartPts;
    }

    segmentEndPts += frames.duration;

    this.trigger('timingInfo', {
      start: segmentStartPts,
      end: segmentEndPts
    });

    for (i = 0; i < frames.length; i++) {
      var frame = frames[i];

      track.samples = frameUtils.generateSampleTableForFrame(frame);

      var mdat = mp4.mdat(frameUtils.concatenateNalDataForFrame(frame));

      trackInfo.clearDtsInfo(track);
      trackInfo.collectDtsInfo(track, frame);

      track.baseMediaDecodeTime = trackInfo.calculateTrackBaseMediaDecodeTime(
        track, options.keepOriginalTimestamps);

      var moof = mp4.moof(sequenceNumber, [track]);

      sequenceNumber++;

      track.initSegment = mp4.initSegment([track]);

      var boxes = new Uint8Array(moof.byteLength + mdat.byteLength);

      boxes.set(moof);
      boxes.set(mdat, moof.byteLength);

      this.trigger('data', {
        track: track,
        boxes: boxes,
        sequence: sequenceNumber,
        videoFrameDts: frame.dts,
        videoFramePts: frame.pts
      });
    }

    nalUnits = [];
  };

  this.resetTimingAndConfig_ = function() {
    config = undefined;
    pps = undefined;
    segmentStartPts = null;
    segmentEndPts = null;
  };

  this.partialFlush = function() {
    this.processNals_(true);
    this.trigger('partialdone', 'VideoSegmentStream');
  };

  this.flush = function() {
    this.processNals_(false);
    // reset config and pps because they may differ across segments
    // for instance, when we are rendition switching
    this.resetTimingAndConfig_();
    this.trigger('done', 'VideoSegmentStream');
  };

  this.endTimeline = function() {
    this.flush();
    this.trigger('endedtimeline', 'VideoSegmentStream');
  };

  this.reset = function() {
    this.resetTimingAndConfig_();
    frameCache = [];
    nalUnits = [];
    ensureNextFrameIsKeyFrame = true;
    this.trigger('reset');
  };
};

VideoSegmentStream.prototype = new Stream();

module.exports = VideoSegmentStream;