diff --git a/dev/index.html b/dev/index.html
index 69808ee..dde4b8c 100644
--- a/dev/index.html
+++ b/dev/index.html
@@ -86,7 +86,7 @@
bitrate: 1e6
});
let audioSource = new Metamuxer.AudioBufferSource({
- codec: 'opus',
+ codec: 'flac',
bitrate: 128e3,
});
let subtitleSource = new Metamuxer.TextSubtitleSource('webvtt');
diff --git a/src/codec.ts b/src/codec.ts
index 3aa1171..367e2f5 100644
--- a/src/codec.ts
+++ b/src/codec.ts
@@ -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',
diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts
index e6258be..49f3cbe 100644
--- a/src/isobmff/isobmff-boxes.ts
+++ b/src/isobmff/isobmff-boxes.ts
@@ -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 = {
+const AUDIO_CODEC_TO_BOX_NAME: Partial> = {
aac: 'mp4a',
opus: 'Opus',
};
-const AUDIO_CODEC_TO_CONFIGURATION_BOX: Record Box | null> = {
+const AUDIO_CODEC_TO_CONFIGURATION_BOX: Partial<
+ Record Box | null>
+> = {
aac: esds,
opus: dOps,
};
diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts
index 83316f4..0cd8740 100644
--- a/src/matroska/matroska-muxer.ts
+++ b/src/matroska/matroska-muxer.ts
@@ -101,7 +101,7 @@ type MatroskaVideoTrackData = MatroskaTrackData & { type: 'video' };
type MatroskaAudioTrackData = MatroskaTrackData & { type: 'audio' };
type MatroskaSubtitleTrackData = MatroskaTrackData & { type: 'subtitle' };
-const CODEC_STRING_MAP: Record = {
+const CODEC_STRING_MAP: Partial> = {
avc: 'V_MPEG4/ISO/AVC',
hevc: 'V_MPEGH/ISO/HEVC',
vp8: 'V_VP8',
@@ -109,6 +109,7 @@ const CODEC_STRING_MAP: Record
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),
diff --git a/src/muxer.ts b/src/muxer.ts
index 393b86f..4c940d3 100644
--- a/src/muxer.ts
+++ b/src/muxer.ts
@@ -24,8 +24,6 @@ export abstract class Muxer {
abstract addSubtitleCue(track: OutputSubtitleTrack, cue: SubtitleCue, meta?: SubtitleMetadata): Promise;
abstract finalize(): Promise;
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- beforeTrackAdd(track: OutputTrack) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onTrackClose(track: OutputTrack) {}
diff --git a/src/output-format.ts b/src/output-format.ts
index efe9da6..68eeccd 100644
--- a/src/output-format.ts
+++ b/src/output-format.ts
@@ -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 '';
+ }
+ }
+}
diff --git a/src/output.ts b/src/output.ts
index 64fadef..2458332 100644
--- a/src/output.ts
+++ b/src/output.ts
@@ -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;