From d323c2be8e99b52ef8be71078cd4da9445d6594e Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:26:12 +0100 Subject: [PATCH] Add track count limit checking --- dev/demux.html | 25 +++++++++++++++++-- src/index.ts | 4 +++ src/output-format.ts | 41 +++++++++++++++++++++++++++++-- src/output.ts | 58 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 4 deletions(-) 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();