Home Reference Source

src/remux/passthrough-remuxer.ts

import type { InitData } from '../utils/mp4-tools';
import { getDuration, getStartDTS, offsetStartDTS, parseInitSegment } from '../utils/mp4-tools';
import { logger } from '../utils/logger';
import type { TrackSet } from '../types/track';
import type { InitSegmentData, RemuxedTrack, Remuxer, RemuxerResult } from '../types/remuxer';
import type { DemuxedAudioTrack, DemuxedTrack, PassthroughVideoTrack } from '../types/demuxer';

class PassThroughRemuxer implements Remuxer {
  private emitInitSegment: boolean = false;
  private audioCodec?: string;
  private videoCodec?: string;
  private initData?: InitData;
  private initPTS?: number;
  private initTracks?: TrackSet;
  private lastEndDTS: number | null = null;

  destroy () {
  }

  resetTimeStamp (defaultInitPTS) {
    this.initPTS = defaultInitPTS;
    this.lastEndDTS = null;
  }

  resetNextTimestamp () {
    this.lastEndDTS = null;
  }

  resetInitSegment (initSegment: Uint8Array, audioCodec: string | undefined, videoCodec: string | undefined) {
    this.audioCodec = audioCodec;
    this.videoCodec = videoCodec;
    this.generateInitSegment(initSegment);
    this.emitInitSegment = true;
  }

  generateInitSegment (initSegment: Uint8Array): void {
    let { audioCodec, videoCodec } = this;
    if (!initSegment || !initSegment.byteLength) {
      this.initTracks = undefined;
      this.initData = undefined;
      return;
    }
    const initData = this.initData = parseInitSegment(initSegment);

    // default audio codec if nothing specified
    // TODO : extract that from initsegment
    if (!audioCodec) {
      audioCodec = 'mp4a.40.5';
    }

    if (!videoCodec) {
      videoCodec = 'avc1.42e01e';
    }

    const tracks: TrackSet = {};
    if (initData.audio && initData.video) {
      tracks.audiovideo = {
        container: 'video/mp4',
        codec: audioCodec + ',' + videoCodec,
        initSegment,
        id: 'main'
      };
    } else if (initData.audio) {
      tracks.audio = { container: 'audio/mp4', codec: audioCodec, initSegment, id: 'audio' };
    } else if (initData.video) {
      tracks.video = { container: 'video/mp4', codec: videoCodec, initSegment, id: 'main' };
    } else {
      logger.warn('[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes.');
    }
    this.initTracks = tracks;
  }

  remux (audioTrack: DemuxedAudioTrack, videoTrack: PassthroughVideoTrack, id3Track: DemuxedTrack, textTrack: DemuxedTrack, timeOffset: number): RemuxerResult {
    let { initPTS, lastEndDTS } = this;
    const result: RemuxerResult = {
      audio: undefined,
      video: undefined,
      text: textTrack,
      id3: id3Track,
      initSegment: undefined
    };

    // If we haven't yet set a lastEndDTS, or it was reset, set it to the provided timeOffset. We want to use the
    // lastEndDTS over timeOffset whenever possible; during progressive playback, the media source will not update
    // the media duration (which is what timeOffset is provided as) before we need to process the next chunk.
    if (!Number.isFinite(lastEndDTS!)) {
      lastEndDTS = this.lastEndDTS = timeOffset || 0;
    }

    // The binary segment data is added to the videoTrack in the mp4demuxer. We don't check to see if the data is only
    // audio or video (or both); adding it to video was an arbitrary choice.
    const data = videoTrack.samples;
    if (!data || !data.length) {
      return result;
    }

    const initSegment: InitSegmentData = {
      initPTS: undefined,
      timescale: 1
    };
    let initData = this.initData;
    if (!initData || !initData.length) {
      this.generateInitSegment(data);
      initData = this.initData;
    }
    if (!initData || !initData.length) {
      // We can't remux if the initSegment could not be generated
      logger.warn('[passthrough-remuxer.ts]: Failed to generate initSegment.');
      return result;
    }
    if (this.emitInitSegment) {
      initSegment.tracks = this.initTracks as TrackSet;
      this.emitInitSegment = false;
    }

    if (!Number.isFinite(initPTS!)) {
      this.initPTS = initSegment.initPTS = initPTS = computeInitPTS(initData, data, lastEndDTS);
    }

    const duration = getDuration(data, initData);
    const startDTS = lastEndDTS as number;
    const endDTS = duration + startDTS;
    offsetStartDTS(initData, data, initPTS as number);

    if (duration > 0) {
      this.lastEndDTS = endDTS;
    } else {
      logger.warn('Duration parsed from mp4 should be greater than zero');
      this.resetNextTimestamp();
    }

    const hasAudio = !!initData.audio;
    const hasVideo = !!initData.video;

    let type: any = '';
    if (hasAudio) {
      type += 'audio';
    }

    if (hasVideo) {
      type += 'video';
    }

    const track: RemuxedTrack = {
      data1: data,
      startPTS: startDTS,
      startDTS,
      endPTS: endDTS,
      endDTS,
      type,
      hasAudio,
      hasVideo,
      nb: 1,
      dropped: 0
    };

    result.audio = track.type === 'audio' ? track : undefined;
    result.video = track.type !== 'audio' ? track : undefined;
    result.text = textTrack;
    result.id3 = id3Track;
    result.initSegment = initSegment;

    return result;
  }
}

const computeInitPTS = (initData, data, timeOffset) => getStartDTS(initData, data) - timeOffset;

export default PassThroughRemuxer;