Source: segment-inspector.js

/**
 * @file segment-inspector.js
 */

import {TransportPacketStream, TransportParseStream, ElementaryStream, TimestampRolloverStream} from 'mux.js/lib/m2ts';
import StreamTypes from 'mux.js/lib/m2ts/stream-types.js';

const PES_TIMESCALE = 90000;

/**
 * An object that inspects ts segment data to extract timing information for
 * more precise calculations
 *
 * @class SegmentInspector
 */
export default class SegmentInspector {
  constructor() {
    this.transportPacketStream = new TransportPacketStream();
    this.transportParseStream = new TransportParseStream();
    this.elementaryStream = new ElementaryStream();
    this.audioTimestampRolloverStream = new TimestampRolloverStream('audio');
    this.videoTimestampRolloverStream = new TimestampRolloverStream('video');
  }

  /**
   * Inspects the given segment byte array and returns an object with timing
   * information for the first and last packet.
   */
  inspect(segment) {
    var tsPackets = [];
    var segmentInfo;

    this.transportPacketStream
      .pipe(this.transportParseStream);

    this.transportParseStream.on('data', function(event) {
      tsPackets.push(event);
    });

    this.transportParseStream.on('done', () => {
      segmentInfo = this.parsePackets_(tsPackets);
    });

    this.transportPacketStream.push(segment);
    this.transportPacketStream.flush();

    this.adjustTimestamp_(segmentInfo);

    this.dispose();
    return segmentInfo;
  }

  /**
   * Parse the given pes packets to gain information from the first and last complete packet
   */
  parsePackets_(packets) {
    var segmentInfo = {
      video: [],
      audio: []
    };

    var processData = true;
    var first = true;

    this.elementaryStream.on('data', function(data) {
      if (processData) {
        if (data.type === 'audio') {
          if ((first && segmentInfo.audio.length === 0) ||
              (!first && segmentInfo.audio.length === 1)) {
            segmentInfo.audio.push(data);
          }
        }
        if (data.type === 'video') {
          if ((first && segmentInfo.video.length === 0) ||
              (!first && segmentInfo.video.length === 1)) {
            segmentInfo.video.push(data);
          }
        }
        if (first &&
            segmentInfo.audio.length === 1 &&
            segmentInfo.video.length === 1) {
          processData = false;
        } else if (!first &&
                    segmentInfo.audio.length === 2 &&
                    segmentInfo.video.length === 2) {
          processData = false;
        }
      }
    });

    let i = 0;
    let packet;

    while(processData && i < packets.length) {
      packet = packets[i];
      this.elementaryStream.push(packet);
      i++;
    }

    this.elementaryStream.flush();

    processData = true;
    first = false;

    i = packets.length - 1;

    let lastPes = {
      audio: {
        done: false,
        data: []
      },
      video: {
        done: false,
        data: []
      }
    };

    // Walk back from the end to find the last video and audio pes packets
    while(i > -1) {
      packet = packets[i];
      let streamType;

      switch (packet.streamType) {
        case StreamTypes.H264_STREAM_TYPE:
          streamType = 'video';
          break;
        case StreamTypes.ADTS_STREAM_TYPE:
          streamType = 'audio';
          break;
        default:
          i--;
          continue;
      }
      if (!lastPes[streamType].done) {
        lastPes[streamType].data.unshift(packet);

        if (packet.payloadUnitStartIndicator) {
          lastPes[streamType].done = true;
        }
      }

      if (lastPes.audio.done && lastPes.video.done) {
        break;
      }

      i--;
    }

    lastPes.audio.data.forEach((packet) => {
      this.elementaryStream.push(packet);
    });
    lastPes.video.data.forEach((packet) => {
      this.elementaryStream.push(packet);
    });
    this.elementaryStream.flush();

    return segmentInfo;
  }

  /**
   * Adjusts the timestamp information for the segment to account for
   * rollover and convert to seconds based on pes packet timescale (90khz clock)
   */
  adjustTimestamp_(segmentInfo) {
    var i = 0;

    this.audioTimestampRolloverStream.on('data', function(data) {
      segmentInfo.audio[i].pts = data.pts / PES_TIMESCALE;
      segmentInfo.audio[i].dts = data.dts / PES_TIMESCALE;
    });

    this.videoTimestampRolloverStream.on('data', function(data) {
      segmentInfo.video[i].pts = data.pts / PES_TIMESCALE;
      segmentInfo.video[i].dts = data.dts / PES_TIMESCALE;
    });

    this.audioTimestampRolloverStream.push(segmentInfo.audio[i]);
    this.videoTimestampRolloverStream.push(segmentInfo.video[i]);

    i = 1;

    this.audioTimestampRolloverStream.push(segmentInfo.audio[i]);
    this.videoTimestampRolloverStream.push(segmentInfo.video[i]);

    this.audioTimestampRolloverStream.flush();
    this.videoTimestampRolloverStream.flush();
  }

  dispose() {
    this.transportPacketStream.dispose();
    this.transportParseStream.dispose();
    this.elementaryStream.dispose();
    this.audioTimestampRolloverStream.dispose();
    this.videoTimestampRolloverStream.dispose();
  }
}