mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
Explicitly model the supported codecs per output format
This commit is contained in:
+1
-1
@@ -86,7 +86,7 @@
|
||||
bitrate: 1e6
|
||||
});
|
||||
let audioSource = new Metamuxer.AudioBufferSource({
|
||||
codec: 'opus',
|
||||
codec: 'flac',
|
||||
bitrate: 128e3,
|
||||
});
|
||||
let subtitleSource = new Metamuxer.TextSubtitleSource('webvtt');
|
||||
|
||||
+1
-1
@@ -19,7 +19,6 @@ export const VIDEO_CODECS = [
|
||||
'vp9',
|
||||
'av1',
|
||||
] as const;
|
||||
/** @public */
|
||||
export const PCM_CODECS = [
|
||||
'pcm-u8',
|
||||
'pcm-s8',
|
||||
@@ -32,6 +31,7 @@ export const PCM_CODECS = [
|
||||
'pcm-f32',
|
||||
'pcm-f32be',
|
||||
] as const;
|
||||
/** @public */
|
||||
export const AUDIO_CODECS = [
|
||||
'aac',
|
||||
'mp3',
|
||||
|
||||
@@ -520,8 +520,11 @@ export const stsd = (trackData: IsobmffTrackData) => {
|
||||
trackData,
|
||||
);
|
||||
} else if (trackData.type === 'audio') {
|
||||
const boxName = AUDIO_CODEC_TO_BOX_NAME[trackData.track.source._codec];
|
||||
assert(boxName);
|
||||
|
||||
sampleDescription = soundSampleDescription(
|
||||
AUDIO_CODEC_TO_BOX_NAME[trackData.track.source._codec],
|
||||
boxName,
|
||||
trackData,
|
||||
);
|
||||
} else if (trackData.type === 'subtitle') {
|
||||
@@ -690,7 +693,7 @@ export const soundSampleDescription = (
|
||||
u16(0), // Packet size
|
||||
u32(2 ** 16 * trackData.info.sampleRate), // Sample rate
|
||||
], [
|
||||
AUDIO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec](trackData),
|
||||
AUDIO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec]!(trackData),
|
||||
]);
|
||||
|
||||
/** MPEG-4 Elementary Stream Descriptor Box. */
|
||||
@@ -1086,12 +1089,14 @@ const VIDEO_CODEC_TO_CONFIGURATION_BOX: Record<VideoCodec, (trackData: IsobmffVi
|
||||
av1: av1C,
|
||||
};
|
||||
|
||||
const AUDIO_CODEC_TO_BOX_NAME: Record<AudioCodec, string> = {
|
||||
const AUDIO_CODEC_TO_BOX_NAME: Partial<Record<AudioCodec, string>> = {
|
||||
aac: 'mp4a',
|
||||
opus: 'Opus',
|
||||
};
|
||||
|
||||
const AUDIO_CODEC_TO_CONFIGURATION_BOX: Record<AudioCodec, (trackData: IsobmffAudioTrackData) => Box | null> = {
|
||||
const AUDIO_CODEC_TO_CONFIGURATION_BOX: Partial<
|
||||
Record<AudioCodec, (trackData: IsobmffAudioTrackData) => Box | null>
|
||||
> = {
|
||||
aac: esds,
|
||||
opus: dOps,
|
||||
};
|
||||
|
||||
@@ -101,7 +101,7 @@ type MatroskaVideoTrackData = MatroskaTrackData & { type: 'video' };
|
||||
type MatroskaAudioTrackData = MatroskaTrackData & { type: 'audio' };
|
||||
type MatroskaSubtitleTrackData = MatroskaTrackData & { type: 'subtitle' };
|
||||
|
||||
const CODEC_STRING_MAP: Record<VideoCodec | AudioCodec | SubtitleCodec, string> = {
|
||||
const CODEC_STRING_MAP: Partial<Record<VideoCodec | AudioCodec | SubtitleCodec, string>> = {
|
||||
avc: 'V_MPEG4/ISO/AVC',
|
||||
hevc: 'V_MPEGH/ISO/HEVC',
|
||||
vp8: 'V_VP8',
|
||||
@@ -109,6 +109,7 @@ const CODEC_STRING_MAP: Record<VideoCodec | AudioCodec | SubtitleCodec, string>
|
||||
av1: 'V_AV1',
|
||||
aac: 'A_AAC', // TODO is this even correct
|
||||
opus: 'A_OPUS',
|
||||
vorbis: 'A_VORBIS',
|
||||
webvtt: 'S_TEXT/WEBVTT',
|
||||
};
|
||||
|
||||
@@ -330,36 +331,6 @@ export class MatroskaMuxer extends Muxer {
|
||||
}
|
||||
}
|
||||
|
||||
override beforeTrackAdd(track: OutputTrack) {
|
||||
if (!(this.format instanceof WebMOutputFormat)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (track.type === 'video') {
|
||||
if (!['vp8', 'vp9', 'av1'].includes(track.source._codec)) {
|
||||
throw new Error(
|
||||
`WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.`,
|
||||
);
|
||||
}
|
||||
} else if (track.type === 'audio') {
|
||||
if (!['opus', 'vorbis'].includes(track.source._codec)) {
|
||||
throw new Error(
|
||||
`WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.`,
|
||||
);
|
||||
}
|
||||
} else if (track.type === 'subtitle') {
|
||||
if (track.source._codec !== 'webvtt') {
|
||||
throw new Error(
|
||||
`WebM only supports WebVTT as subtitle codec. Switching to MKV removes this restriction.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
'WebM only supports video, audio and subtitle tracks. Switching to MKV removes this restriction.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
@@ -434,11 +405,14 @@ export class MatroskaMuxer extends Muxer {
|
||||
this.tracksElement = tracksElement;
|
||||
|
||||
for (const trackData of this.trackDatas) {
|
||||
const codecId = CODEC_STRING_MAP[trackData.track.source._codec];
|
||||
assert(codecId);
|
||||
|
||||
tracksElement.data.push({ id: EBMLId.TrackEntry, data: [
|
||||
{ id: EBMLId.TrackNumber, data: trackData.track.id },
|
||||
{ id: EBMLId.TrackUID, data: trackData.track.id },
|
||||
{ id: EBMLId.TrackType, data: TRACK_TYPE_MAP[trackData.type] },
|
||||
{ id: EBMLId.CodecID, data: CODEC_STRING_MAP[trackData.track.source._codec] },
|
||||
{ id: EBMLId.CodecID, data: codecId },
|
||||
(trackData.type === 'video' ? this.videoSpecificTrackInfo(trackData) : null),
|
||||
(trackData.type === 'audio' ? this.audioSpecificTrackInfo(trackData) : null),
|
||||
(trackData.type === 'subtitle' ? this.subtitleSpecificTrackInfo(trackData) : null),
|
||||
|
||||
@@ -24,8 +24,6 @@ export abstract class Muxer {
|
||||
abstract addSubtitleCue(track: OutputSubtitleTrack, cue: SubtitleCue, meta?: SubtitleMetadata): Promise<void>;
|
||||
abstract finalize(): Promise<void>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
beforeTrackAdd(track: OutputTrack) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onTrackClose(track: OutputTrack) {}
|
||||
|
||||
|
||||
+84
-1
@@ -1,3 +1,4 @@
|
||||
import { AUDIO_CODECS, MediaCodec, SUBTITLE_CODECS, VIDEO_CODECS } from './codec';
|
||||
import { IsobmffMuxer } from './isobmff/isobmff-muxer';
|
||||
import { MatroskaMuxer } from './matroska/matroska-muxer';
|
||||
import { Muxer } from './muxer';
|
||||
@@ -7,6 +8,28 @@ import { Output } from './output';
|
||||
export abstract class OutputFormat {
|
||||
/** @internal */
|
||||
abstract _createMuxer(output: Output): Muxer;
|
||||
|
||||
/** @internal */
|
||||
abstract _getName(): string;
|
||||
abstract getSupportedCodecs(): MediaCodec[];
|
||||
|
||||
getSupportedVideoCodecs() {
|
||||
return this.getSupportedCodecs().filter(codec => (VIDEO_CODECS as readonly string[]).includes(codec));
|
||||
}
|
||||
|
||||
getSupportedAudioCodecs() {
|
||||
return this.getSupportedCodecs().filter(codec => (AUDIO_CODECS as readonly string[]).includes(codec));
|
||||
}
|
||||
|
||||
getSupportedSubtitleCodecs() {
|
||||
return this.getSupportedCodecs().filter(codec => (SUBTITLE_CODECS as readonly string[]).includes(codec));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_codecUnsupportedHint(codec: MediaCodec) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -36,6 +59,23 @@ export class Mp4OutputFormat extends OutputFormat {
|
||||
override _createMuxer(output: Output) {
|
||||
return new IsobmffMuxer(output, this);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_getName() {
|
||||
return 'MP4';
|
||||
}
|
||||
|
||||
getSupportedCodecs() {
|
||||
return Mp4OutputFormat.getSupportedCodecs();
|
||||
}
|
||||
|
||||
static getSupportedCodecs(): MediaCodec[] {
|
||||
return [
|
||||
'avc', 'hevc', 'vp8', 'vp9', 'av1',
|
||||
'aac', 'opus',
|
||||
'webvtt',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -65,10 +105,53 @@ export class MkvOutputFormat extends OutputFormat {
|
||||
override _createMuxer(output: Output) {
|
||||
return new MatroskaMuxer(output, this);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_getName() {
|
||||
return 'Matroska';
|
||||
}
|
||||
|
||||
getSupportedCodecs() {
|
||||
return MkvOutputFormat.getSupportedCodecs();
|
||||
}
|
||||
|
||||
static getSupportedCodecs(): MediaCodec[] {
|
||||
return [
|
||||
'avc', 'hevc', 'vp8', 'vp9', 'av1',
|
||||
'aac', 'opus', 'vorbis',
|
||||
'webvtt',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type WebMOutputFormatOptions = MkvOutputFormatOptions;
|
||||
|
||||
/** @public */
|
||||
export class WebMOutputFormat extends MkvOutputFormat {}
|
||||
export class WebMOutputFormat extends MkvOutputFormat {
|
||||
override getSupportedCodecs() {
|
||||
return WebMOutputFormat.getSupportedCodecs();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override _getName() {
|
||||
return 'WebM';
|
||||
}
|
||||
|
||||
static override getSupportedCodecs(): MediaCodec[] {
|
||||
return [
|
||||
'vp8', 'vp9', 'av1',
|
||||
'opus', 'vorbis',
|
||||
'webvtt',
|
||||
];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override _codecUnsupportedHint(codec: MediaCodec) {
|
||||
if (MkvOutputFormat.getSupportedCodecs().includes(codec)) {
|
||||
return ' Switching to MKV will grant support for this codec.';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-1
@@ -44,6 +44,10 @@ export type SubtitleTrackMetadata = {};
|
||||
|
||||
/** @public */
|
||||
export class Output {
|
||||
/** @internal */
|
||||
_format: OutputFormat;
|
||||
/** @internal */
|
||||
_target: Target;
|
||||
/** @internal */
|
||||
_muxer: Muxer;
|
||||
/** @internal */
|
||||
@@ -75,6 +79,9 @@ export class Output {
|
||||
}
|
||||
options.target._output = this;
|
||||
|
||||
this._format = options.format;
|
||||
this._target = options.target;
|
||||
|
||||
this._writer = options.target._createWriter();
|
||||
this._muxer = options.format._createMuxer(this);
|
||||
}
|
||||
@@ -145,7 +152,52 @@ export class Output {
|
||||
metadata,
|
||||
} as OutputTrack;
|
||||
|
||||
this._muxer.beforeTrackAdd(track);
|
||||
if (track.type === 'video') {
|
||||
const supportedVideoCodecs = this._format.getSupportedVideoCodecs();
|
||||
|
||||
if (supportedVideoCodecs.length === 0) {
|
||||
throw new Error(
|
||||
`${this._format._getName()} does not support video tracks.`
|
||||
+ this._format._codecUnsupportedHint(track.source._codec),
|
||||
);
|
||||
} else if (!supportedVideoCodecs.includes(track.source._codec)) {
|
||||
throw new Error(
|
||||
`Codec '${track.source._codec}' cannot be contained within ${this._format._getName()}. Supported`
|
||||
+ ` video codecs are: ${supportedVideoCodecs.map(codec => `'${codec}'`).join(', ')}.`
|
||||
+ this._format._codecUnsupportedHint(track.source._codec),
|
||||
);
|
||||
}
|
||||
} else if (track.type === 'audio') {
|
||||
const supportedAudioCodecs = this._format.getSupportedAudioCodecs();
|
||||
|
||||
if (supportedAudioCodecs.length === 0) {
|
||||
throw new Error(
|
||||
`${this._format._getName()} does not support audio tracks.`
|
||||
+ this._format._codecUnsupportedHint(track.source._codec),
|
||||
);
|
||||
} else if (!supportedAudioCodecs.includes(track.source._codec)) {
|
||||
throw new Error(
|
||||
`Codec '${track.source._codec}' cannot be contained within ${this._format._getName()}. Supported`
|
||||
+ ` audio codecs are: ${supportedAudioCodecs.map(codec => `'${codec}'`).join(', ')}.`
|
||||
+ this._format._codecUnsupportedHint(track.source._codec),
|
||||
);
|
||||
}
|
||||
} else if (track.type === 'subtitle') {
|
||||
const supportedSubtitleCodecs = this._format.getSupportedSubtitleCodecs();
|
||||
|
||||
if (supportedSubtitleCodecs.length === 0) {
|
||||
throw new Error(
|
||||
`${this._format._getName()} does not support subtitle tracks.`
|
||||
+ this._format._codecUnsupportedHint(track.source._codec),
|
||||
);
|
||||
} else if (!supportedSubtitleCodecs.includes(track.source._codec)) {
|
||||
throw new Error(
|
||||
`Codec '${track.source._codec}' cannot be contained within ${this._format._getName()}. Supported`
|
||||
+ ` subtitle codecs are: ${supportedSubtitleCodecs.map(codec => `'${codec}'`).join(', ')}.`
|
||||
+ this._format._codecUnsupportedHint(track.source._codec),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this._tracks.push(track);
|
||||
source._connectedTrack = track;
|
||||
|
||||
Reference in New Issue
Block a user