Define DTS codec, add muxing and demuxing support across ISOBMFF, Matroska, and MPEG-TS, add DTS encode/decode support to @mediabunny/server

This commit is contained in:
Vanilagy
2026-08-15 16:19:12 +02:00
parent ae66f01566
commit 914907b5db
27 changed files with 1182 additions and 56 deletions
+186 -11
View File
@@ -13,6 +13,7 @@ import { aacChannelMap, aacFrequencyTable } from '../../shared/aac-misc';
import {
AacCodecInfo,
AudioCodec,
DtsFourCc,
extractAudioCodecString,
extractVideoCodecString,
MediaCodec,
@@ -38,6 +39,12 @@ import {
AC3_FRAME_SIZES,
extractNalUnitTypeForAvc,
extractNalUnitTypeForHevc,
parseDtsFrame,
parseDtsCoreFrameHeader,
parseDtsExssHeader,
DTS_CORE_FRAME_HEADER_SIZE,
DTS_EXSS_HEADER_PREFIX_SIZE,
DTS_EXSS_MAX_HEADER_SIZE,
} from '../codec-data';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
@@ -82,6 +89,22 @@ import { Bitstream } from '../../shared/bitstream';
const MISSING_PTS_ERROR_MESSAGE = 'PES packet is missing PTS where it was expected. PES packets without PTS are not'
+ ' currently supported. If you think this file should be supported, please report it.';
const REGISTRATION_DESCRIPTOR_TAG = 0x05;
// The 'HDMV' and 'HDPR' format identifiers, which mark a program as Blu-ray-derived
const HDMV_FORMAT_IDENTIFIER = 0x48444d56;
const HDPR_FORMAT_IDENTIFIER = 0x48445052;
// 'DTS1', 'DTS2' and 'DTS3' all share this prefix
const DTS_FORMAT_IDENTIFIER_PREFIX = 0x44545300;
// Stream types that only mean DTS within a Blu-ray-derived program
const BLU_RAY_DTS_STREAM_TYPES = new Set<number>([
MpegTsStreamType.BLU_RAY_DTS_HD,
MpegTsStreamType.BLU_RAY_DTS_HD_MASTER,
MpegTsStreamType.BLU_RAY_DTS_EXPRESS_SECONDARY,
]);
type ElementaryStream = {
demuxer: MpegTsDemuxer;
pid: number;
@@ -110,6 +133,7 @@ type ElementaryStream = {
codec: AudioCodec;
decoderConfig: AudioDecoderConfig | null;
aacCodecInfo: AacCodecInfo | null;
dtsFormat: DtsFourCc | null;
numberOfChannels: number;
sampleRate: number;
};
@@ -294,7 +318,27 @@ export class MpegTsDemuxer extends Demuxer {
// "The remaining 10 bits specify the number of bytes of the descriptors immediately following the
// program_info_length field"
const programInfoLength = bitstream.readBits(10);
bitstream.skipBits(8 * programInfoLength);
// A few stream types only mean what Blu-ray says they mean, and the program's registration
// descriptor is what tells us we're looking at a Blu-ray-derived stream.
const programInfoEndPos = bitstream.pos + 8 * programInfoLength;
let isBluRayProgram = false;
while (bitstream.pos < programInfoEndPos) {
const descriptorTag = bitstream.readBits(8);
const descriptorLength = bitstream.readBits(8);
const descriptorEndPos = bitstream.pos + 8 * descriptorLength;
if (descriptorTag === REGISTRATION_DESCRIPTOR_TAG && descriptorLength >= 4) {
const formatIdentifier = bitstream.readBits(32);
isBluRayProgram ||= formatIdentifier === HDMV_FORMAT_IDENTIFIER
|| formatIdentifier === HDPR_FORMAT_IDENTIFIER;
}
bitstream.pos = descriptorEndPos;
}
bitstream.pos = programInfoEndPos;
while (8 * (sectionLength + BYTES_BEFORE_SECTION_LENGTH) - bitstream.pos > BITS_IN_CRC_32) {
const streamType = bitstream.readBits(8);
@@ -304,24 +348,40 @@ export class MpegTsDemuxer extends Demuxer {
bitstream.skipBits(6);
const esInfoLength = bitstream.readBits(10);
// Check ES descriptors to detect AC-3/E-AC-3 in System B
// Check ES descriptors to detect AC-3/E-AC-3/DTS in System B
const esInfoEndPos = bitstream.pos + 8 * esInfoLength;
let hasAc3Descriptor = false;
let hasEac3Descriptor = false;
let hasDtsDescriptor = false;
while (bitstream.pos < esInfoEndPos) {
const descriptorTag = bitstream.readBits(8);
const descriptorLength = bitstream.readBits(8);
const descriptorEndPos = bitstream.pos + 8 * descriptorLength;
if (descriptorTag === 0x6a) {
hasAc3Descriptor = true;
} else if (descriptorTag === 0x7a || descriptorTag === 0xcc) {
hasEac3Descriptor = true;
} else if (descriptorTag === 0x7b) {
hasDtsDescriptor = true;
} else if (descriptorTag === REGISTRATION_DESCRIPTOR_TAG && descriptorLength >= 4) {
// DTS uses 'DTS1', 'DTS2' and 'DTS3' to also signal the stream's frame size,
// which we work out from the bitstream anyway
const formatIdentifier = bitstream.readBits(32);
hasDtsDescriptor ||= (formatIdentifier & 0xffffff00) === DTS_FORMAT_IDENTIFIER_PREFIX;
}
bitstream.skipBits(8 * descriptorLength);
bitstream.pos = descriptorEndPos;
}
let info: ElementaryStream['info'] | null = null;
switch (streamType) {
// Blu-ray has its own stream types for the DTS extensions
const effectiveStreamType = isBluRayProgram && BLU_RAY_DTS_STREAM_TYPES.has(streamType)
? MpegTsStreamType.DTS
: streamType;
switch (effectiveStreamType) {
case MpegTsStreamType.AVC:
case MpegTsStreamType.HEVC: {
const codec = streamType === MpegTsStreamType.AVC ? 'avc' : 'hevc';
@@ -350,21 +410,23 @@ export class MpegTsDemuxer extends Demuxer {
case MpegTsStreamType.MP3_MPEG2:
case MpegTsStreamType.AAC:
case MpegTsStreamType.AC3_SYSTEM_A:
case MpegTsStreamType.EAC3_SYSTEM_A: {
case MpegTsStreamType.EAC3_SYSTEM_A:
case MpegTsStreamType.DTS:
case MpegTsStreamType.DTS_ATSC: {
let codec: AudioCodec;
if (
streamType === MpegTsStreamType.MP3_MPEG1
|| streamType === MpegTsStreamType.MP3_MPEG2
effectiveStreamType === MpegTsStreamType.MP3_MPEG1
|| effectiveStreamType === MpegTsStreamType.MP3_MPEG2
) {
codec = 'mp3';
} else if (streamType === MpegTsStreamType.AAC) {
} else if (effectiveStreamType === MpegTsStreamType.AAC) {
codec = 'aac';
} else if (streamType === MpegTsStreamType.AC3_SYSTEM_A) {
} else if (effectiveStreamType === MpegTsStreamType.AC3_SYSTEM_A) {
codec = 'ac3';
} else if (streamType === MpegTsStreamType.EAC3_SYSTEM_A) {
} else if (effectiveStreamType === MpegTsStreamType.EAC3_SYSTEM_A) {
codec = 'eac3';
} else {
throw new Error('Unreachable.');
codec = 'dts';
}
info = {
@@ -372,6 +434,7 @@ export class MpegTsDemuxer extends Demuxer {
codec,
decoderConfig: null,
aacCodecInfo: null,
dtsFormat: null,
numberOfChannels: -1,
sampleRate: -1,
};
@@ -384,6 +447,7 @@ export class MpegTsDemuxer extends Demuxer {
codec: 'eac3',
decoderConfig: null,
aacCodecInfo: null,
dtsFormat: null,
numberOfChannels: -1,
sampleRate: -1,
};
@@ -393,6 +457,17 @@ export class MpegTsDemuxer extends Demuxer {
codec: 'ac3',
decoderConfig: null,
aacCodecInfo: null,
dtsFormat: null,
numberOfChannels: -1,
sampleRate: -1,
};
} else if (hasDtsDescriptor) {
info = {
type: 'audio',
codec: 'dts',
decoderConfig: null,
aacCodecInfo: null,
dtsFormat: null,
numberOfChannels: -1,
sampleRate: -1,
};
@@ -678,6 +753,22 @@ export class MpegTsDemuxer extends Demuxer {
elementaryStream.info.numberOfChannels = getEac3ChannelCount(frameInfo);
elementaryStream.info.sampleRate = sampleRate;
} else if (elementaryStream.info.codec === 'dts') {
const frameInfo = parseDtsFrame(context.suppliedPacket.data);
if (!frameInfo) {
throw new Error(
'Invalid DTS audio stream; could not read frame header from first packet.',
);
}
elementaryStream.info.numberOfChannels = frameInfo.numberOfChannels;
elementaryStream.info.sampleRate = frameInfo.sampleRate;
if (frameInfo.core) {
// Telling the two extension-only variants apart would need us to work out
// whether the asset holds XLL or LBR data, so we leave those unlabeled
elementaryStream.info.dtsFormat = frameInfo.hasExtensions ? 'dtsh' : 'dtsc';
}
} else {
throw new Error('Unhandled.');
}
@@ -687,6 +778,7 @@ export class MpegTsDemuxer extends Demuxer {
codec: elementaryStream.info.codec,
codecDescription: null,
aacCodecInfo: elementaryStream.info.aacCodecInfo,
dtsFormat: elementaryStream.info.dtsFormat,
}),
numberOfChannels: elementaryStream.info.numberOfChannels,
sampleRate: elementaryStream.info.sampleRate,
@@ -2315,6 +2407,89 @@ class PacketReadingContext {
samplesPerFrame * TIMESCALE / elementaryStream.info.sampleRate,
);
return this.supplyPacket(remaining, duration);
} else if (codec === 'dts') {
if (byte !== 0x7f && byte !== 0x64) {
continue;
}
this.skip(-1);
const possibleSyncPos = this.currentPos;
let remaining = this.ensureBuffered(DTS_CORE_FRAME_HEADER_SIZE);
if (remaining instanceof Promise) remaining = await remaining;
if (remaining < DTS_CORE_FRAME_HEADER_SIZE) {
return;
}
const headerBytes = this.readBytes(DTS_CORE_FRAME_HEADER_SIZE);
const core = parseDtsCoreFrameHeader(headerBytes);
let leadingExss = core ? null : parseDtsExssHeader(headerBytes);
if (!core && !leadingExss) {
this.seekTo(possibleSyncPos + 1);
continue;
}
if (leadingExss && !leadingExss.asset) {
// The static fields and the asset descriptor reach past the bytes we read for the
// core header, so take another look with the substream's whole header available
this.seekTo(possibleSyncPos);
const headerBound = Math.min(leadingExss.frameSize, DTS_EXSS_MAX_HEADER_SIZE);
let remaining = this.ensureBuffered(headerBound);
if (remaining instanceof Promise) remaining = await remaining;
leadingExss = parseDtsExssHeader(this.readBytes(remaining)) ?? leadingExss;
}
let frameSize = core ? core.frameSize : leadingExss!.frameSize;
// Extension-only streams put exactly one substream in each frame, so it's only a core
// frame that needs us to go looking for the extensions riding along with it. The core is
// padded out to a 4-byte boundary before the first of them starts.
if (core) {
let nextSubstreamPos = Math.ceil(core.frameSize / 4) * 4;
while (true) {
this.seekTo(possibleSyncPos);
const neededBytes = nextSubstreamPos + DTS_EXSS_HEADER_PREFIX_SIZE;
let remaining = this.ensureBuffered(neededBytes);
if (remaining instanceof Promise) remaining = await remaining;
if (remaining < neededBytes) {
break;
}
this.seekTo(possibleSyncPos + nextSubstreamPos);
const exss = parseDtsExssHeader(this.readBytes(DTS_EXSS_HEADER_PREFIX_SIZE));
if (!exss) {
break;
}
nextSubstreamPos += exss.frameSize;
frameSize = nextSubstreamPos;
}
}
// Only the substream leading the frame declares how many samples the frame holds
const sampleCount = core?.sampleCount ?? leadingExss!.asset?.sampleCount;
if (sampleCount === undefined) {
this.seekTo(possibleSyncPos + 1);
continue;
}
this.seekTo(possibleSyncPos);
remaining = this.ensureBuffered(frameSize);
if (remaining instanceof Promise) remaining = await remaining;
const duration = Math.round(
sampleCount * TIMESCALE / elementaryStream.info.sampleRate,
);
return this.supplyPacket(remaining, duration);
} else {
throw new Error('Unhandled.');
}
+5
View File
@@ -15,6 +15,11 @@ export const enum MpegTsStreamType {
AAC = 0x0f,
AC3_SYSTEM_A = 0x81,
EAC3_SYSTEM_A = 0x87,
DTS = 0x82,
DTS_ATSC = 0x8a,
BLU_RAY_DTS_HD = 0x85,
BLU_RAY_DTS_HD_MASTER = 0x86,
BLU_RAY_DTS_EXPRESS_SECONDARY = 0xa2,
PRIVATE_DATA = 0x06,
AVC = 0x1b,
HEVC = 0x24,
+7 -2
View File
@@ -161,7 +161,7 @@ export class MpegTsMuxer extends Muxer {
assert(meta?.decoderConfig);
const codec = track.source._codec;
assert(codec === 'aac' || codec === 'mp3' || codec === 'ac3' || codec === 'eac3');
assert(codec === 'aac' || codec === 'mp3' || codec === 'ac3' || codec === 'eac3' || codec === 'dts');
let streamType: MpegTsStreamType;
let streamId: number;
@@ -186,6 +186,11 @@ export class MpegTsMuxer extends Muxer {
streamType = MpegTsStreamType.EAC3_SYSTEM_A;
streamId = 0xbd;
}; break;
case 'dts': {
streamType = MpegTsStreamType.DTS;
streamId = 0xbd;
}; break;
}
const pid = FIRST_TRACK_PID + this.trackDatas.length;
@@ -414,7 +419,7 @@ export class MpegTsMuxer extends Muxer {
): Uint8Array {
const codec = (trackData.track as OutputAudioTrack).source._codec;
if (codec === 'mp3' || codec === 'ac3' || codec === 'eac3') {
if (codec === 'mp3' || codec === 'ac3' || codec === 'eac3' || codec === 'dts') {
// We're good
return packet.data;
}