diff --git a/docs/guide/converting-media-files.md b/docs/guide/converting-media-files.md index ade6f9c..7c92198 100644 --- a/docs/guide/converting-media-files.md +++ b/docs/guide/converting-media-files.md @@ -306,8 +306,8 @@ const conversion = await Conversion.init({ output, // Function gets invoked for each video track: - video: (videoTrack, n) => { - if (n > 1) { + video: (videoTrack) => { + if (videoTrack.number > 1) { // Keep only the first video track return { discard: true }; } @@ -319,7 +319,7 @@ const conversion = await Conversion.init({ }, // Async functions work too: - audio: async (audioTrack, n) => { + audio: async (audioTrack) => { if (audioTrack.languageCode !== 'rus') { // Keep only Russian audio tracks return { discard: true }; diff --git a/docs/guide/quick-start.md b/docs/guide/quick-start.md index 3360687..3759661 100644 --- a/docs/guide/quick-start.md +++ b/docs/guide/quick-start.md @@ -556,14 +556,16 @@ const output = new Output(...); const conversion = await Conversion.init({ input, output, - video: { + video: track => ({ width: 480, bitrate: QUALITY_LOW, - }, - audio: { + discard: track.number > 1, // Keep only the first video track + }), + audio: track => ({ numberOfChannels: 1, bitrate: QUALITY_LOW, - }, + discard: track.number > 1, // Keep only the first audio track + }), trim: { // Let's keep only the first 60 seconds start: 0, diff --git a/docs/guide/reading-media-files.md b/docs/guide/reading-media-files.md index 36b44e2..6e3faab 100644 --- a/docs/guide/reading-media-files.md +++ b/docs/guide/reading-media-files.md @@ -98,6 +98,10 @@ Once you have an `InputTrack`, you can start extracting metadata from it. // Get a unique ID for this track in the input file: track.id; // => number +// Get the 1-based index of this track among all tracks of the same type +// (e.g., first video track is 1, second video track is 2, etc.): +track.number; // => number + // Check the track's type: track.type; // => 'video' | 'audio' | 'subtitle'; diff --git a/examples/file-compression/file-compression.ts b/examples/file-compression/file-compression.ts index 0992dca..975dfa6 100644 --- a/examples/file-compression/file-compression.ts +++ b/examples/file-compression/file-compression.ts @@ -62,13 +62,15 @@ const compressFile = async (resource: File | string) => { currentConversion = await Conversion.init({ input, output, - video: { + video: track => ({ width: 320, // Height will be deduced automatically to retain aspect ratio bitrate: QUALITY_VERY_LOW, - }, - audio: { + discard: track.number > 1, // Keep only the first video track + }), + audio: track => ({ bitrate: 32e3, - }, + discard: track.number > 1, // Keep only the first audio track + }), }); // Keep track of progress diff --git a/src/adts/adts-demuxer.ts b/src/adts/adts-demuxer.ts index 8d4985a..aaade92 100644 --- a/src/adts/adts-demuxer.ts +++ b/src/adts/adts-demuxer.ts @@ -146,6 +146,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking { return 1; } + getNumber() { + return 1; + } + async getFirstTimestamp() { return 0; } diff --git a/src/conversion.ts b/src/conversion.ts index a303c58..76a70c2 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -66,7 +66,8 @@ export type ConversionOptions = { * Video-specific options. When passing an object, the same options are applied to all video tracks. When passing a * function, it will be invoked for each video track and is expected to return or resolve to the options * for that specific track. The function is passed an instance of {@link InputVideoTrack} as well as a number `n`, - * which is the 1-based index of the track in the list of all video tracks. + * which is the 1-based index of the track in the list of all video tracks. Using `n` is deprecated, prefer the + * identical `track.number` instead. */ video?: ConversionVideoOptions | ((track: InputVideoTrack, n: number) => MaybePromise); @@ -75,7 +76,8 @@ export type ConversionOptions = { * Audio-specific options. When passing an object, the same options are applied to all audio tracks. When passing a * function, it will be invoked for each audio track and is expected to return or resolve to the options * for that specific track. The function is passed an instance of {@link InputAudioTrack} as well as a number `n`, - * which is the 1-based index of the track in the list of all audio tracks. + * which is the 1-based index of the track in the list of all audio tracks. Using `n` is deprecated, prefer the + * identical `track.number` instead. */ audio?: ConversionAudioOptions | ((track: InputAudioTrack, n: number) => MaybePromise); diff --git a/src/flac/flac-demuxer.ts b/src/flac/flac-demuxer.ts index 027eb8e..4547c4b 100644 --- a/src/flac/flac-demuxer.ts +++ b/src/flac/flac-demuxer.ts @@ -529,6 +529,10 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking { return 1; } + getNumber() { + return 1; + } + getCodec() { return 'flac' as const; } diff --git a/src/input-track.ts b/src/input-track.ts index 0b20858..89689fb 100644 --- a/src/input-track.ts +++ b/src/input-track.ts @@ -32,6 +32,7 @@ export type PacketStats = { export interface InputTrackBacking { getId(): number; + getNumber(): number; getCodec(): MediaCodec | null; getInternalCodecId(): string | number | Uint8Array | null; getName(): string | null; @@ -94,6 +95,15 @@ export abstract class InputTrack { return this._backing.getId(); } + /** + * The 1-based index of this track among all tracks of the same type in the input file. For example, the first + * video track has number 1, the second video track has number 2, and so on. The index refers to the order in + * which the tracks are returned by {@link Input.getTracks()}. + */ + get number() { + return this._backing.getNumber(); + } + /** * The identifier of the codec used internally by the container. It is not homogenized by Mediabunny * and depends entirely on the container format. diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index 4554c8d..951b728 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -2335,6 +2335,25 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { return this.internalTrack.id; } + getNumber() { + const demuxer = this.internalTrack.demuxer; + const inputTrack = this.internalTrack.inputTrack!; + const trackType = inputTrack.type; + + let number = 0; + for (const track of demuxer.tracks) { + if (track.inputTrack!.type === trackType) { + number++; + } + + if (track === this.internalTrack) { + break; + } + } + + return number; + } + getCodec(): MediaCodec | null { throw new Error('Not implemented on base class.'); } diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index 69b40df..f9d8a39 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -1859,6 +1859,27 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { return this.internalTrack.id; } + getNumber() { + const demuxer = this.internalTrack.demuxer; + const inputTrack = this.internalTrack.inputTrack!; + const trackType = inputTrack.type; + + let number = 0; + for (const segment of demuxer.segments) { + for (const track of segment.tracks) { + if (track.inputTrack!.type === trackType) { + number++; + } + + if (track === this.internalTrack) { + break; + } + } + } + + return number; + } + getCodec(): MediaCodec | null { throw new Error('Not implemented on base class.'); } diff --git a/src/mp3/mp3-demuxer.ts b/src/mp3/mp3-demuxer.ts index 0295978..12ada99 100644 --- a/src/mp3/mp3-demuxer.ts +++ b/src/mp3/mp3-demuxer.ts @@ -217,6 +217,10 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking { return 1; } + getNumber() { + return 1; + } + async getFirstTimestamp() { return 0; } diff --git a/src/mpeg-ts/mpeg-ts-demuxer.ts b/src/mpeg-ts/mpeg-ts-demuxer.ts index dc2a25c..7ae49c5 100644 --- a/src/mpeg-ts/mpeg-ts-demuxer.ts +++ b/src/mpeg-ts/mpeg-ts-demuxer.ts @@ -746,6 +746,25 @@ export abstract class MpegTsTrackBacking implements InputTrackBacking { return this.elementaryStream.pid; } + getNumber() { + const demuxer = this.elementaryStream.demuxer; + const trackType = this.elementaryStream.info.type; + + let number = 0; + for (const track of demuxer.tracks) { + if (track.type === trackType) { + number++; + } + + assert(track._backing instanceof MpegTsTrackBacking); + if (track._backing.elementaryStream === this.elementaryStream) { + break; + } + } + + return number; + } + getCodec(): MediaCodec | null { throw new Error('Not implemented on base class.'); } diff --git a/src/ogg/ogg-demuxer.ts b/src/ogg/ogg-demuxer.ts index 2dec7de..fa364d4 100644 --- a/src/ogg/ogg-demuxer.ts +++ b/src/ogg/ogg-demuxer.ts @@ -428,6 +428,16 @@ class OggAudioTrackBacking implements InputAudioTrackBacking { return this.bitstream.serialNumber; } + getNumber() { + // All Ogg tracks are audio, so the track's index + 1 is its number + const index = this.demuxer.tracks.findIndex( + t => (t._backing as OggAudioTrackBacking).bitstream === this.bitstream, + ); + assert(index !== -1); + + return index + 1; + } + getNumberOfChannels() { return this.bitstream.numberOfChannels; } diff --git a/src/wave/wave-demuxer.ts b/src/wave/wave-demuxer.ts index 5f0cc5e..8623963 100644 --- a/src/wave/wave-demuxer.ts +++ b/src/wave/wave-demuxer.ts @@ -356,6 +356,10 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking { return 1; } + getNumber() { + return 1; + } + getCodec() { return this.demuxer.getCodec(); }