diff --git a/src/mpeg-ts/mpeg-ts-demuxer.ts b/src/mpeg-ts/mpeg-ts-demuxer.ts index c939b0d..b9a3774 100644 --- a/src/mpeg-ts/mpeg-ts-demuxer.ts +++ b/src/mpeg-ts/mpeg-ts-demuxer.ts @@ -50,13 +50,22 @@ import { TRANSFER_CHARACTERISTICS_MAP_INVERSE, UNDETERMINED_LANGUAGE, } from '../misc'; +import { FRAME_HEADER_SIZE as MP3_FRAME_HEADER_SIZE, readMp3FrameHeader } from '../../shared/mp3-misc'; import { EncodedPacket, PacketType, PLACEHOLDER_DATA } from '../packet'; -import { FileSlice, readBytes, Reader, readU16Be, readU8 } from '../reader'; +import { FileSlice, readBytes, Reader, readU16Be, readU32Be, readU8 } from '../reader'; const TIMESCALE = 90_000; // MPEG-TS timestamps run on a 90 kHz clock const TS_PACKET_SIZE = 188; const MISSING_PES_PACKET_ERROR = 'No PES packet found where one was expected.'; +const enum MpegTsStreamType { + MP3_MPEG1 = 0x03, + MP3_MPEG2 = 0x04, + AAC = 0x0f, + AVC = 0x1b, + HEVC = 0x24, +} + type ElementaryStream = { demuxer: MpegTsDemuxer; pid: number; @@ -218,7 +227,18 @@ export class MpegTsDemuxer extends Demuxer { let info: ElementaryStream['info'] | null = null; switch (streamType) { - case 0xf: { + case MpegTsStreamType.MP3_MPEG1: + case MpegTsStreamType.MP3_MPEG2: { + info = { + type: 'audio', + codec: 'mp3', + aacCodecInfo: null, + numberOfChannels: -1, + sampleRate: -1, + }; + }; break; + + case MpegTsStreamType.AAC: { info = { type: 'audio', codec: 'aac', @@ -228,9 +248,9 @@ export class MpegTsDemuxer extends Demuxer { }; }; break; - case 0x1b: - case 0x24: { - const codec = streamType === 0x1b ? 'avc' : 'hevc'; + case MpegTsStreamType.AVC: + case MpegTsStreamType.HEVC: { + const codec = streamType === MpegTsStreamType.AVC ? 'avc' : 'hevc'; info = { type: 'video', @@ -359,6 +379,19 @@ export class MpegTsDemuxer extends Demuxer { elementaryStream.info.sampleRate = aacFrequencyTable[header.samplingFrequencyIndex]!; + elementaryStream.initialized = true; + } else if (elementaryStream.info.codec === 'mp3') { + const word = readU32Be(FileSlice.tempFromBytes(pesPacket.data)); + const result = readMp3FrameHeader(word, pesPacket.data.byteLength); + if (!result.header) { + throw new Error( + 'Invalid MP3 audio stream; could not read frame header from first packet.', + ); + } + + elementaryStream.info.numberOfChannels = result.header.channel === 3 ? 1 : 2; + elementaryStream.info.sampleRate = result.header.sampleRate; + elementaryStream.initialized = true; } else { throw new Error('Unhandled.'); @@ -1438,6 +1471,7 @@ class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTr override async markNextPacket(context: PacketReadingContext): Promise { assert(!context.suppliedPacket); + const codec = this.elementaryStream.info.codec; const CHUNK_SIZE = 128; while (true) { @@ -1448,35 +1482,71 @@ class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTr while (context.currentPos - startPos < remaining) { const byte = context.readU8(); - if (byte !== 0xff) { - continue; - } - context.skip(-1); - const possibleHeaderStartPos = context.currentPos; + if (codec === 'aac') { + if (byte !== 0xff) { + continue; + } - let remaining = context.ensureBuffered(MAX_FRAME_HEADER_SIZE); - if (remaining instanceof Promise) remaining = await remaining; + context.skip(-1); + const possibleHeaderStartPos = context.currentPos; - if (remaining < MAX_FRAME_HEADER_SIZE) { - return; - } - - const headerBytes = context.readBytes(MAX_FRAME_HEADER_SIZE); - const header = readAdtsFrameHeader(FileSlice.tempFromBytes(headerBytes)); - - if (header) { - context.seekTo(possibleHeaderStartPos); - - let remaining = context.ensureBuffered(header.frameLength); + let remaining = context.ensureBuffered(MAX_FRAME_HEADER_SIZE); if (remaining instanceof Promise) remaining = await remaining; - return context.supplyPacket( - remaining, - Math.round(SAMPLES_PER_AAC_FRAME * TIMESCALE / this.elementaryStream.info.sampleRate), - ); + if (remaining < MAX_FRAME_HEADER_SIZE) { + return; + } + + const headerBytes = context.readBytes(MAX_FRAME_HEADER_SIZE); + const header = readAdtsFrameHeader(FileSlice.tempFromBytes(headerBytes)); + + if (header) { + context.seekTo(possibleHeaderStartPos); + + let remaining = context.ensureBuffered(header.frameLength); + if (remaining instanceof Promise) remaining = await remaining; + + return context.supplyPacket( + remaining, + Math.round(SAMPLES_PER_AAC_FRAME * TIMESCALE / this.elementaryStream.info.sampleRate), + ); + } else { + context.seekTo(possibleHeaderStartPos + 1); + } + } else if (codec === 'mp3') { + if (byte !== 0xff) { + continue; + } + + context.skip(-1); + const possibleHeaderStartPos = context.currentPos; + + let remaining = context.ensureBuffered(MP3_FRAME_HEADER_SIZE); + if (remaining instanceof Promise) remaining = await remaining; + + if (remaining < MP3_FRAME_HEADER_SIZE) { + return; + } + + const headerBytes = context.readBytes(MP3_FRAME_HEADER_SIZE); + const word = readU32Be(FileSlice.tempFromBytes(headerBytes)); + const result = readMp3FrameHeader(word, null); + + if (result.header) { + context.seekTo(possibleHeaderStartPos); + + let remaining = context.ensureBuffered(result.header.totalSize); + if (remaining instanceof Promise) remaining = await remaining; + + const duration = result.header.audioSamplesInFrame * TIMESCALE + / this.elementaryStream.info.sampleRate; + return context.supplyPacket(remaining, Math.round(duration)); + } else { + context.seekTo(possibleHeaderStartPos + 1); + } } else { - context.seekTo(possibleHeaderStartPos + 1); + throw new Error('Unreachable'); } } diff --git a/test/node/mpeg-ts-demuxing.test.ts b/test/node/mpeg-ts-demuxing.test.ts index 40883e5..bda897b 100644 --- a/test/node/mpeg-ts-demuxing.test.ts +++ b/test/node/mpeg-ts-demuxing.test.ts @@ -483,7 +483,7 @@ test('MPEG-TS transmuxed by FFmpeg', async () => { expect(audioPacketStats.packetCount).toBe(235); }); -test.only('MPEG-TS with HEVC video', async () => { +test('MPEG-TS with HEVC video', async () => { using input = new Input({ source: new FilePathSource(path.join(__dirname, '../public/hevc.ts')), formats: ALL_FORMATS, @@ -518,3 +518,40 @@ test.only('MPEG-TS with HEVC video', async () => { expect(packet.duration).toBeCloseTo(0.04166666666); } }); + +test('MPEG-TS with MP3 audio', async () => { + using input = new Input({ + source: new FilePathSource(path.join(__dirname, '../public/mp3.ts')), + formats: ALL_FORMATS, + }); + + const audioTrack = await input.getPrimaryAudioTrack(); + assert(audioTrack); + + expect(audioTrack.codec).toBe('mp3'); + expect(audioTrack.internalCodecId).toBe(0x03); + + const audioDecoderConfig = await audioTrack.getDecoderConfig(); + expect(audioDecoderConfig).toEqual({ + codec: 'mp3', + numberOfChannels: 2, + sampleRate: 48000, + }); + + const sink = new EncodedPacketSink(audioTrack); + + const firstPacket = await sink.getFirstPacket(); + assert(firstPacket); + + expect(firstPacket.data[0]).toBe(0xff); // MP3 sync byte + expect(firstPacket.type).toBe('key'); + expect(firstPacket.duration).toBeGreaterThan(0); + + let count = 0; + for await (const packet of sink.packets()) { + expect(packet.data[0]).toBe(0xff); + count++; + } + + expect(count).toBeGreaterThan(0); +}); diff --git a/test/public/mp3.ts b/test/public/mp3.ts new file mode 100644 index 0000000..30ece63 Binary files /dev/null and b/test/public/mp3.ts differ