src/demux/tsdemuxer.ts
- /**
- * highly optimized TS demuxer:
- * parse PAT, PMT
- * extract PES packet from audio and video PIDs
- * extract AVC/H264 NAL units and AAC/ADTS samples from PES packet
- * trigger the remuxer upon parsing completion
- * it also tries to workaround as best as it can audio codec switch (HE-AAC to AAC and vice versa), without having to restart the MediaSource.
- * it also controls the remuxing process :
- * upon discontinuity or level switch detection, it will also notifies the remuxer so that it can reset its state.
- */
-
- import * as ADTS from './adts';
- import * as MpegAudio from './mpegaudio';
- import ExpGolomb from './exp-golomb';
- import { utf8ArrayToStr } from './id3';
- import SampleAesDecrypter from './sample-aes';
- import { Events } from '../events';
- import { appendUint8Array } from '../utils/mp4-tools';
- import { logger } from '../utils/logger';
- import { ErrorTypes, ErrorDetails } from '../errors';
- import type { HlsConfig } from '../config';
- import type { HlsEventEmitter } from '../events';
- import type {
- DemuxedAvcTrack,
- DemuxedAudioTrack,
- DemuxedTrack,
- Demuxer,
- DemuxerResult,
- AvcSample,
- DemuxedMetadataTrack,
- DemuxedUserdataTrack,
- ElementaryStreamData,
- KeyData,
- } from '../types/demuxer';
-
- // We are using fixed track IDs for driving the MP4 remuxer
- // instead of following the TS PIDs.
- // There is no reason not to do this and some browsers/SourceBuffer-demuxers
- // may not like if there are TrackID "switches"
- // See https://github.com/video-dev/hls.js/issues/1331
- // Here we are mapping our internal track types to constant MP4 track IDs
- // With MSE currently one can only have one track of each, and we are muxing
- // whatever video/audio rendition in them.
- const RemuxerTrackIdConfig = {
- video: 1,
- audio: 2,
- id3: 3,
- text: 4,
- };
-
- type ParsedTimestamp = {
- pts?: number;
- dts?: number;
- };
-
- type PES = ParsedTimestamp & {
- data: Uint8Array;
- len: number;
- };
-
- type ParsedAvcSample = ParsedTimestamp & Omit<AvcSample, 'pts' | 'dts'>;
-
- export interface TypeSupported {
- mpeg: boolean;
- mp3: boolean;
- mp4: boolean;
- }
-
- class TSDemuxer implements Demuxer {
- static readonly minProbeByteLength = 188;
-
- private readonly observer: HlsEventEmitter;
- private readonly config: HlsConfig;
- private typeSupported: TypeSupported;
-
- private sampleAes: SampleAesDecrypter | null = null;
- private pmtParsed: boolean = false;
- private audioCodec!: string;
- private videoCodec!: string;
- private _duration: number = 0;
- private aacLastPTS: number | null = null;
- private _initPTS: number | null = null;
- private _initDTS?: number | null = null;
- private _pmtId: number = -1;
-
- private _avcTrack!: DemuxedAvcTrack;
- private _audioTrack!: DemuxedAudioTrack;
- private _id3Track!: DemuxedMetadataTrack;
- private _txtTrack!: DemuxedUserdataTrack;
- private aacOverFlow: Uint8Array | null = null;
- private avcSample: ParsedAvcSample | null = null;
- private remainderData: Uint8Array | null = null;
-
- constructor(
- observer: HlsEventEmitter,
- config: HlsConfig,
- typeSupported: TypeSupported
- ) {
- this.observer = observer;
- this.config = config;
- this.typeSupported = typeSupported;
- }
-
- static probe(data: Uint8Array) {
- 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: Uint8Array) {
- // scan 1000 first bytes
- const scanwindow = Math.min(1000, data.length - 3 * 188);
- let i = 0;
- while (i < scanwindow) {
- // a TS fragment should contain at least 3 TS packets, 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;
- }
-
- /**
- * Creates a track model internal to demuxer used to drive remuxing input
- *
- * @param type 'audio' | 'video' | 'id3' | 'text'
- * @param duration
- * @return TSDemuxer's internal track model
- */
- static createTrack(
- type: 'audio' | 'video' | 'id3' | 'text',
- duration: number
- ): DemuxedTrack {
- return {
- container:
- type === 'video' || type === 'audio' ? 'video/mp2t' : undefined,
- type,
- id: RemuxerTrackIdConfig[type],
- pid: -1,
- inputTimeScale: 90000,
- sequenceNumber: 0,
- samples: [],
- dropped: 0,
- duration: type === 'audio' ? duration : undefined,
- };
- }
-
- /**
- * Initializes a new init segment on the demuxer/remuxer interface. Needed for discontinuities/track-switches (or at stream start)
- * Resets all internal track instances of the demuxer.
- */
- public resetInitSegment(
- audioCodec: string,
- videoCodec: string,
- duration: number
- ) {
- this.pmtParsed = false;
- this._pmtId = -1;
-
- this._avcTrack = TSDemuxer.createTrack(
- 'video',
- duration
- ) as DemuxedAvcTrack;
- this._audioTrack = TSDemuxer.createTrack(
- 'audio',
- duration
- ) as DemuxedAudioTrack;
- this._id3Track = TSDemuxer.createTrack(
- 'id3',
- duration
- ) as DemuxedMetadataTrack;
- this._txtTrack = TSDemuxer.createTrack(
- 'text',
- duration
- ) as DemuxedUserdataTrack;
- this._audioTrack.isAAC = true;
-
- // flush any partial content
- this.aacOverFlow = null;
- this.aacLastPTS = null;
- this.avcSample = null;
- this.audioCodec = audioCodec;
- this.videoCodec = videoCodec;
- this._duration = duration;
- }
-
- public resetTimeStamp() {}
-
- public resetContiguity(): void {
- const { _audioTrack, _avcTrack, _id3Track } = this;
- if (_audioTrack) {
- _audioTrack.pesData = null;
- }
- if (_avcTrack) {
- _avcTrack.pesData = null;
- }
- if (_id3Track) {
- _id3Track.pesData = null;
- }
- this.aacOverFlow = null;
- this.aacLastPTS = null;
- }
-
- public demux(
- data: Uint8Array,
- timeOffset: number,
- isSampleAes = false,
- flush = false
- ): DemuxerResult {
- if (!isSampleAes) {
- this.sampleAes = null;
- }
-
- let pes: PES | null;
-
- const avcTrack = this._avcTrack;
- const audioTrack = this._audioTrack;
- const id3Track = this._id3Track;
-
- let avcId = avcTrack.pid;
- let avcData = avcTrack.pesData;
- let audioId = audioTrack.pid;
- let id3Id = id3Track.pid;
- let audioData = audioTrack.pesData;
- let id3Data = id3Track.pesData;
- let unknownPIDs = false;
- let pmtParsed = this.pmtParsed;
- let pmtId = this._pmtId;
-
- let len = data.length;
- if (this.remainderData) {
- data = appendUint8Array(this.remainderData, data);
- len = data.length;
- this.remainderData = null;
- }
-
- if (len < 188 && !flush) {
- this.remainderData = data;
- return {
- audioTrack,
- avcTrack,
- id3Track,
- textTrack: this._txtTrack,
- };
- }
-
- const syncOffset = Math.max(0, TSDemuxer.syncOffset(data));
-
- len -= (len + syncOffset) % 188;
- if (len < data.byteLength && !flush) {
- this.remainderData = new Uint8Array(
- data.buffer,
- len,
- data.buffer.byteLength - len
- );
- }
-
- // loop through TS packets
- for (let start = syncOffset; start < len; start += 188) {
- if (data[start] === 0x47) {
- const stt = !!(data[start + 1] & 0x40);
- // pid is a 13-bit field starting at the last bit of TS[1]
- const pid = ((data[start + 1] & 0x1f) << 8) + data[start + 2];
- const atf = (data[start + 3] & 0x30) >> 4;
-
- // if an adaption field is present, its length is specified by the fifth byte of the TS packet header.
- let offset: number;
- if (atf > 1) {
- offset = start + 5 + data[start + 4];
- // continue if there is only adaptation field
- if (offset === start + 188) {
- continue;
- }
- } else {
- offset = start + 4;
- }
- switch (pid) {
- case avcId:
- if (stt) {
- if (avcData && (pes = parsePES(avcData))) {
- this.parseAVCPES(pes, false);
- }
-
- avcData = { data: [], size: 0 };
- }
- if (avcData) {
- avcData.data.push(data.subarray(offset, start + 188));
- avcData.size += start + 188 - offset;
- }
- break;
- case audioId:
- if (stt) {
- if (audioData && (pes = parsePES(audioData))) {
- if (audioTrack.isAAC) {
- this.parseAACPES(pes);
- } else {
- this.parseMPEGPES(pes);
- }
- }
- audioData = { data: [], size: 0 };
- }
- if (audioData) {
- audioData.data.push(data.subarray(offset, start + 188));
- audioData.size += start + 188 - offset;
- }
- break;
- case id3Id:
- if (stt) {
- if (id3Data && (pes = parsePES(id3Data))) {
- this.parseID3PES(pes);
- }
-
- id3Data = { data: [], size: 0 };
- }
- if (id3Data) {
- id3Data.data.push(data.subarray(offset, start + 188));
- id3Data.size += start + 188 - offset;
- }
- break;
- case 0:
- if (stt) {
- offset += data[offset] + 1;
- }
-
- pmtId = this._pmtId = parsePAT(data, offset);
- break;
- case pmtId: {
- if (stt) {
- offset += data[offset] + 1;
- }
-
- const parsedPIDs = parsePMT(
- data,
- offset,
- this.typeSupported.mpeg === true ||
- this.typeSupported.mp3 === true,
- isSampleAes
- );
-
- // only update track id if track PID found while parsing PMT
- // this is to avoid resetting the PID to -1 in case
- // track PID transiently disappears from the stream
- // this could happen in case of transient missing audio samples for example
- // NOTE this is only the PID of the track as found in TS,
- // but we are not using this for MP4 track IDs.
- avcId = parsedPIDs.avc;
- if (avcId > 0) {
- avcTrack.pid = avcId;
- }
-
- audioId = parsedPIDs.audio;
- if (audioId > 0) {
- audioTrack.pid = audioId;
- audioTrack.isAAC = parsedPIDs.isAAC;
- }
- id3Id = parsedPIDs.id3;
- if (id3Id > 0) {
- id3Track.pid = id3Id;
- }
-
- if (unknownPIDs && !pmtParsed) {
- logger.log('reparse from beginning');
- unknownPIDs = false;
- // we set it to -188, the += 188 in the for loop will reset start to 0
- start = syncOffset - 188;
- }
- pmtParsed = this.pmtParsed = true;
- break;
- }
- case 17:
- case 0x1fff:
- break;
- default:
- unknownPIDs = true;
- break;
- }
- } else {
- this.observer.emit(Events.ERROR, Events.ERROR, {
- type: ErrorTypes.MEDIA_ERROR,
- details: ErrorDetails.FRAG_PARSING_ERROR,
- fatal: false,
- reason: 'TS packet did not start with 0x47',
- });
- }
- }
-
- avcTrack.pesData = avcData;
- audioTrack.pesData = audioData;
- id3Track.pesData = id3Data;
-
- const demuxResult: DemuxerResult = {
- audioTrack,
- avcTrack,
- id3Track,
- textTrack: this._txtTrack,
- };
- this.extractRemainingSamples(demuxResult);
- return demuxResult;
- }
-
- public flush(): DemuxerResult | Promise<DemuxerResult> {
- const { remainderData } = this;
- this.remainderData = null;
- let result: DemuxerResult;
- if (remainderData) {
- result = this.demux(remainderData, -1, false, true);
- } else {
- result = {
- audioTrack: this._audioTrack,
- avcTrack: this._avcTrack,
- textTrack: this._txtTrack,
- id3Track: this._id3Track,
- };
- }
- this.extractRemainingSamples(result);
- if (this.sampleAes) {
- return this.decrypt(result, this.sampleAes);
- }
- return result;
- }
-
- private extractRemainingSamples(demuxResult: DemuxerResult) {
- const { audioTrack, avcTrack, id3Track } = demuxResult;
- const avcData = avcTrack.pesData;
- const audioData = audioTrack.pesData;
- const id3Data = id3Track.pesData;
- // try to parse last PES packets
- let pes: PES | null;
- if (avcData && (pes = parsePES(avcData))) {
- this.parseAVCPES(pes, true);
- avcTrack.pesData = null;
- } else {
- // either avcData null or PES truncated, keep it for next frag parsing
- avcTrack.pesData = avcData;
- }
-
- if (audioData && (pes = parsePES(audioData))) {
- if (audioTrack.isAAC) {
- this.parseAACPES(pes);
- } else {
- this.parseMPEGPES(pes);
- }
-
- audioTrack.pesData = null;
- } else {
- if (audioData?.size) {
- logger.log(
- 'last AAC PES packet truncated,might overlap between fragments'
- );
- }
-
- // either audioData null or PES truncated, keep it for next frag parsing
- audioTrack.pesData = audioData;
- }
-
- if (id3Data && (pes = parsePES(id3Data))) {
- this.parseID3PES(pes);
- id3Track.pesData = null;
- } else {
- // either id3Data null or PES truncated, keep it for next frag parsing
- id3Track.pesData = id3Data;
- }
- }
-
- public demuxSampleAes(
- data: Uint8Array,
- keyData: KeyData,
- timeOffset: number
- ): Promise<DemuxerResult> {
- const demuxResult = this.demux(data, timeOffset, true);
- const sampleAes = (this.sampleAes = new SampleAesDecrypter(
- this.observer,
- this.config,
- keyData
- ));
- return this.decrypt(demuxResult, sampleAes);
- }
-
- private decrypt(
- demuxResult: DemuxerResult,
- sampleAes: SampleAesDecrypter
- ): Promise<DemuxerResult> {
- return new Promise((resolve) => {
- const { audioTrack, avcTrack } = demuxResult;
- if (audioTrack.samples && audioTrack.isAAC) {
- sampleAes.decryptAacSamples(audioTrack.samples, 0, () => {
- if (avcTrack.samples) {
- sampleAes.decryptAvcSamples(avcTrack.samples, 0, 0, () => {
- resolve(demuxResult);
- });
- } else {
- resolve(demuxResult);
- }
- });
- } else if (avcTrack.samples) {
- sampleAes.decryptAvcSamples(avcTrack.samples, 0, 0, () => {
- resolve(demuxResult);
- });
- }
- });
- }
-
- public destroy() {
- this._initPTS = this._initDTS = null;
- this._duration = 0;
- }
-
- private parseAVCPES(pes: PES, last: boolean) {
- const track = this._avcTrack;
- const units = this.parseAVCNALu(pes.data);
- const debug = false;
- let avcSample = this.avcSample;
- let push: boolean;
- let spsfound = false;
- // free pes.data to save up some memory
- (pes as any).data = null;
-
- // if new NAL units found and last sample still there, let's push ...
- // this helps parsing streams with missing AUD (only do this if AUD never found)
- if (avcSample && units.length && !track.audFound) {
- pushAccessUnit(avcSample, track);
- avcSample = this.avcSample = createAVCSample(false, pes.pts, pes.dts, '');
- }
-
- units.forEach((unit) => {
- switch (unit.type) {
- // NDR
- case 1: {
- push = true;
- if (!avcSample) {
- avcSample = this.avcSample = createAVCSample(
- true,
- pes.pts,
- pes.dts,
- ''
- );
- }
-
- if (debug) {
- avcSample.debug += 'NDR ';
- }
-
- avcSample.frame = true;
- const data = unit.data;
- // only check slice type to detect KF in case SPS found in same packet (any keyframe is preceded by SPS ...)
- if (spsfound && data.length > 4) {
- // retrieve slice type by parsing beginning of NAL unit (follow H264 spec, slice_header definition) to detect keyframe embedded in NDR
- const sliceType = new ExpGolomb(data).readSliceType();
- // 2 : I slice, 4 : SI slice, 7 : I slice, 9: SI slice
- // SI slice : A slice that is coded using intra prediction only and using quantisation of the prediction samples.
- // An SI slice can be coded such that its decoded samples can be constructed identically to an SP slice.
- // I slice: A slice that is not an SI slice that is decoded using intra prediction only.
- // if (sliceType === 2 || sliceType === 7) {
- if (
- sliceType === 2 ||
- sliceType === 4 ||
- sliceType === 7 ||
- sliceType === 9
- ) {
- avcSample.key = true;
- }
- }
- break;
- // IDR
- }
- case 5:
- push = true;
- // handle PES not starting with AUD
- if (!avcSample) {
- avcSample = this.avcSample = createAVCSample(
- true,
- pes.pts,
- pes.dts,
- ''
- );
- }
-
- if (debug) {
- avcSample.debug += 'IDR ';
- }
-
- avcSample.key = true;
- avcSample.frame = true;
- break;
- // SEI
- case 6: {
- push = true;
- if (debug && avcSample) {
- avcSample.debug += 'SEI ';
- }
-
- const expGolombDecoder = new ExpGolomb(discardEPB(unit.data));
-
- // skip frameType
- expGolombDecoder.readUByte();
-
- let payloadType = 0;
- let payloadSize = 0;
- let endOfCaptions = false;
- let b = 0;
-
- while (!endOfCaptions && expGolombDecoder.bytesAvailable > 1) {
- payloadType = 0;
- do {
- b = expGolombDecoder.readUByte();
- payloadType += b;
- } while (b === 0xff);
-
- // Parse payload size.
- payloadSize = 0;
- do {
- b = expGolombDecoder.readUByte();
- payloadSize += b;
- } while (b === 0xff);
-
- // TODO: there can be more than one payload in an SEI packet...
- // TODO: need to read type and size in a while loop to get them all
- if (payloadType === 4 && expGolombDecoder.bytesAvailable !== 0) {
- endOfCaptions = true;
-
- const countryCode = expGolombDecoder.readUByte();
-
- if (countryCode === 181) {
- const providerCode = expGolombDecoder.readUShort();
-
- if (providerCode === 49) {
- const userStructure = expGolombDecoder.readUInt();
-
- if (userStructure === 0x47413934) {
- const userDataType = expGolombDecoder.readUByte();
-
- // Raw CEA-608 bytes wrapped in CEA-708 packet
- if (userDataType === 3) {
- const firstByte = expGolombDecoder.readUByte();
- const secondByte = expGolombDecoder.readUByte();
-
- const totalCCs = 31 & firstByte;
- const byteArray = [firstByte, secondByte];
-
- for (let i = 0; i < totalCCs; i++) {
- // 3 bytes per CC
- byteArray.push(expGolombDecoder.readUByte());
- byteArray.push(expGolombDecoder.readUByte());
- byteArray.push(expGolombDecoder.readUByte());
- }
-
- insertSampleInOrder(this._txtTrack.samples, {
- type: 3,
- pts: pes.pts,
- bytes: byteArray,
- });
- }
- }
- }
- }
- } else if (
- payloadType === 5 &&
- expGolombDecoder.bytesAvailable !== 0
- ) {
- endOfCaptions = true;
-
- if (payloadSize > 16) {
- const uuidStrArray: Array<string> = [];
- for (let i = 0; i < 16; i++) {
- uuidStrArray.push(expGolombDecoder.readUByte().toString(16));
-
- if (i === 3 || i === 5 || i === 7 || i === 9) {
- uuidStrArray.push('-');
- }
- }
- const length = payloadSize - 16;
- const userDataPayloadBytes = new Uint8Array(length);
- for (let i = 0; i < length; i++) {
- userDataPayloadBytes[i] = expGolombDecoder.readUByte();
- }
-
- insertSampleInOrder(this._txtTrack.samples, {
- pts: pes.pts,
- payloadType: payloadType,
- uuid: uuidStrArray.join(''),
- userData: utf8ArrayToStr(userDataPayloadBytes),
- userDataBytes: userDataPayloadBytes,
- });
- }
- } else if (payloadSize < expGolombDecoder.bytesAvailable) {
- for (let i = 0; i < payloadSize; i++) {
- expGolombDecoder.readUByte();
- }
- }
- }
- break;
- // SPS
- }
- case 7:
- push = true;
- spsfound = true;
- if (debug && avcSample) {
- avcSample.debug += 'SPS ';
- }
-
- if (!track.sps) {
- const expGolombDecoder = new ExpGolomb(unit.data);
- const config = expGolombDecoder.readSPS();
- track.width = config.width;
- track.height = config.height;
- track.pixelRatio = config.pixelRatio;
- // TODO: `track.sps` is defined as a `number[]`, but we're setting it to a `Uint8Array[]`.
- track.sps = [unit.data] as any;
- track.duration = this._duration;
- const codecarray = unit.data.subarray(1, 4);
- let codecstring = 'avc1.';
- for (let i = 0; i < 3; i++) {
- let h = codecarray[i].toString(16);
- if (h.length < 2) {
- h = '0' + h;
- }
-
- codecstring += h;
- }
- track.codec = codecstring;
- }
- break;
- // PPS
- case 8:
- push = true;
- if (debug && avcSample) {
- avcSample.debug += 'PPS ';
- }
-
- if (!track.pps) {
- // TODO: `track.pss` is defined as a `number[]`, but we're setting it to a `Uint8Array[]`.
- track.pps = [unit.data] as any;
- }
-
- break;
- // AUD
- case 9:
- push = false;
- track.audFound = true;
- if (avcSample) {
- pushAccessUnit(avcSample, track);
- }
-
- avcSample = this.avcSample = createAVCSample(
- false,
- pes.pts,
- pes.dts,
- debug ? 'AUD ' : ''
- );
- break;
- // Filler Data
- case 12:
- push = false;
- break;
- default:
- push = false;
- if (avcSample) {
- avcSample.debug += 'unknown NAL ' + unit.type + ' ';
- }
-
- break;
- }
- if (avcSample && push) {
- const units = avcSample.units;
- units.push(unit);
- }
- });
- // if last PES packet, push samples
- if (last && avcSample) {
- pushAccessUnit(avcSample, track);
- this.avcSample = null;
- }
- }
-
- private getLastNalUnit() {
- let avcSample = this.avcSample;
- let lastUnit;
- // try to fallback to previous sample if current one is empty
- if (!avcSample || avcSample.units.length === 0) {
- const samples = this._avcTrack.samples;
- avcSample = samples[samples.length - 1];
- }
- if (avcSample?.units) {
- const units = avcSample.units;
- lastUnit = units[units.length - 1];
- }
- return lastUnit;
- }
-
- private parseAVCNALu(
- array: Uint8Array
- ): Array<{
- data: Uint8Array;
- type: number;
- state?: number;
- }> {
- const len = array.byteLength;
- const track = this._avcTrack;
- let state = track.naluState || 0;
- const lastState = state;
- const units = [] as Array<{
- data: Uint8Array;
- type: number;
- state?: number;
- }>;
- let i = 0;
- let value;
- let overflow;
- let unitType;
- let lastUnitStart = -1;
- let lastUnitType: number = 0;
- // logger.log('PES:' + Hex.hexDump(array));
-
- if (state === -1) {
- // special use case where we found 3 or 4-byte start codes exactly at the end of previous PES packet
- lastUnitStart = 0;
- // NALu type is value read from offset 0
- lastUnitType = array[0] & 0x1f;
- state = 0;
- i = 1;
- }
-
- while (i < len) {
- value = array[i++];
- // optimization. state 0 and 1 are the predominant case. let's handle them outside of the switch/case
- if (!state) {
- state = value ? 0 : 1;
- continue;
- }
- if (state === 1) {
- state = value ? 0 : 2;
- continue;
- }
- // here we have state either equal to 2 or 3
- if (!value) {
- state = 3;
- } else if (value === 1) {
- if (lastUnitStart >= 0) {
- const unit = {
- data: array.subarray(lastUnitStart, i - state - 1),
- type: lastUnitType,
- };
- // logger.log('pushing NALU, type/size:' + unit.type + '/' + unit.data.byteLength);
- units.push(unit);
- } else {
- // lastUnitStart is undefined => this is the first start code found in this PES packet
- // first check if start code delimiter is overlapping between 2 PES packets,
- // ie it started in last packet (lastState not zero)
- // and ended at the beginning of this PES packet (i <= 4 - lastState)
- const lastUnit = this.getLastNalUnit();
- if (lastUnit) {
- if (lastState && i <= 4 - lastState) {
- // start delimiter overlapping between PES packets
- // strip start delimiter bytes from the end of last NAL unit
- // check if lastUnit had a state different from zero
- if (lastUnit.state) {
- // strip last bytes
- lastUnit.data = lastUnit.data.subarray(
- 0,
- lastUnit.data.byteLength - lastState
- );
- }
- }
- // If NAL units are not starting right at the beginning of the PES packet, push preceding data into previous NAL unit.
- overflow = i - state - 1;
- if (overflow > 0) {
- // logger.log('first NALU found with overflow:' + overflow);
- const tmp = new Uint8Array(lastUnit.data.byteLength + overflow);
- tmp.set(lastUnit.data, 0);
- tmp.set(array.subarray(0, overflow), lastUnit.data.byteLength);
- lastUnit.data = tmp;
- }
- }
- }
- // check if we can read unit type
- if (i < len) {
- unitType = array[i] & 0x1f;
- // logger.log('find NALU @ offset:' + i + ',type:' + unitType);
- lastUnitStart = i;
- lastUnitType = unitType;
- state = 0;
- } else {
- // not enough byte to read unit type. let's read it on next PES parsing
- state = -1;
- }
- } else {
- state = 0;
- }
- }
- if (lastUnitStart >= 0 && state >= 0) {
- const unit = {
- data: array.subarray(lastUnitStart, len),
- type: lastUnitType,
- state: state,
- };
- units.push(unit);
- // logger.log('pushing NALU, type/size/state:' + unit.type + '/' + unit.data.byteLength + '/' + state);
- }
- // no NALu found
- if (units.length === 0) {
- // append pes.data to previous NAL unit
- const lastUnit = this.getLastNalUnit();
- if (lastUnit) {
- const tmp = new Uint8Array(lastUnit.data.byteLength + array.byteLength);
- tmp.set(lastUnit.data, 0);
- tmp.set(array, lastUnit.data.byteLength);
- lastUnit.data = tmp;
- }
- }
- track.naluState = state;
- return units;
- }
-
- private parseAACPES(pes: PES) {
- const startOffset = 0;
- const track = this._audioTrack;
- const aacLastPTS = this.aacLastPTS;
- const aacOverFlow = this.aacOverFlow;
- let data = pes.data;
- if (aacOverFlow) {
- const tmp = new Uint8Array(aacOverFlow.byteLength + data.byteLength);
- tmp.set(aacOverFlow, 0);
- tmp.set(data, aacOverFlow.byteLength);
- // logger.log(`AAC: append overflowing ${aacOverFlow.byteLength} bytes to beginning of new PES`);
- data = tmp;
- }
- // look for ADTS header (0xFFFx)
- let offset: number;
- let len: number;
- for (offset = startOffset, len = data.length; offset < len - 1; offset++) {
- if (ADTS.isHeader(data, offset)) {
- break;
- }
- }
- // if ADTS header does not start straight from the beginning of the PES payload, raise an error
- if (offset) {
- let reason;
- let fatal;
- if (offset < len - 1) {
- reason = `AAC PES did not start with ADTS header,offset:${offset}`;
- fatal = false;
- } else {
- reason = 'no ADTS header found in AAC PES';
- fatal = true;
- }
- logger.warn(`parsing error:${reason}`);
- this.observer.emit(Events.ERROR, Events.ERROR, {
- type: ErrorTypes.MEDIA_ERROR,
- details: ErrorDetails.FRAG_PARSING_ERROR,
- fatal,
- reason,
- });
- if (fatal) {
- return;
- }
- }
-
- ADTS.initTrackConfig(track, this.observer, data, offset, this.audioCodec);
-
- let frameIndex = 0;
- const frameDuration = ADTS.getFrameDuration(track.samplerate as number);
-
- // if last AAC frame is overflowing, we should ensure timestamps are contiguous:
- // first sample PTS should be equal to last sample PTS + frameDuration
- let pts: number;
- if (pes.pts !== undefined) {
- pts = pes.pts;
- } else if (aacLastPTS !== null) {
- pts = aacLastPTS;
- } else {
- logger.warn('[tsdemuxer]: AAC PES unknown PTS');
- return;
- }
- if (aacOverFlow && aacLastPTS !== null) {
- const newPTS = aacLastPTS + frameDuration;
- if (Math.abs(newPTS - pts) > 1) {
- logger.log(
- `[tsdemuxer]: AAC: align PTS for overlapping frames by ${Math.round(
- (newPTS - pts) / 90
- )}`
- );
- pts = newPTS;
- }
- }
-
- // scan for aac samples
- let stamp: number | null = null;
- while (offset < len) {
- if (ADTS.isHeader(data, offset)) {
- if (offset + 5 < len) {
- const frame = ADTS.appendFrame(track, data, offset, pts, frameIndex);
- if (frame) {
- offset += frame.length;
- stamp = frame.sample.pts;
- frameIndex++;
- continue;
- }
- }
- // We are at an ADTS header, but do not have enough data for a frame
- // Remaining data will be added to aacOverFlow
- break;
- } else {
- // nothing found, keep looking
- offset++;
- }
- }
-
- this.aacOverFlow = offset < len ? data.subarray(offset, len) : null;
- this.aacLastPTS = stamp;
- }
-
- private parseMPEGPES(pes: PES) {
- const data = pes.data;
- const length = data.length;
- let frameIndex = 0;
- let offset = 0;
- const pts = pes.pts;
- if (pts === undefined) {
- logger.warn('[tsdemuxer]: MPEG PES unknown PTS');
- return;
- }
-
- while (offset < length) {
- if (MpegAudio.isHeader(data, offset)) {
- const frame = MpegAudio.appendFrame(
- this._audioTrack,
- data,
- offset,
- pts,
- frameIndex
- );
- if (frame) {
- offset += frame.length;
- frameIndex++;
- } else {
- // logger.log('Unable to parse Mpeg audio frame');
- break;
- }
- } else {
- // nothing found, keep looking
- offset++;
- }
- }
- }
-
- private parseID3PES(pes: PES) {
- if (pes.pts === undefined) {
- logger.warn('[tsdemuxer]: ID3 PES unknown PTS');
- return;
- }
- this._id3Track.samples.push(pes as Required<PES>);
- }
- }
-
- function createAVCSample(
- key: boolean,
- pts: number | undefined,
- dts: number | undefined,
- debug: string
- ): ParsedAvcSample {
- return {
- key,
- frame: false,
- pts,
- dts,
- units: [],
- debug,
- length: 0,
- };
- }
-
- function parsePAT(data, offset) {
- // skip the PSI header and parse the first PMT entry
- return ((data[offset + 10] & 0x1f) << 8) | data[offset + 11];
- // logger.log('PMT PID:' + this._pmtId);
- }
-
- function parsePMT(data, offset, mpegSupported, isSampleAes) {
- const result = { audio: -1, avc: -1, id3: -1, isAAC: true };
- const sectionLength = ((data[offset + 1] & 0x0f) << 8) | data[offset + 2];
- const tableEnd = offset + 3 + sectionLength - 4;
- // to determine where the table is, we have to figure out how
- // long the program info descriptors are
- const programInfoLength =
- ((data[offset + 10] & 0x0f) << 8) | data[offset + 11];
- // advance the offset to the first entry in the mapping table
- offset += 12 + programInfoLength;
- while (offset < tableEnd) {
- const pid = ((data[offset + 1] & 0x1f) << 8) | data[offset + 2];
- switch (data[offset]) {
- case 0xcf: // SAMPLE-AES AAC
- if (!isSampleAes) {
- logger.log(
- 'ADTS AAC with AES-128-CBC frame encryption found in unencrypted stream'
- );
- break;
- }
- /* falls through */
- case 0x0f: // ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio)
- // logger.log('AAC PID:' + pid);
- if (result.audio === -1) {
- result.audio = pid;
- }
-
- break;
-
- // Packetized metadata (ID3)
- case 0x15:
- // logger.log('ID3 PID:' + pid);
- if (result.id3 === -1) {
- result.id3 = pid;
- }
-
- break;
-
- case 0xdb: // SAMPLE-AES AVC
- if (!isSampleAes) {
- logger.log(
- 'H.264 with AES-128-CBC slice encryption found in unencrypted stream'
- );
- break;
- }
- /* falls through */
- case 0x1b: // ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video)
- // logger.log('AVC PID:' + pid);
- if (result.avc === -1) {
- result.avc = pid;
- }
-
- break;
-
- // ISO/IEC 11172-3 (MPEG-1 audio)
- // or ISO/IEC 13818-3 (MPEG-2 halved sample rate audio)
- case 0x03:
- case 0x04:
- // logger.log('MPEG PID:' + pid);
- if (!mpegSupported) {
- logger.log('MPEG audio found, not supported in this browser');
- } else if (result.audio === -1) {
- result.audio = pid;
- result.isAAC = false;
- }
- break;
-
- case 0x24:
- logger.warn('Unsupported HEVC stream type found');
- break;
-
- default:
- // logger.log('unknown stream type:' + data[offset]);
- break;
- }
- // move to the next table entry
- // skip past the elementary stream descriptors, if present
- offset += (((data[offset + 3] & 0x0f) << 8) | data[offset + 4]) + 5;
- }
- return result;
- }
-
- function parsePES(stream: ElementaryStreamData): PES | null {
- let i = 0;
- let frag: Uint8Array;
- let pesLen: number;
- let pesHdrLen: number;
- let pesPts: number | undefined;
- let pesDts: number | undefined;
- const data = stream.data;
- // safety check
- if (!stream || stream.size === 0) {
- return null;
- }
-
- // we might need up to 19 bytes to read PES header
- // if first chunk of data is less than 19 bytes, let's merge it with following ones until we get 19 bytes
- // usually only one merge is needed (and this is rare ...)
- while (data[0].length < 19 && data.length > 1) {
- const newData = new Uint8Array(data[0].length + data[1].length);
- newData.set(data[0]);
- newData.set(data[1], data[0].length);
- data[0] = newData;
- data.splice(1, 1);
- }
- // retrieve PTS/DTS from first fragment
- frag = data[0];
- const pesPrefix = (frag[0] << 16) + (frag[1] << 8) + frag[2];
- if (pesPrefix === 1) {
- pesLen = (frag[4] << 8) + frag[5];
- // if PES parsed length is not zero and greater than total received length, stop parsing. PES might be truncated
- // minus 6 : PES header size
- if (pesLen && pesLen > stream.size - 6) {
- return null;
- }
-
- const pesFlags = frag[7];
- if (pesFlags & 0xc0) {
- /* PES header described here : http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
- as PTS / DTS is 33 bit we cannot use bitwise operator in JS,
- as Bitwise operators treat their operands as a sequence of 32 bits */
- pesPts =
- (frag[9] & 0x0e) * 536870912 + // 1 << 29
- (frag[10] & 0xff) * 4194304 + // 1 << 22
- (frag[11] & 0xfe) * 16384 + // 1 << 14
- (frag[12] & 0xff) * 128 + // 1 << 7
- (frag[13] & 0xfe) / 2;
-
- if (pesFlags & 0x40) {
- pesDts =
- (frag[14] & 0x0e) * 536870912 + // 1 << 29
- (frag[15] & 0xff) * 4194304 + // 1 << 22
- (frag[16] & 0xfe) * 16384 + // 1 << 14
- (frag[17] & 0xff) * 128 + // 1 << 7
- (frag[18] & 0xfe) / 2;
-
- if (pesPts - pesDts > 60 * 90000) {
- logger.warn(
- `${Math.round(
- (pesPts - pesDts) / 90000
- )}s delta between PTS and DTS, align them`
- );
- pesPts = pesDts;
- }
- } else {
- pesDts = pesPts;
- }
- }
- pesHdrLen = frag[8];
- // 9 bytes : 6 bytes for PES header + 3 bytes for PES extension
- let payloadStartOffset = pesHdrLen + 9;
- if (stream.size <= payloadStartOffset) {
- return null;
- }
- stream.size -= payloadStartOffset;
- // reassemble PES packet
- const pesData = new Uint8Array(stream.size);
- for (let j = 0, dataLen = data.length; j < dataLen; j++) {
- frag = data[j];
- let len = frag.byteLength;
- if (payloadStartOffset) {
- if (payloadStartOffset > len) {
- // trim full frag if PES header bigger than frag
- payloadStartOffset -= len;
- continue;
- } else {
- // trim partial frag if PES header smaller than frag
- frag = frag.subarray(payloadStartOffset);
- len -= payloadStartOffset;
- payloadStartOffset = 0;
- }
- }
- pesData.set(frag, i);
- i += len;
- }
- if (pesLen) {
- // payload size : remove PES header + PES extension
- pesLen -= pesHdrLen + 3;
- }
- return { data: pesData, pts: pesPts, dts: pesDts, len: pesLen };
- }
- return null;
- }
-
- function pushAccessUnit(avcSample: ParsedAvcSample, avcTrack: DemuxedAvcTrack) {
- if (avcSample.units.length && avcSample.frame) {
- // if sample does not have PTS/DTS, patch with last sample PTS/DTS
- if (avcSample.pts === undefined) {
- const samples = avcTrack.samples;
- const nbSamples = samples.length;
- if (nbSamples) {
- const lastSample = samples[nbSamples - 1];
- avcSample.pts = lastSample.pts;
- avcSample.dts = lastSample.dts;
- } else {
- // dropping samples, no timestamp found
- avcTrack.dropped++;
- return;
- }
- }
- avcTrack.samples.push(avcSample as AvcSample);
- }
- if (avcSample.debug.length) {
- logger.log(avcSample.pts + '/' + avcSample.dts + ':' + avcSample.debug);
- }
- }
-
- function insertSampleInOrder(arr, data) {
- const len = arr.length;
- if (len > 0) {
- if (data.pts >= arr[len - 1].pts) {
- arr.push(data);
- } else {
- for (let pos = len - 1; pos >= 0; pos--) {
- if (data.pts < arr[pos].pts) {
- arr.splice(pos, 0, data);
- break;
- }
- }
- }
- } else {
- arr.push(data);
- }
- }
-
- /**
- * remove Emulation Prevention bytes from a RBSP
- */
- export function discardEPB(data: Uint8Array): Uint8Array {
- const length = data.byteLength;
- const EPBPositions = [] as Array<number>;
- let i = 1;
-
- // Find all `Emulation Prevention Bytes`
- while (i < length - 2) {
- if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
- EPBPositions.push(i + 2);
- i += 2;
- } else {
- i++;
- }
- }
-
- // If no Emulation Prevention Bytes were found just return the original
- // array
- if (EPBPositions.length === 0) {
- return data;
- }
-
- // Create a new array to hold the NAL unit data
- const newLength = length - EPBPositions.length;
- const newData = new Uint8Array(newLength);
- let sourceIndex = 0;
-
- for (i = 0; i < newLength; sourceIndex++, i++) {
- if (sourceIndex === EPBPositions[0]) {
- // Skip this byte
- sourceIndex++;
- // Remove this position index
- EPBPositions.shift();
- }
- newData[i] = data[sourceIndex];
- }
- return newData;
- }
-
- export default TSDemuxer;