mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +02:00
Add MP3 support to MPEG-TS demuxer
This commit is contained in:
@@ -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<void> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user