mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
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:
@@ -25,7 +25,8 @@ export class NodeAvAudioDecoder extends CustomAudioDecoder {
|
||||
|| codec === 'vorbis'
|
||||
|| codec === 'flac'
|
||||
|| codec === 'ac3'
|
||||
|| codec === 'eac3';
|
||||
|| codec === 'eac3'
|
||||
|| codec === 'dts';
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
EncodedPacket,
|
||||
} from 'mediabunny';
|
||||
import * as NodeAv from 'node-av';
|
||||
import { CODEC_TO_CODEC_ID, getChannelLayout } from './misc';
|
||||
import { CODEC_TO_CODEC_ID, getChannelLayout, getDtsChannelLayout } from './misc';
|
||||
import { assert, toUint8Array } from '../../../src/misc';
|
||||
import { copyAudioSampleToAvFrame, AvFrameAudioSampleResource } from './audio-sample';
|
||||
import {
|
||||
@@ -29,6 +29,11 @@ const AAC_SAMPLE_RATES
|
||||
const OPUS_SAMPLE_RATES = [8000, 12000, 16000, 24000, 48000];
|
||||
const MP3_SAMPLE_RATES = [8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000];
|
||||
const AC3_SAMPLE_RATES = [32000, 44100, 48000];
|
||||
const DTS_SAMPLE_RATES = [8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000];
|
||||
|
||||
/** DTS only has layouts for these channel counts; 3 and 7 have no mapping. */
|
||||
const DTS_CHANNEL_COUNTS = [1, 2, 4, 5, 6];
|
||||
const DTS_MAX_FRAME_SIZE = 16384;
|
||||
|
||||
const FRAME_SIZE_FALLBACK = 1024; // Just 'cause
|
||||
|
||||
@@ -72,6 +77,10 @@ export class NodeAvAudioEncoder extends CustomAudioEncoder {
|
||||
) || (
|
||||
codec === 'eac3' && numberOfChannels >= 1 && numberOfChannels <= 16
|
||||
&& AC3_SAMPLE_RATES.includes(sampleRate)
|
||||
) || (
|
||||
codec === 'dts' && DTS_CHANNEL_COUNTS.includes(numberOfChannels)
|
||||
&& DTS_SAMPLE_RATES.includes(sampleRate)
|
||||
&& dtsBitrateFits(resolveBitrate(config, codec), sampleRate, numberOfChannels)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,21 +116,26 @@ export class NodeAvAudioEncoder extends CustomAudioEncoder {
|
||||
}
|
||||
|
||||
codecContext.sampleRate = this.config.sampleRate;
|
||||
codecContext.channelLayout = getChannelLayout(this.config.numberOfChannels);
|
||||
codecContext.channelLayout = this.codec === 'dts'
|
||||
? getDtsChannelLayout(this.config.numberOfChannels)
|
||||
: getChannelLayout(this.config.numberOfChannels);
|
||||
codecContext.codecType = NodeAv.AVMEDIA_TYPE_AUDIO;
|
||||
codecContext.codecId = CODEC_TO_CODEC_ID[this.codec]!;
|
||||
codecContext.sampleFormat = sampleFormat;
|
||||
codecContext.timeBase = new NodeAv.Rational(1, this.config.sampleRate);
|
||||
codecContext.bitRate = BigInt(
|
||||
this.config.bitrate ?? new Quality('medium')._toAudioBitrate(this.codec) ?? 0,
|
||||
);
|
||||
codecContext.bitRate = BigInt(resolveBitrate(this.config, this.codec));
|
||||
|
||||
if (this.config.bitrateMode === 'constant') {
|
||||
codecContext.rcMinRate = codecContext.bitRate;
|
||||
codecContext.rcMaxRate = codecContext.bitRate;
|
||||
}
|
||||
|
||||
const ret = await codecContext.open2();
|
||||
// libav's DTS encoder is marked experimental, so it refuses to open at the default compliance level
|
||||
const options = this.codec === 'dts'
|
||||
? NodeAv.Dictionary.fromObject({ strict: -2 })
|
||||
: null;
|
||||
|
||||
const ret = await codecContext.open2(this.avCodec, options);
|
||||
NodeAv.FFmpegError.throwIfError(ret, 'Open codec context');
|
||||
|
||||
this.codecContext = codecContext;
|
||||
@@ -172,7 +186,9 @@ export class NodeAvAudioEncoder extends CustomAudioEncoder {
|
||||
this.resampler = new NodeAv.SoftwareResampleContext();
|
||||
this.resamplerInputSampleRate = this.frame.sampleRate;
|
||||
|
||||
const outLayout = getChannelLayout(this.codecContext.channels);
|
||||
// Resample straight to whatever layout the encoder was opened with, since not every codec accepts
|
||||
// the layout that getChannelLayout hands out for a given channel count
|
||||
const outLayout = this.codecContext.channelLayout;
|
||||
const inLayout = getChannelLayout(this.frame.channels);
|
||||
|
||||
const ret = this.resampler.allocSetOpts2(
|
||||
@@ -400,3 +416,25 @@ export class NodeAvAudioEncoder extends CustomAudioEncoder {
|
||||
this.resampler?.free();
|
||||
}
|
||||
}
|
||||
|
||||
const resolveBitrate = (config: AudioEncoderConfig, codec: AudioCodec) => {
|
||||
return config.bitrate ?? new Quality('medium')._toAudioBitrate(codec) ?? 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* The DTS encoder needs each frame to be big enough to hold the per-channel side info and rejects the whole
|
||||
* configuration when it isn't, so this mirrors the check from FFmpeg's dcaenc.c.
|
||||
*/
|
||||
const dtsBitrateFits = (bitrate: number, sampleRate: number, numberOfChannels: number) => {
|
||||
if (bitrate < 32000 || bitrate > 3840000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasLfe = numberOfChannels === 6;
|
||||
const fullbandChannels = numberOfChannels - (hasLfe ? 1 : 0);
|
||||
|
||||
const frameBits = 32 * Math.ceil(Math.ceil(bitrate * 512 / sampleRate) / 32);
|
||||
const minFrameBits = 132 + (493 + 28 * 32) * fullbandChannels + (hasLfe ? 72 : 0);
|
||||
|
||||
return frameBits >= minFrameBits && frameBits <= 8 * DTS_MAX_FRAME_SIZE;
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ export const CODEC_TO_CODEC_ID: Partial<Record<MediaCodec, NodeAv.AVCodecID>> =
|
||||
flac: NodeAv.AV_CODEC_ID_FLAC,
|
||||
ac3: NodeAv.AV_CODEC_ID_AC3,
|
||||
eac3: NodeAv.AV_CODEC_ID_EAC3,
|
||||
dts: NodeAv.AV_CODEC_ID_DTS,
|
||||
};
|
||||
|
||||
let cachedHardwareContext: NodeAv.HardwareContext | null | undefined = undefined;
|
||||
@@ -267,3 +268,21 @@ export const getChannelLayout = (numChannels: number): NodeAv.ChannelLayout => {
|
||||
default: return { nbChannels: numChannels, order: NodeAv.AV_CHANNEL_ORDER_UNSPEC, mask: 0n };
|
||||
}
|
||||
};
|
||||
|
||||
/** FL + FR + SL + SR, which libav has no constant for. */
|
||||
const QUAD_SIDE_MASK = 0x603n;
|
||||
|
||||
/**
|
||||
* DTS insists on the side-based surround layouts and rejects the back-based ones that {@link getChannelLayout}
|
||||
* hands out, so it gets its own mapping.
|
||||
*/
|
||||
export const getDtsChannelLayout = (numChannels: number): NodeAv.ChannelLayout => {
|
||||
switch (numChannels) {
|
||||
case 1: return NodeAv.AV_CHANNEL_LAYOUT_MONO;
|
||||
case 2: return NodeAv.AV_CHANNEL_LAYOUT_STEREO;
|
||||
case 4: return { nbChannels: 4, order: NodeAv.AV_CHANNEL_ORDER_NATIVE, mask: QUAD_SIDE_MASK };
|
||||
case 5: return NodeAv.AV_CHANNEL_LAYOUT_5POINT0;
|
||||
case 6: return NodeAv.AV_CHANNEL_LAYOUT_5POINT1;
|
||||
default: return { nbChannels: numChannels, order: NodeAv.AV_CHANNEL_ORDER_UNSPEC, mask: 0n };
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user