mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-02 13:23:51 +02:00
Add MP3 support to MPEG-TS demuxer
This commit is contained in:
@@ -50,13 +50,22 @@ import {
|
|||||||
TRANSFER_CHARACTERISTICS_MAP_INVERSE,
|
TRANSFER_CHARACTERISTICS_MAP_INVERSE,
|
||||||
UNDETERMINED_LANGUAGE,
|
UNDETERMINED_LANGUAGE,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
|
import { FRAME_HEADER_SIZE as MP3_FRAME_HEADER_SIZE, readMp3FrameHeader } from '../../shared/mp3-misc';
|
||||||
import { EncodedPacket, PacketType, PLACEHOLDER_DATA } from '../packet';
|
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 TIMESCALE = 90_000; // MPEG-TS timestamps run on a 90 kHz clock
|
||||||
const TS_PACKET_SIZE = 188;
|
const TS_PACKET_SIZE = 188;
|
||||||
const MISSING_PES_PACKET_ERROR = 'No PES packet found where one was expected.';
|
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 = {
|
type ElementaryStream = {
|
||||||
demuxer: MpegTsDemuxer;
|
demuxer: MpegTsDemuxer;
|
||||||
pid: number;
|
pid: number;
|
||||||
@@ -218,7 +227,18 @@ export class MpegTsDemuxer extends Demuxer {
|
|||||||
let info: ElementaryStream['info'] | null = null;
|
let info: ElementaryStream['info'] | null = null;
|
||||||
|
|
||||||
switch (streamType) {
|
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 = {
|
info = {
|
||||||
type: 'audio',
|
type: 'audio',
|
||||||
codec: 'aac',
|
codec: 'aac',
|
||||||
@@ -228,9 +248,9 @@ export class MpegTsDemuxer extends Demuxer {
|
|||||||
};
|
};
|
||||||
}; break;
|
}; break;
|
||||||
|
|
||||||
case 0x1b:
|
case MpegTsStreamType.AVC:
|
||||||
case 0x24: {
|
case MpegTsStreamType.HEVC: {
|
||||||
const codec = streamType === 0x1b ? 'avc' : 'hevc';
|
const codec = streamType === MpegTsStreamType.AVC ? 'avc' : 'hevc';
|
||||||
|
|
||||||
info = {
|
info = {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
@@ -359,6 +379,19 @@ export class MpegTsDemuxer extends Demuxer {
|
|||||||
elementaryStream.info.sampleRate
|
elementaryStream.info.sampleRate
|
||||||
= aacFrequencyTable[header.samplingFrequencyIndex]!;
|
= 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;
|
elementaryStream.initialized = true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unhandled.');
|
throw new Error('Unhandled.');
|
||||||
@@ -1438,6 +1471,7 @@ class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTr
|
|||||||
override async markNextPacket(context: PacketReadingContext): Promise<void> {
|
override async markNextPacket(context: PacketReadingContext): Promise<void> {
|
||||||
assert(!context.suppliedPacket);
|
assert(!context.suppliedPacket);
|
||||||
|
|
||||||
|
const codec = this.elementaryStream.info.codec;
|
||||||
const CHUNK_SIZE = 128;
|
const CHUNK_SIZE = 128;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -1448,35 +1482,71 @@ class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTr
|
|||||||
|
|
||||||
while (context.currentPos - startPos < remaining) {
|
while (context.currentPos - startPos < remaining) {
|
||||||
const byte = context.readU8();
|
const byte = context.readU8();
|
||||||
if (byte !== 0xff) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.skip(-1);
|
if (codec === 'aac') {
|
||||||
const possibleHeaderStartPos = context.currentPos;
|
if (byte !== 0xff) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let remaining = context.ensureBuffered(MAX_FRAME_HEADER_SIZE);
|
context.skip(-1);
|
||||||
if (remaining instanceof Promise) remaining = await remaining;
|
const possibleHeaderStartPos = context.currentPos;
|
||||||
|
|
||||||
if (remaining < MAX_FRAME_HEADER_SIZE) {
|
let remaining = context.ensureBuffered(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;
|
if (remaining instanceof Promise) remaining = await remaining;
|
||||||
|
|
||||||
return context.supplyPacket(
|
if (remaining < MAX_FRAME_HEADER_SIZE) {
|
||||||
remaining,
|
return;
|
||||||
Math.round(SAMPLES_PER_AAC_FRAME * TIMESCALE / this.elementaryStream.info.sampleRate),
|
}
|
||||||
);
|
|
||||||
|
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 {
|
} 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);
|
expect(audioPacketStats.packetCount).toBe(235);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.only('MPEG-TS with HEVC video', async () => {
|
test('MPEG-TS with HEVC video', async () => {
|
||||||
using input = new Input({
|
using input = new Input({
|
||||||
source: new FilePathSource(path.join(__dirname, '../public/hevc.ts')),
|
source: new FilePathSource(path.join(__dirname, '../public/hevc.ts')),
|
||||||
formats: ALL_FORMATS,
|
formats: ALL_FORMATS,
|
||||||
@@ -518,3 +518,40 @@ test.only('MPEG-TS with HEVC video', async () => {
|
|||||||
expect(packet.duration).toBeCloseTo(0.04166666666);
|
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