diff --git a/dev/demux.html b/dev/demux.html
index 7d7796e..8057547 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -32,7 +32,7 @@
fileInput.addEventListener('change', async () => {
const file = fileInput.files[0];
- const source = new Metamuxer.BlobSource(file);
+ const source = new Metamuxer.BufferSource(await file.arrayBuffer()) ?? new Metamuxer.BlobSource(file);
const start = performance.now();
const input = new Metamuxer.Input({
@@ -40,7 +40,27 @@
source
});
- const videoTrack = await input.getPrimaryVideoTrack();
+ const output = new Metamuxer.Output({
+ format: new Metamuxer.WaveOutputFormat(),
+ target: new Metamuxer.BufferTarget()
+ });
+ output.start();
+
+ /*
+ const audioTrack = await input.getPrimaryAudioTrack();
+ const sink = new Metamuxer.EncodedAudioSampleSink(audioTrack);
+
+ let total = 0;
+ for await (const sample of sink.samples(undefined, undefined, { metadataOnly: true })) {
+ total++;
+ if (performance.now() > 10000) {
+ break;
+ }
+ }
+ console.log("Done", total);
+ */
+
+ /*
const sink = new Metamuxer.VideoFrameSink(videoTrack);
console.log(await videoTrack.getLanguageCode());
@@ -49,6 +69,7 @@
console.log(thing)
}
console.log("Done")
+ */
/*
//const videoTrack = await input.getPrimaryVideoTrack();
diff --git a/src/index.ts b/src/index.ts
index 1e4c1c4..40b82e5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,6 +5,8 @@ export {
VideoTrackMetadata,
AudioTrackMetadata,
SubtitleTrackMetadata,
+ TrackType,
+ ALL_TRACK_TYPES,
} from './output';
export {
OutputFormat,
@@ -17,6 +19,8 @@ export {
WebMOutputFormat,
WebMOutputFormatOptions,
WaveOutputFormat,
+ TrackCountLimits,
+ InclusiveRange,
} from './output-format';
export {
VideoEncodingConfig,
diff --git a/src/output-format.ts b/src/output-format.ts
index bbed1f4..0a426b9 100644
--- a/src/output-format.ts
+++ b/src/output-format.ts
@@ -2,18 +2,28 @@ import { AUDIO_CODECS, MediaCodec, PCM_CODECS, SUBTITLE_CODECS, VIDEO_CODECS } f
import { IsobmffMuxer } from './isobmff/isobmff-muxer';
import { MatroskaMuxer } from './matroska/matroska-muxer';
import { Muxer } from './muxer';
-import { Output } from './output';
+import { Output, TrackType } from './output';
import { WaveMuxer } from './wave/wave-muxer';
+/** @public */
+export type InclusiveRange = { min: number; max: number };
+/** @public */
+export type TrackCountLimits = {
+ [K in TrackType]: InclusiveRange;
+} & {
+ total: InclusiveRange;
+};
+
/** @public */
export abstract class OutputFormat {
/** @internal */
abstract _createMuxer(output: Output): Muxer;
-
/** @internal */
abstract _getName(): string;
+
abstract getFileExtension(): string;
abstract getSupportedCodecs(): MediaCodec[];
+ abstract getSupportedTrackCounts(): TrackCountLimits;
getSupportedVideoCodecs() {
return this.getSupportedCodecs().filter(codec => (VIDEO_CODECS as readonly string[]).includes(codec));
@@ -57,6 +67,15 @@ export abstract class IsobmffOutputFormat extends OutputFormat {
this._options = options;
}
+ getSupportedTrackCounts(): TrackCountLimits {
+ return {
+ video: { min: 0, max: Infinity },
+ audio: { min: 0, max: Infinity },
+ subtitle: { min: 0, max: Infinity },
+ total: { min: 1, max: 2 ** 32 - 1 }, // Have fun reaching this one
+ };
+ }
+
/** @internal */
_createMuxer(output: Output) {
return new IsobmffMuxer(output, this);
@@ -161,6 +180,15 @@ export class MkvOutputFormat extends OutputFormat {
return 'Matroska';
}
+ getSupportedTrackCounts(): TrackCountLimits {
+ return {
+ video: { min: 0, max: Infinity },
+ audio: { min: 0, max: Infinity },
+ subtitle: { min: 0, max: Infinity },
+ total: { min: 1, max: 127 },
+ };
+ }
+
getFileExtension() {
return '.mkv';
}
@@ -228,6 +256,15 @@ export class WaveOutputFormat extends OutputFormat {
return 'WAVE';
}
+ getSupportedTrackCounts(): TrackCountLimits {
+ return {
+ video: { min: 0, max: 0 },
+ audio: { min: 1, max: 1 },
+ subtitle: { min: 0, max: 0 },
+ total: { min: 1, max: 1 },
+ };
+ }
+
getFileExtension() {
return '.wav';
}
diff --git a/src/output.ts b/src/output.ts
index dd648df..7e31b57 100644
--- a/src/output.ts
+++ b/src/output.ts
@@ -11,9 +11,15 @@ export type OutputOptions = {
target: Target;
};
+/** @public */
+export const ALL_TRACK_TYPES = ['video', 'audio', 'subtitle'] as const;
+/** @public */
+export type TrackType = typeof ALL_TRACK_TYPES[number];
+
export type OutputTrack = {
id: number;
output: Output;
+ type: TrackType;
} & ({
type: 'video';
source: VideoSource;
@@ -152,6 +158,29 @@ export class Output {
throw new Error('Source is already used for a track.');
}
+ // Verify maximum track count constraints
+ const supportedTrackCounts = this._format.getSupportedTrackCounts();
+ const presentTracksOfThisType = this._tracks.reduce(
+ (count, track) => count + (track.type === type ? 1 : 0),
+ 0,
+ );
+ const maxCount = supportedTrackCounts[type].max;
+ if (presentTracksOfThisType === maxCount) {
+ throw new Error(
+ maxCount === 0
+ ? `${this._format._getName()} does not support ${type} tracks.`
+ : (`${this._format._getName()} does not support more than ${maxCount} ${type} track`
+ + `${maxCount === 1 ? '' : 's'}.`),
+ );
+ }
+ const maxTotalCount = supportedTrackCounts.total.max;
+ if (this._tracks.length === maxTotalCount) {
+ throw new Error(
+ `${this._format._getName()} does not support more than ${maxTotalCount} tracks`
+ + `${maxTotalCount === 1 ? '' : 's'} in total.`,
+ );
+ }
+
const track = {
id: this._tracks.length + 1,
output: this,
@@ -219,6 +248,35 @@ export class Output {
throw new Error('Output already started.');
}
+ // Verify minimum track count constraints
+ const supportedTrackCounts = this._format.getSupportedTrackCounts();
+ for (const trackType of ALL_TRACK_TYPES) {
+ const presentTracksOfThisType = this._tracks.reduce(
+ (count, track) => count + (track.type === trackType ? 1 : 0),
+ 0,
+ );
+ const minCount = supportedTrackCounts[trackType].min;
+ if (presentTracksOfThisType < minCount) {
+ throw new Error(
+ minCount === supportedTrackCounts[trackType].max
+ ? (`${this._format._getName()} requires exactly ${minCount} ${trackType}`
+ + ` track${minCount === 1 ? '' : 's'}.`)
+ : (`${this._format._getName()} requires at least ${minCount} ${trackType}`
+ + ` track${minCount === 1 ? '' : 's'}.`),
+ );
+ }
+ }
+ const totalMinCount = supportedTrackCounts.total.min;
+ if (this._tracks.length < totalMinCount) {
+ throw new Error(
+ totalMinCount === supportedTrackCounts.total.max
+ ? (`${this._format._getName()} requires exactly ${totalMinCount} track`
+ + `${totalMinCount === 1 ? '' : 's'}.`)
+ : (`${this._format._getName()} requires at least ${totalMinCount} track`
+ + `${totalMinCount === 1 ? '' : 's'}.`),
+ );
+ }
+
this._started = true;
this._writer.start();