From 051578c2ca0a833106d6070e025ce4892c9cd717 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:23:17 +0200 Subject: [PATCH] Remove input track descriptors, add async getters on tracks for everything, deprecate old sync getters --- README.md | 7 +- dev/demux.html | 18 +- docs/guide/converting-media-files.md | 6 +- docs/guide/quick-start.md | 10 +- docs/guide/reading-media-files.md | 30 +- docs/index.md | 7 +- eslint.config.mjs | 1 + examples/media-player/media-player.ts | 12 +- .../metadata-extraction.ts | 20 +- .../thumbnail-generation.ts | 12 +- src/conversion.ts | 34 +- src/hls/hls-demuxer.ts | 177 ++-- src/index.ts | 9 +- src/input-track-descriptor.ts | 530 ------------ src/input-track.ts | 755 ++++++++++++++---- src/input.ts | 215 +---- src/media-sink.ts | 119 +-- src/media-source.ts | 5 + src/misc.ts | 2 + src/segmented-input.ts | 62 +- src/source.ts | 1 + src/target.ts | 2 + test/browser/adts-demuxing.test.ts | 6 +- test/browser/adts-muxing.test.ts | 2 +- test/browser/conversion.test.ts | 20 +- test/browser/flac.test.ts | 6 +- test/browser/media-sources.test.ts | 44 +- test/browser/mpeg-ts-muxing.test.ts | 38 +- test/browser/par.test.ts | 74 +- test/browser/transparency.test.ts | 2 +- test/node/aac-encoder-extension.test.ts | 6 +- test/node/ac3.test.ts | 52 +- test/node/adts-muxer.test.ts | 6 +- test/node/annex-b-conversion.test.ts | 4 +- test/node/disposition.test.ts | 4 +- test/node/flac-encoder-extension.test.ts | 6 +- test/node/flac.test.ts | 8 +- test/node/hls-input.test.ts | 516 ++++++------ test/node/isobmff-muxer.test.ts | 6 +- test/node/matroska-muxer.test.ts | 6 +- test/node/mp3-encoder-extension.test.ts | 6 +- test/node/mpeg-ts-demuxing.test.ts | 44 +- 42 files changed, 1386 insertions(+), 1504 deletions(-) delete mode 100644 src/input-track-descriptor.ts diff --git a/README.md b/README.md index e97c50f..c257932 100644 --- a/README.md +++ b/README.md @@ -119,8 +119,11 @@ const input = new Input({ const duration = await input.computeDuration(); // in seconds const videoTrack = await input.getPrimaryVideoTrack(); const audioTrack = await input.getPrimaryAudioTrack(); -const { displayWidth, displayHeight, rotation } = videoTrack; -const { sampleRate, numberOfChannels } = audioTrack; +const displayWidth = await videoTrack.getDisplayWidth(); +const displayHeight = await videoTrack.getDisplayHeight(); +const { rotation } = videoTrack; +const sampleRate = await audioTrack.getSampleRate(); +const numberOfChannels = await audioTrack.getNumberOfChannels(); const { title, artist, album } = await input.getMetadataTags(); ``` diff --git a/dev/demux.html b/dev/demux.html index 7aaf58a..30cff7b 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -28,13 +28,19 @@ return; */ - const manifest = new Mediabunny.Input({ - entryPath: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', - source: ({ path }) => new Mediabunny.UrlSource(path), - formats: Mediabunny.ALL_FORMATS, - }); + const manifest = Mediabunny.createInputFrom( + 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', + Mediabunny.ALL_FORMATS, + ); - const audioTrack = await manifest.getPrimaryAudioTrack(); + const [track] = await manifest.getTracks(); + console.log(track); + + window.kekw = () => { + console.log(track.getFirstTimestamp()) + }; + + return; const ugh = new Mediabunny.EncodedPacketSink(audioTrack); for await (const packet of ugh.packets()) { diff --git a/docs/guide/converting-media-files.md b/docs/guide/converting-media-files.md index 9d5ecef..5f5e24b 100644 --- a/docs/guide/converting-media-files.md +++ b/docs/guide/converting-media-files.md @@ -306,7 +306,7 @@ const conversion = await Conversion.init({ output, // Function gets invoked for each video track: - video: (videoTrack) => { + video: async (videoTrack) => { if (videoTrack.number > 1) { // Keep only the first video track return { discard: true }; @@ -314,13 +314,13 @@ const conversion = await Conversion.init({ return { // Shrink width to 640 only if the track is wider - width: Math.min(videoTrack.displayWidth, 640), + width: Math.min(await videoTrack.getDisplayWidth(), 640), }; }, // Async functions work too: audio: async (audioTrack) => { - if (audioTrack.languageCode !== 'rus') { + if (await audioTrack.getLanguageCode() !== 'rus') { // Keep only Russian audio tracks return { discard: true }; } diff --git a/docs/guide/quick-start.md b/docs/guide/quick-start.md index 3759661..a38eb38 100644 --- a/docs/guide/quick-start.md +++ b/docs/guide/quick-start.md @@ -18,9 +18,9 @@ const allTracks = await input.getTracks(); // List of all tracks // Extract video metadata const videoTrack = await input.getPrimaryVideoTrack(); if (videoTrack) { - videoTrack.displayWidth; // in pixels - videoTrack.displayHeight; // in pixels - videoTrack.rotation; // in degrees clockwise + await videoTrack.getDisplayWidth(); // in pixels + await videoTrack.getDisplayHeight(); // in pixels + await videoTrack.getRotation(); // in degrees clockwise // Estimate frame rate (FPS) const packetStats = await videoTrack.computePacketStats(100); @@ -30,8 +30,8 @@ if (videoTrack) { // Extract audio metadata const audioTrack = await input.getPrimaryAudioTrack(); if (audioTrack) { - audioTrack.numberOfChannels; - audioTrack.sampleRate; // in Hz + await audioTrack.getNumberOfChannels(); + await audioTrack.getSampleRate(); // in Hz } // Extract metadata tags diff --git a/docs/guide/reading-media-files.md b/docs/guide/reading-media-files.md index 8193df6..3b370e2 100644 --- a/docs/guide/reading-media-files.md +++ b/docs/guide/reading-media-files.md @@ -111,21 +111,21 @@ track.isAudioTrack(); // => boolean // Retrieve the track's language as an ISO 639-2/T language code. // Resolves to 'und' (undetermined) if the language isn't known. -track.languageCode; // => string +await track.getLanguageCode(); // => string // A user-defined name for this track. -track.name; // => string +await track.getName(); // => string | null // Information about the intended usage of the track // (default, commentary, hearing-impaired, visually-impaired, etc.) -track.disposition; // TrackDisposition +await track.getDisposition(); // TrackDisposition ``` #### Codec information You can query metadata related to the track's codec: ```ts -track.codec; // => MediaCodec | null +await track.getCodec(); // => MediaCodec | null ``` This field is `null` when the track's codec couldn't be recognized or is not supported by Mediabunny. See [Codecs](./supported-formats-and-codecs#codecs) for the full list of supported codecs. When Mediabunny doesn't recognize the format, you can still use the `internalCodecId` field to figure out the codec of the track, although its format depends on the container format used and is not homogenized by Mediabunny. @@ -167,7 +167,7 @@ A _negative start timestamp_ means the track begins *before* the composition doe Another metric related to track timing info is its *time resolution*, which is given in hertz: ```ts -track.timeResolution; // => 24 +await track.getTimeResolution(); // => 24 ``` Intuitively, this is the maximum possible "frame rate" of the track (assuming that no two samples have the same timestamp). Mathematically, if $x$ is equal to a track's time resolution, then all timestamps and durations of that track can be expressed as: @@ -217,25 +217,25 @@ In addition to the [common track metadata](#common-track-metadata), video tracks ```ts // Get the raw pixel dimensions of the track's coded samples: -videoTrack.codedWidth; // => number -videoTrack.codedHeight; // => number +await videoTrack.getCodedWidth(); // => number +await videoTrack.getCodedHeight(); // => number // Get the pixel dimensions of the track after aspect ratio adjustments, // but before rotation: -videoTrack.squarePixelWidth; // => number -videoTrack.squarePixelHeight; // => number +await videoTrack.getSquarePixelWidth(); // => number +await videoTrack.getSquarePixelHeight(); // => number // Get the displayed pixel dimensions of the track's samples, after // aspect ratio adjustments and rotation: -videoTrack.displayWidth; // => number -videoTrack.displayHeight; // => number +await videoTrack.getDisplayWidth(); // => number +await videoTrack.getDisplayHeight(); // => number // Get the clockwise rotation in degrees by which the // track's frames should be rotated: -videoTrack.rotation; // => 0 | 90 | 180 | 270 +await videoTrack.getRotation(); // => 0 | 90 | 180 | 270 // Get the aspect ratio of the track's pixels (usually 1:1): -videoTrack.pixelAspectRatio; // => { num: number, den: number } +await videoTrack.getPixelAspectRatio(); // => { num: number, den: number } ``` To compute a video track's average frame rate (FPS), use [`computePacketStats`](#packet-statistics): @@ -284,10 +284,10 @@ In addition to the [common track metadata](#common-track-metadata), audio tracks ```ts // Get the number of audio channels: -audioTrack.numberOfChannels; // => number +await audioTrack.getNumberOfChannels(); // => number // Get the audio sample rate in hertz: -audioTrack.sampleRate; // => number +await audioTrack.getSampleRate(); // => number ``` You can retrieve the track's decoder configuration, which is an `AudioDecoderConfig` from the WebCodecs API for usage within `AudioDecoder`: diff --git a/docs/index.md b/docs/index.md index 7c50daa..4db63c5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -189,10 +189,13 @@ const input = new Input({ const duration = await input.computeDuration(); const videoTrack = await input.getPrimaryVideoTrack(); -const { displayWidth, displayHeight, rotation } = videoTrack; +const displayWidth = await videoTrack.getDisplayWidth(); +const displayHeight = await videoTrack.getDisplayHeight(); +const rotation = await videoTrack.getRotation(); const audioTrack = await input.getPrimaryAudioTrack(); -const { sampleRate, numberOfChannels } = audioTrack; +const sampleRate = await audioTrack.getSampleRate(); +const numberOfChannels = await audioTrack.getNumberOfChannels(); // Get the frame halfway through the video const sink = new VideoSampleSink(videoTrack); diff --git a/eslint.config.mjs b/eslint.config.mjs index dcc3101..1e3a0f2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -31,6 +31,7 @@ export default tseslint.config( '@stylistic/yield-star-spacing': ['error', { before: false, after: true }], '@typescript-eslint/no-unsafe-enum-comparison': 'off', '@typescript-eslint/no-unsafe-unary-minus': 'off', + '@typescript-eslint/no-deprecated': 'error', }, }, { diff --git a/examples/media-player/media-player.ts b/examples/media-player/media-player.ts index 1e1169a..b4116fa 100644 --- a/examples/media-player/media-player.ts +++ b/examples/media-player/media-player.ts @@ -109,7 +109,7 @@ const initMediaPlayer = async (resource: File | string) => { ); endTimestamp = await input.getDurationFromMetadata(tracks, { skipLiveWait: true }) ?? await input.computeDuration(tracks, { skipLiveWait: true }); - isRelativeToUnixEpoch = tracks.some(t => t?.isRelativeToUnixEpoch); + isRelativeToUnixEpoch = (await Promise.all(tracks.map(t => t.getIsRelativeToUnixEpoch()))).some(Boolean); playbackTimeAtStart = firstTimestamp; // Configure the time display elements accordingly @@ -127,7 +127,7 @@ const initMediaPlayer = async (resource: File | string) => { let problemMessage = ''; if (videoTrack) { - if (videoTrack.codec === null) { + if (await videoTrack.getCodec() === null) { problemMessage += 'Unsupported video codec. '; videoTrack = null; } else if (!(await videoTrack.canDecode())) { @@ -137,7 +137,7 @@ const initMediaPlayer = async (resource: File | string) => { } if (audioTrack) { - if (audioTrack.codec === null) { + if (await audioTrack.getCodec() === null) { problemMessage += 'Unsupported audio codec. '; audioTrack = null; } else if (!(await audioTrack.canDecode())) { @@ -163,7 +163,7 @@ const initMediaPlayer = async (resource: File | string) => { // We must create the audio context with the matching sample rate for correct acoustic results // (especially for low-sample rate files) - audioContext = new AudioContext({ sampleRate: audioTrack?.sampleRate }); + audioContext = new AudioContext({ sampleRate: await audioTrack?.getSampleRate() }); gainNode = audioContext.createGain(); gainNode.connect(audioContext.destination); updateVolume(); @@ -187,8 +187,8 @@ const initMediaPlayer = async (resource: File | string) => { // Show the canvas if there's a video track, otherwise hide it if (videoTrack) { canvas.style.display = ''; - canvas.width = videoTrack.displayWidth; - canvas.height = videoTrack.displayHeight; + canvas.width = await videoTrack.getDisplayWidth(); + canvas.height = await videoTrack.getDisplayHeight(); } else { canvas.style.display = 'none'; } diff --git a/examples/metadata-extraction/metadata-extraction.ts b/examples/metadata-extraction/metadata-extraction.ts index af2acd1..c59e138 100644 --- a/examples/metadata-extraction/metadata-extraction.ts +++ b/examples/metadata-extraction/metadata-extraction.ts @@ -56,25 +56,25 @@ const extractMetadata = (resource: File | string) => { 'Ends at': input.computeDuration().then(duration => `${duration} seconds`), 'Tracks': input.getTracks().then(tracks => tracks.map(track => ({ 'Type': track.type, - 'Codec': track.codec, + 'Codec': track.getCodec(), 'Full codec string': track.getCodecParameterString(), 'Starts at': track.getFirstTimestamp().then(start => `${start} seconds`), 'Ends at': track.computeDuration().then(duration => `${duration} seconds`), - 'Language code': track.languageCode, + 'Language code': track.getLanguageCode(), ...(track.isVideoTrack() ? { - 'Coded width': `${track.codedWidth} pixels`, - 'Coded height': `${track.codedHeight} pixels`, - 'Rotation': `${track.rotation}° clockwise`, - 'Pixel aspect ratio': `${track.pixelAspectRatio.num}:${track.pixelAspectRatio.den}`, - 'Display width': `${track.displayWidth} pixels`, - 'Display height': `${track.displayHeight} pixels`, + 'Coded width': track.getCodedWidth().then(w => `${w} pixels`), + 'Coded height': track.getCodedHeight().then(h => `${h} pixels`), + 'Rotation': track.getRotation().then(rot => `${rot}° clockwise`), + 'Pixel aspect ratio': track.getPixelAspectRatio().then(par => `${par.num}:${par.den}`), + 'Display width': track.getDisplayWidth().then(w => `${w} pixels`), + 'Display height': track.getDisplayHeight().then(h => `${h} pixels`), 'Transparency': track.canBeTransparent(), } : track.isAudioTrack() ? { - 'Number of channels': track.numberOfChannels, - 'Sample rate': `${track.sampleRate} Hz`, + 'Number of channels': track.getNumberOfChannels(), + 'Sample rate': track.getSampleRate().then(rate => `${rate} Hz`), } : {}), 'Packet statistics': shortDelay().then(() => track.computePacketStats()).then(stats => ({ diff --git a/examples/thumbnail-generation/thumbnail-generation.ts b/examples/thumbnail-generation/thumbnail-generation.ts index a62ad6f..562921f 100644 --- a/examples/thumbnail-generation/thumbnail-generation.ts +++ b/examples/thumbnail-generation/thumbnail-generation.ts @@ -34,7 +34,7 @@ const generateThumbnails = async (resource: File | string) => { throw new Error('File has no video track.'); } - if (videoTrack.codec === null) { + if (await videoTrack.getCodec() === null) { throw new Error('Unsupported video codec.'); } @@ -43,12 +43,14 @@ const generateThumbnails = async (resource: File | string) => { } // Compute width and height of the thumbnails such that the larger dimension is equal to THUMBNAIL_SIZE - const width = videoTrack.displayWidth > videoTrack.displayHeight + const displayWidth = await videoTrack.getDisplayWidth(); + const displayHeight = await videoTrack.getDisplayHeight(); + const width = displayWidth > displayHeight ? THUMBNAIL_SIZE - : Math.floor(THUMBNAIL_SIZE * videoTrack.displayWidth / videoTrack.displayHeight); - const height = videoTrack.displayHeight > videoTrack.displayWidth + : Math.floor(THUMBNAIL_SIZE * displayWidth / displayHeight); + const height = displayHeight > displayWidth ? THUMBNAIL_SIZE - : Math.floor(THUMBNAIL_SIZE * videoTrack.displayHeight / videoTrack.displayWidth); + : Math.floor(THUMBNAIL_SIZE * displayHeight / displayWidth); // Create thumbnail elements const thumbnailElements = []; diff --git a/src/conversion.ts b/src/conversion.ts index 2c3449a..df5da96 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -972,7 +972,7 @@ export class Conversion { /** @internal */ async _processVideoTrack(track: InputVideoTrack, trackOptions: ConversionVideoOptions) { - const sourceCodec = track.codec; + const sourceCodec = await track.getCodec(); if (!sourceCodec) { this.discardedTracks.push({ track, @@ -983,14 +983,16 @@ export class Conversion { let videoSource: VideoSource; - const totalRotation = normalizeRotation(track.rotation + (trackOptions.rotate ?? 0)); + const totalRotation = normalizeRotation(await track.getRotation() + (trackOptions.rotate ?? 0)); let outputTrackRotation = totalRotation; const canUseRotationMetadata = this.output.format.supportsVideoRotationMetadata && (trackOptions.allowRotationMetadata ?? true); + const squarePixelWidth = await track.getSquarePixelWidth(); + const squarePixelHeight = await track.getSquarePixelHeight(); const [rotatedWidth, rotatedHeight] = totalRotation % 180 === 0 - ? [track.squarePixelWidth, track.squarePixelHeight] - : [track.squarePixelHeight, track.squarePixelWidth]; + ? [squarePixelWidth, squarePixelHeight] + : [squarePixelHeight, squarePixelWidth]; let crop = trackOptions.crop; if (crop) { @@ -1131,8 +1133,8 @@ export class Conversion { || (totalRotation !== 0 && (!canUseRotationMetadata || trackOptions.process !== undefined)) || !!crop // Don't expect encoders to reliably handle non-square pixels: - || track.squarePixelWidth !== track.codedWidth - || track.squarePixelHeight !== track.codedHeight; + || squarePixelWidth !== await track.getCodedWidth() + || squarePixelHeight !== await track.getCodedHeight(); if (!needsRerender) { // If we're directly passing decoded samples back to the encoder, sometimes the encoder may error due @@ -1346,12 +1348,13 @@ export class Conversion { } } + const videoTrackLanguageCode = await track.getLanguageCode(); this.output.addVideoTrack(videoSource, { frameRate: trackOptions.frameRate, // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2 - languageCode: isIso639Dash2LanguageCode(track.languageCode) ? track.languageCode : undefined, - name: track.name ?? undefined, - disposition: track.disposition, + languageCode: isIso639Dash2LanguageCode(videoTrackLanguageCode) ? videoTrackLanguageCode : undefined, + name: await track.getName() ?? undefined, + disposition: await track.getDisposition(), rotation: outputTrackRotation, }); this._addedCounts.video++; @@ -1423,7 +1426,7 @@ export class Conversion { /** @internal */ async _processAudioTrack(track: InputAudioTrack, trackOptions: ConversionAudioOptions) { - const sourceCodec = track.codec; + const sourceCodec = await track.getCodec(); if (!sourceCodec) { this.discardedTracks.push({ track, @@ -1434,8 +1437,8 @@ export class Conversion { let audioSource: AudioSource; - const originalNumberOfChannels = track.numberOfChannels; - const originalSampleRate = track.sampleRate; + const originalNumberOfChannels = await track.getNumberOfChannels(); + const originalSampleRate = await track.getSampleRate(); const firstTimestamp = await track.getFirstTimestamp(); @@ -1597,11 +1600,12 @@ export class Conversion { } } + const audioTrackLanguageCode = await track.getLanguageCode(); this.output.addAudioTrack(audioSource, { // TODO: This condition can be removed when all demuxers properly homogenize to BCP47 in v2 - languageCode: isIso639Dash2LanguageCode(track.languageCode) ? track.languageCode : undefined, - name: track.name ?? undefined, - disposition: track.disposition, + languageCode: isIso639Dash2LanguageCode(audioTrackLanguageCode) ? audioTrackLanguageCode : undefined, + name: await track.getName() ?? undefined, + disposition: await track.getDisposition(), }); this._addedCounts.audio++; this._totalTrackCount++; diff --git a/src/hls/hls-demuxer.ts b/src/hls/hls-demuxer.ts index 45dcdd9..7144980 100644 --- a/src/hls/hls-demuxer.ts +++ b/src/hls/hls-demuxer.ts @@ -18,7 +18,7 @@ import { import { PacketRetrievalOptions } from '../media-sink'; import { DEFAULT_TRACK_DISPOSITION, MetadataTags, TrackDisposition } from '../metadata'; import { TrackType } from '../output'; -import { assert, joinPaths, Rotation, UNDETERMINED_LANGUAGE } from '../misc'; +import { assert, joinPaths, MaybePromise, Rotation, UNDETERMINED_LANGUAGE } from '../misc'; import { EncodedPacket } from '../packet'; import { readAllLines } from '../reader'; import { AttributeList, canIgnoreLine } from './hls-misc'; @@ -200,10 +200,13 @@ export class HlsDemuxer extends Demuxer { const input = segmentedInput.toInput(); const tracks = await input.getTracks(); + const tracksWithCodec = await Promise.all( + tracks.map(async t => ({ track: t, codec: await t.getCodec() })), + ); codecStrings = await Promise.all( - tracks + tracksWithCodec .filter(x => x.codec !== null) - .map(x => x.getCodecParameterString()), + .map(x => x.track.getCodecParameterString()), ) as string[]; } @@ -243,7 +246,7 @@ export class HlsDemuxer extends Demuxer { const input = segmentedInput.toInput(); const videoTrack = await input.getPrimaryVideoTrack(); - if (!videoTrack || videoTrack.codec === null) { + if (!videoTrack || (await videoTrack.getCodec()) === null) { return null; } @@ -284,7 +287,7 @@ export class HlsDemuxer extends Demuxer { const input = segmentedInput.toInput(); const audioTrack = await input.getPrimaryAudioTrack(); - if (!audioTrack || audioTrack.codec === null) { + if (!audioTrack || (await audioTrack.getCodec()) === null) { return null; } @@ -610,41 +613,45 @@ export class HlsDemuxer extends Demuxer { } abstract class HlsInputTrackBacking implements InputTrackBacking { + hydrationPromise: Promise | null = null; + constructor(public internalTrack: InternalTrack) {} abstract getType(): TrackType; abstract getDecoderConfig(): Promise; - isHydrated(): boolean { - return !!this.internalTrack.backingTrack; + hydrate() { + return this.hydrationPromise ??= (async () => { + const segmentedInput = this.internalTrack.demuxer.getSegmentedInputForPath(this.internalTrack.fullPath); + const input = segmentedInput.toInput(); + + let track: InputTrack | null; + if (this instanceof HlsInputVideoTrackBacking) { + track = await input.getPrimaryVideoTrack({ + filter: async t => (await t.getCodec()) === this.getCodec(), + }); + } else { + assert(this instanceof HlsInputAudioTrackBacking); + track = await input.getPrimaryAudioTrack({ + filter: async t => (await t.getCodec()) === this.getCodec(), + }); + } + + if (!track) { + throw new Error('Could not find matching track in underlying media data.'); + } + + this.internalTrack.backingTrack = track; + })(); } - async hydrate() { - const segmentedInput = this.internalTrack.demuxer.getSegmentedInputForPath(this.internalTrack.fullPath); - const input = segmentedInput.toInput(); - - let track: InputTrack | null; - if (this instanceof HlsInputVideoTrackBacking) { - track = await input.getPrimaryVideoTrack({ - filter: track => track.codec === this.getCodec(), - }); - } else { - assert(this instanceof HlsInputAudioTrackBacking); - track = await input.getPrimaryAudioTrack({ - filter: track => track.codec === this.getCodec(), - }); + /** If the backing track is already present, delegate synchronously; otherwise, hydrate first. */ + delegate(fn: () => MaybePromise): MaybePromise { + if (this.internalTrack.backingTrack) { + return fn(); } - if (!track) { - throw new Error('Could not find matching track in underlying media data.'); - } - - if (!(track._backing.isHydrated?.() ?? true)) { - // Just in case, typically not needed except for cursed shit like recursive .m3u8 - await track.input._hydrateBacking(track._backing); - } - - this.internalTrack.backingTrack = track; + return this.hydrate().then(fn); } getCodec(): MediaCodec | null { @@ -696,12 +703,12 @@ abstract class HlsInputTrackBacking implements InputTrackBacking { return number; } - getTimeResolution(): number | undefined { - return this.internalTrack.backingTrack?._backing.getTimeResolution(); + getTimeResolution(): MaybePromise { + return this.delegate(() => this.internalTrack.backingTrack!._backing.getTimeResolution()); } - isRelativeToUnixEpoch(): boolean | undefined { - return this.internalTrack.backingTrack?._backing.isRelativeToUnixEpoch(); + isRelativeToUnixEpoch(): MaybePromise { + return this.delegate(() => this.internalTrack.backingTrack!._backing.isRelativeToUnixEpoch()); } getBitrate(): number | null { @@ -713,42 +720,42 @@ abstract class HlsInputTrackBacking implements InputTrackBacking { } async getDurationFromMetadata(options: DurationMetadataRequestOptions): Promise { - assert(this.internalTrack.backingTrack); - return this.internalTrack.backingTrack._backing.getDurationFromMetadata(options); + await this.hydrate(); + return this.internalTrack.backingTrack!._backing.getDurationFromMetadata(options); } async getLiveRefreshInterval(): Promise { - assert(this.internalTrack.backingTrack); - return this.internalTrack.backingTrack._backing.getLiveRefreshInterval(); + await this.hydrate(); + return this.internalTrack.backingTrack!._backing.getLiveRefreshInterval(); } getHasOnlyKeyPackets() { return this.internalTrack.hasOnlyKeyPackets || null; } - getFirstPacket(options: PacketRetrievalOptions): Promise { - assert(this.internalTrack.backingTrack); - return this.internalTrack.backingTrack._backing.getFirstPacket(options); + async getFirstPacket(options: PacketRetrievalOptions): Promise { + await this.hydrate(); + return this.internalTrack.backingTrack!._backing.getFirstPacket(options); } - getPacket(timestamp: number, options: PacketRetrievalOptions): Promise { - assert(this.internalTrack.backingTrack); - return this.internalTrack.backingTrack._backing.getPacket(timestamp, options); + async getPacket(timestamp: number, options: PacketRetrievalOptions): Promise { + await this.hydrate(); + return this.internalTrack.backingTrack!._backing.getPacket(timestamp, options); } - getKeyPacket(timestamp: number, options: PacketRetrievalOptions): Promise { - assert(this.internalTrack.backingTrack); - return this.internalTrack.backingTrack._backing.getKeyPacket(timestamp, options); + async getKeyPacket(timestamp: number, options: PacketRetrievalOptions): Promise { + await this.hydrate(); + return this.internalTrack.backingTrack!._backing.getKeyPacket(timestamp, options); } - getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise { - assert(this.internalTrack.backingTrack); - return this.internalTrack.backingTrack._backing.getNextPacket(packet, options); + async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise { + await this.hydrate(); + return this.internalTrack.backingTrack!._backing.getNextPacket(packet, options); } - getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise { - assert(this.internalTrack.backingTrack); - return this.internalTrack.backingTrack._backing.getNextKeyPacket(packet, options); + async getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise { + await this.hydrate(); + return this.internalTrack.backingTrack!._backing.getNextKeyPacket(packet, options); } } @@ -775,20 +782,20 @@ class HlsInputVideoTrackBacking return inferredCodec as VideoCodec; } - getCodedWidth(): number | undefined { - return this.backingVideoTrack?._backing.getCodedWidth(); + getCodedWidth(): MaybePromise { + return this.delegate(() => this.backingVideoTrack!._backing.getCodedWidth()); } - getCodedHeight(): number | undefined { - return this.backingVideoTrack?._backing.getCodedHeight(); + getCodedHeight(): MaybePromise { + return this.delegate(() => this.backingVideoTrack!._backing.getCodedHeight()); } - getSquarePixelWidth(): number | undefined { - return this.backingVideoTrack?._backing.getSquarePixelWidth(); + getSquarePixelWidth(): MaybePromise { + return this.delegate(() => this.backingVideoTrack!._backing.getSquarePixelWidth()); } - getSquarePixelHeight(): number | undefined { - return this.backingVideoTrack?._backing.getSquarePixelHeight(); + getSquarePixelHeight(): MaybePromise { + return this.delegate(() => this.backingVideoTrack!._backing.getSquarePixelHeight()); } getMetadataDisplayWidth(): number | null { @@ -807,27 +814,30 @@ class HlsInputVideoTrackBacking return this.internalTrack.info.height; } - getRotation(): Rotation | undefined { - return this.backingVideoTrack?._backing.getRotation(); + getRotation(): MaybePromise { + return this.delegate(() => this.backingVideoTrack!._backing.getRotation()); } - getColorSpace(): Promise { - assert(this.backingVideoTrack); - return this.backingVideoTrack._backing.getColorSpace(); + async getColorSpace(): Promise { + await this.hydrate(); + return this.backingVideoTrack!._backing.getColorSpace(); } - canBeTransparent(): Promise { - assert(this.backingVideoTrack); - return this.backingVideoTrack._backing.canBeTransparent(); + async canBeTransparent(): Promise { + await this.hydrate(); + return this.backingVideoTrack!._backing.canBeTransparent(); } - getCodecParameterString(): string { + getMetadataCodecParameterString(): string | null { + if (this.backingVideoTrack) { + return null; + } return this.internalTrack.fullCodecString; } - getDecoderConfig(): Promise { - assert(this.backingVideoTrack); - return this.backingVideoTrack._backing.getDecoderConfig(); + async getDecoderConfig(): Promise { + await this.hydrate(); + return this.backingVideoTrack!._backing.getDecoderConfig(); } } @@ -854,25 +864,28 @@ class HlsInputAudioTrackBacking return inferredCodec as AudioCodec; } - getNumberOfChannels(): number | undefined { + getNumberOfChannels(): MaybePromise { if (this.internalTrack.info.numberOfChannels !== null) { return this.internalTrack.info.numberOfChannels; } - return this.backingAudioTrack?._backing.getNumberOfChannels(); + return this.delegate(() => this.backingAudioTrack!._backing.getNumberOfChannels()); } - getSampleRate(): number | undefined { - return this.backingAudioTrack?._backing.getSampleRate(); + getSampleRate(): MaybePromise { + return this.delegate(() => this.backingAudioTrack!._backing.getSampleRate()); } - getCodecParameterString(): string { + getMetadataCodecParameterString(): string | null { + if (this.backingAudioTrack) { + return null; + } return this.internalTrack.fullCodecString; } - getDecoderConfig(): Promise { - assert(this.backingAudioTrack); - return this.backingAudioTrack._backing.getDecoderConfig(); + async getDecoderConfig(): Promise { + await this.hydrate(); + return this.backingAudioTrack!._backing.getDecoderConfig(); } } diff --git a/src/index.ts b/src/index.ts index 2dd87a6..2a99be0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -216,17 +216,12 @@ export { InputTrack, InputVideoTrack, InputAudioTrack, + InputTrackQuery, PacketStats, -} from './input-track'; -export { - InputTrackDescriptor, - InputVideoTrackDescriptor, - InputAudioTrackDescriptor, - InputTrackDescriptorQuery, asc, desc, prefer, -} from './input-track-descriptor'; +} from './input-track'; export { EncodedPacket, EncodedPacketSideData, diff --git a/src/input-track-descriptor.ts b/src/input-track-descriptor.ts deleted file mode 100644 index b78d058..0000000 --- a/src/input-track-descriptor.ts +++ /dev/null @@ -1,530 +0,0 @@ -/*! - * Copyright (c) 2026-present, Vanilagy and contributors - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { AudioCodec, MediaCodec, VideoCodec } from './codec'; -import { Input } from './input'; -import { - InputAudioTrack, - InputAudioTrackBacking, - InputTrack, - InputTrackBacking, - InputVideoTrack, - InputVideoTrackBacking, -} from './input-track'; -import { TrackDisposition } from './metadata'; -import { MaybePromise } from './misc'; -import { TrackType } from './output'; - -/** - * A lightweight descriptor for an {@link InputTrack}. Contains a subset of the track's properties, and can be - * converted/upgraded to the full track via the {@link InputTrackDescriptor.getTrack} method. - * - * For some formats, such as HLS with master playlists, obtaining track descriptors is much cheaper than obtaining the - * input track. These descriptors therefore can be used for efficient track selection and filtering without having to - * expensively hydrate all input tracks. - * - * @group Input files & tracks - * @public - */ -export abstract class InputTrackDescriptor { - /** The input file this descriptor belongs to. */ - readonly input: Input; - /** @internal */ - _backing: InputTrackBacking; - - /** @internal */ - constructor(input: Input, backing: InputTrackBacking) { - this.input = input; - this._backing = backing; - } - - /** The unique ID of this track in the input file. */ - get id() { - return this._backing.getId(); - } - - /** The 1-based index of this track among all tracks of the same type in the input file. */ - get number() { - return this._backing.getNumber(); - } - - /** The type of the track. */ - abstract get type(): TrackType; - - /** Returns true if and only if this is a video track descriptor. */ - isVideoTrackDescriptor(): this is InputVideoTrackDescriptor { - return this instanceof InputVideoTrackDescriptor; - } - - /** Returns true if and only if this is an audio track descriptor. */ - isAudioTrackDescriptor(): this is InputAudioTrackDescriptor { - return this instanceof InputAudioTrackDescriptor; - } - - /** The codec of the track's packets, `undefined` if not yet known. */ - abstract get codec(): MediaCodec | null | undefined; - - /** - * The full codec parameter string (e.g. `'avc1.64001f'`), `undefined` if not yet known. - * This is typically available from HLS master playlists. - */ - get codecParameterString(): string | null | undefined { - return this._backing.getCodecParameterString?.(); - } - - /** The ISO 639-2/T language code for this track, `undefined` if not yet known. */ - get languageCode(): string | undefined { - return this._backing.getLanguageCode(); - } - - /** A user-defined name for this track, `undefined` if not yet known. */ - get name(): string | null | undefined { - return this._backing.getName(); - } - - /** The track's disposition, i.e. information about its intended usage, `undefined` if not yet known. */ - get disposition(): TrackDisposition | undefined { - return this._backing.getDisposition(); - } - - /** - * The peak bitrate of the track as specified in the track's metadata. This might not match the actual - * media data's bitrate. - */ - get bitrate(): number | null | undefined { - return this._backing.getBitrate(); - } - - /** - * The average bitrate of the track as specified in the track's metadata. This might not match the actual - * media data's bitrate. - */ - get averageBitrate(): number | null | undefined { - return this._backing.getAverageBitrate(); - } - - /** Whether the track metadata says that this track only contains key packets, `undefined` if not yet known. */ - abstract get hasOnlyKeyPackets(): boolean | undefined; - - /** - * Returns `true` if this descriptor can be paired with the given track or descriptor. Two tracks being pairable - * means they can be presented (displayed) together. - * - * Returns `false` if `other` equals `this`. - */ - canBePairedWith(other: InputTrackDescriptor | InputTrack) { - if (!(other instanceof InputTrack || other instanceof InputTrackDescriptor)) { - throw new TypeError('other must be an InputTrack or InputTrackDescriptor.'); - } - - if (this.input !== other.input || this === other) { - return false; - } - - return (this._backing.getPairingMask() & other._backing.getPairingMask()) !== 0n; - } - - /** - * Gets the list of other descriptors that can be paired with this descriptor. An optional query can be provided - * to narrow down the results. - */ - async getPairableDescriptors(query?: InputTrackDescriptorQuery) { - query &&= toValidatedTrackDescriptorQuery(query); - - const descriptors = await this.input.getTrackDescriptors(); - return queryTrackDescriptors( - descriptors.filter(x => this.canBePairedWith(x)), - query, - ); - } - - /** - * Gets the list of other video track descriptors that can be paired with this descriptor. An optional query can - * be provided to narrow down the results. - */ - async getPairableVideoTrackDescriptors(query?: InputTrackDescriptorQuery) { - query &&= toValidatedTrackDescriptorQuery(query); - - const descriptors = await this.getPairableDescriptors(); - return queryTrackDescriptors( - descriptors.filter((x): x is InputVideoTrackDescriptor => x.isVideoTrackDescriptor()), - query, - ); - } - - /** - * Gets the list of other audio track descriptors that can be paired with this descriptor. An optional query can - * be provided to narrow down the results. - */ - async getPairableAudioTrackDescriptors(query?: InputTrackDescriptorQuery) { - query &&= toValidatedTrackDescriptorQuery(query); - - const descriptors = await this.getPairableDescriptors(); - return queryTrackDescriptors( - descriptors.filter((x): x is InputAudioTrackDescriptor => x.isAudioTrackDescriptor()), - query, - ); - } - - /** Returns `true` if there is another descriptor that can be paired with this descriptor. */ - hasPairableDescriptor(predicate?: (descriptor: InputTrackDescriptor) => boolean) { - const descriptors = [...this.input._backingToDescriptor.values()]; - return descriptors.some(x => this.canBePairedWith(x) && (!predicate || predicate(x))); - } - - /** Returns `true` if there is a video track that can be paired with this descriptor. */ - hasPairableVideoTrack(predicate?: (descriptor: InputVideoTrackDescriptor) => boolean) { - return this.hasPairableDescriptor(x => - x.isVideoTrackDescriptor() && (!predicate || predicate(x)), - ); - } - - /** Returns `true` if there is an audio track that can be paired with this descriptor. */ - hasPairableAudioTrack(predicate?: (descriptor: InputAudioTrackDescriptor) => boolean) { - return this.hasPairableDescriptor(x => - x.isAudioTrackDescriptor() && (!predicate || predicate(x)), - ); - } - - /** - * Loads the full media data for this track and returns a fully-loaded {@link InputTrack}. Calling this - * multiple times returns the same instance. - */ - async getTrack(): Promise { - return this.input._getTrackForBacking(this._backing); - } -} - -/** - * A lightweight descriptor for an {@link InputVideoTrack}. See {@link InputTrackDescriptor} for details. - * - * @group Input files & tracks - * @public - */ -export class InputVideoTrackDescriptor extends InputTrackDescriptor { - /** @internal */ - override _backing: InputVideoTrackBacking; - - /** @internal */ - constructor(input: Input, backing: InputVideoTrackBacking) { - super(input, backing); - this._backing = backing; - } - - get type(): TrackType { - return 'video'; - } - - get hasOnlyKeyPackets(): boolean | undefined { - return this._backing.getHasOnlyKeyPackets?.() ?? false; - } - - get codec(): VideoCodec | null | undefined { - return this._backing.getCodec(); - } - - /** The display width in pixels from metadata, `undefined` if not yet known. */ - get displayWidth(): number | undefined { - const metadataWidth = this._backing.getMetadataDisplayWidth?.() ?? null; - if (metadataWidth !== null) { - return metadataWidth; - } - - const rotation = this._backing.getRotation(); - const squarePixelWidth = this._backing.getSquarePixelWidth(); - const squarePixelHeight = this._backing.getSquarePixelHeight(); - - if (rotation === undefined || squarePixelWidth === undefined || squarePixelHeight === undefined) { - return undefined; - } - - return rotation % 180 === 0 ? squarePixelWidth : squarePixelHeight; - } - - /** The display height in pixels from metadata, `undefined` if not yet known. */ - get displayHeight(): number | undefined { - const metadataHeight = this._backing.getMetadataDisplayHeight?.() ?? null; - if (metadataHeight !== null) { - return metadataHeight; - } - - const rotation = this._backing.getRotation(); - const squarePixelWidth = this._backing.getSquarePixelWidth(); - const squarePixelHeight = this._backing.getSquarePixelHeight(); - - if (rotation === undefined || squarePixelWidth === undefined || squarePixelHeight === undefined) { - return undefined; - } - - return rotation % 180 === 0 ? squarePixelHeight : squarePixelWidth; - } - - override async getTrack(): Promise { - return super.getTrack() as Promise; - } -} - -/** - * A lightweight descriptor for an {@link InputAudioTrack}. See {@link InputTrackDescriptor} for details. - * - * @group Input files & tracks - * @public - */ -export class InputAudioTrackDescriptor extends InputTrackDescriptor { - /** @internal */ - override _backing: InputAudioTrackBacking; - - /** @internal */ - constructor(input: Input, backing: InputAudioTrackBacking) { - super(input, backing); - this._backing = backing; - } - - get hasOnlyKeyPackets(): boolean | undefined { - return this._backing.getHasOnlyKeyPackets?.() ?? true; - } - - get type(): TrackType { - return 'audio'; - } - - get codec(): AudioCodec | null | undefined { - return this._backing.getCodec(); - } - - /** The number of audio channels, `undefined` if not yet known. */ - get numberOfChannels(): number | undefined { - return this._backing.getNumberOfChannels(); - } - - /** The audio sample rate in hertz, `undefined` if not yet known. */ - get sampleRate(): number | undefined { - return this._backing.getSampleRate(); - } - - override async getTrack(): Promise { - return super.getTrack() as Promise; - } -} - -/** - * Defines a query for track descriptors and, by extension, for tracks. Can be used to query tracks tersely and - * expressively, which is especially useful for media inputs with many tracks, such as HLS manifests. - * - * @group Input files & tracks - * @public - */ -export type InputTrackDescriptorQuery = { - /** - * A filter predicate function called for every track descriptor. Returning or resolving to `false` excludes the - * track from the result. - */ - filter?: (descriptor: T) => MaybePromise; - /** - * A function called for every track descriptor, used to define a track ordering. Tracks are ordered in ascending - * order using the value returned by this function. When the function returns an array of numbers `arr`, tracks will - * be sorted by `arr[0]` unless they have the same value, in which case they will be sorted by `arr[1]`, and so on. - * This allows you to construct a list of ordering criteria, sorted by importance. - * - * To help construct complex ordering criteria, the {@link asc}, {@link desc}, and {@link prefer} helper functions - * can be used. - */ - sortBy?: (descriptor: T) => MaybePromise; -}; - -/** - * Helper function for use in {@link InputTrackDescriptorQuery.sortBy}, used to describe sorting tracks by a numeric - * property in ascending order. `null` and `undefined` are accepted too and are last in the order (sorted to the end). - * - * @group Input files & tracks - * @public - */ -export const asc = (value: number | null | undefined) => { - return value ?? Infinity; // nulls and undefined last -}; - -/** - * Helper function for use in {@link InputTrackDescriptorQuery.sortBy}, used to describe sorting tracks by a numeric - * property in descending order. `null` and `undefined` are accepted too and are last in the order (sorted to the end). - * - * @group Input files & tracks - * @public - */ -export const desc = (value: number | null | undefined) => { - return -(value ?? -Infinity); // nulls and undefined last -}; - -/** - * Helper function for use in {@link InputTrackDescriptorQuery.sortBy}, used to sort tracks by boolean properties. - * `true` is sorted to the start, `false` to the end. Useful for expressing soft preferences (e.g., "I'd prefer 1080p, - * but other resolutions are fine too") as opposed to {@link InputTrackDescriptorQuery.filter} which expresses hard - * requirements for tracks. - * - * @group Input files & tracks - * @public - */ -export const prefer = (value: boolean) => { - return -value; -}; - -export const toValidatedTrackDescriptorQuery = ( - query: InputTrackDescriptorQuery, -): InputTrackDescriptorQuery => { - if (typeof query !== 'object' || !query) { - throw new TypeError('query must be an object.'); - } - if (query.filter !== undefined && typeof query.filter !== 'function') { - throw new TypeError('query.filter, when provided, must be a function.'); - } - if (query.sortBy !== undefined && typeof query.sortBy !== 'function') { - throw new TypeError('query.sortBy, when provided, must be a function.'); - } - - // Instead of validating the return types of the functions everywhere the query is used, simply return a new query - // which wraps the old one while validating it. - return { - filter: query.filter - ? (desc) => { - const handle = (bool: boolean) => { - if (typeof bool !== 'boolean') { - throw new TypeError('query.filter must return or resolve to a boolean.'); - } - - return bool; - }; - - const result = query.filter!(desc); - if (result instanceof Promise) { - return result.then(handle); - } else { - return handle(result); - } - } - : undefined, - sortBy: query.sortBy - ? (desc) => { - const handle = (value: number | number[]) => { - if ( - typeof value !== 'number' - && (!Array.isArray(value) || !value.every(x => typeof x === 'number')) - ) { - throw new TypeError( - 'query.sortBy must return or resolve to a number or an array of numbers.', - ); - } - - return value; - }; - - const result = query.sortBy!(desc); - if (result instanceof Promise) { - return result.then(handle); - } else { - return handle(result); - } - } - : undefined, - }; -}; - -export const mergeTrackDescriptorQueries = ( - queryA: InputTrackDescriptorQuery | undefined, - queryB: InputTrackDescriptorQuery | undefined, -): InputTrackDescriptorQuery => { - return { - filter: queryA?.filter || queryB?.filter - ? (descriptor) => { - const resultA = queryA?.filter?.(descriptor) ?? true; - const handleResultA = (resultA: boolean) => { - if (resultA === false) { - return false; - } - - return queryB?.filter?.(descriptor) ?? true; - }; - - if (resultA instanceof Promise) { - return resultA.then(handleResultA); - } else { - return handleResultA(resultA); - } - } - : undefined, - sortBy: queryA?.sortBy || queryB?.sortBy - ? (descriptor) => { - const resultA = queryA?.sortBy?.(descriptor) ?? []; - const resultB = queryB?.sortBy?.(descriptor) ?? []; - - type Result = Awaited; - const join = (resultA: Result, resultB: Result) => { - return [ - ...(Array.isArray(resultA) ? resultA : [resultA]), - ...(Array.isArray(resultB) ? resultB : [resultB]), - ]; - }; - - if (resultA instanceof Promise || resultB instanceof Promise) { - return Promise.all([resultA, resultB]).then(([resultA, resultB]) => { - return join(resultA, resultB); - }); - } else { - return join(resultA, resultB); - } - } - : undefined, - }; -}; - -export const queryTrackDescriptors = async ( - descriptors: T[], - query?: InputTrackDescriptorQuery, -): Promise => { - let matched = descriptors; - if (query?.filter) { - const filterMatches = descriptors.map(d => query.filter!(d)); - const hasAsyncFilter = filterMatches.some(x => x instanceof Promise); - if (hasAsyncFilter) { - // eslint-disable-next-line @typescript-eslint/await-thenable - const resolvedFilterMatches = await Promise.all(filterMatches); - matched = descriptors.filter((_, i) => resolvedFilterMatches[i]); - } else { - matched = descriptors.filter((_, i) => filterMatches[i] as boolean); - } - } - - if (!query?.sortBy) { - return matched; - } - - const sortValues = matched.map(d => query.sortBy!(d)); - const hasAsyncSort = sortValues.some(x => x instanceof Promise); - const resolvedSortValues = hasAsyncSort - // eslint-disable-next-line @typescript-eslint/await-thenable - ? await Promise.all(sortValues) - : sortValues as (number | number[])[]; - - return matched - .map((descriptor, i) => ({ descriptor, sortValue: resolvedSortValues[i] })) - .sort((a, b) => { - const aValues = Array.isArray(a.sortValue) ? a.sortValue : [a.sortValue]; - const bValues = Array.isArray(b.sortValue) ? b.sortValue : [b.sortValue]; - const maxLength = Math.max(aValues.length, bValues.length); - - for (let i = 0; i < maxLength; i++) { - const aValue = aValues[i] ?? 0; - const bValue = bValues[i] ?? 0; - if (aValue === bValue) { - continue; - } - return aValue - bValue; - } - - return 0; - }) - .map(x => x.descriptor); -}; diff --git a/src/input-track.ts b/src/input-track.ts index 48c249a..c0347c5 100644 --- a/src/input-track.ts +++ b/src/input-track.ts @@ -11,18 +11,11 @@ import { determineVideoPacketType } from './codec-data'; import { customAudioDecoders, customVideoDecoders } from './custom-coder'; import { Input } from './input'; import { EncodedPacketSink, PacketRetrievalOptions } from './media-sink'; -import { assert, Rational, Rotation, roundToDivisor, simplifyRational } from './misc'; +import { assert, MaybePromise, Rational, Rotation, roundToDivisor, simplifyRational } from './misc'; import { TrackType } from './output'; import { EncodedPacket, PacketType } from './packet'; import { TrackDisposition } from './metadata'; import { DurationMetadataRequestOptions } from './demuxer'; -import { - InputTrackDescriptor, - mergeTrackDescriptorQueries, - type InputVideoTrackDescriptor, - type InputAudioTrackDescriptor, - type InputTrackDescriptorQuery, -} from './input-track-descriptor'; /** * Contains aggregate statistics about the encoded packets of a track. @@ -42,30 +35,28 @@ export interface InputTrackBacking { getType(): TrackType; getId(): number; getNumber(): number; - getCodec(): MediaCodec | null; - getInternalCodecId(): string | number | Uint8Array | null; - getName(): string | null; - getLanguageCode(): string; - getTimeResolution(): number | undefined; - isRelativeToUnixEpoch(): boolean | undefined; - getDisposition(): TrackDisposition; + + getCodec(): MaybePromise; + getInternalCodecId(): MaybePromise; + getName(): MaybePromise; + getLanguageCode(): MaybePromise; + getTimeResolution(): MaybePromise; + isRelativeToUnixEpoch(): MaybePromise; + getDisposition(): MaybePromise; getPairingMask(): bigint; - getBitrate(): number | null; - getAverageBitrate(): number | null; + getBitrate(): MaybePromise; + getAverageBitrate(): MaybePromise; getDurationFromMetadata(options: DurationMetadataRequestOptions): Promise; getLiveRefreshInterval(): Promise; - getHasOnlyKeyPackets?(): boolean | null; + getHasOnlyKeyPackets?(): MaybePromise; getDecoderConfig(): Promise; - getCodecParameterString?(): string | null; + getMetadataCodecParameterString?(): MaybePromise; getFirstPacket(options: PacketRetrievalOptions): Promise; getPacket(timestamp: number, options: PacketRetrievalOptions): Promise; getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise; getKeyPacket(timestamp: number, options: PacketRetrievalOptions): Promise; getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise; - - isHydrated?(): boolean; - hydrate?(): Promise; } /** @@ -87,6 +78,12 @@ export abstract class InputTrack { /** The type of the track. */ abstract get type(): TrackType; /** The codec of the track's packets. */ + abstract getCodec(): Promise; + /** + * The codec of the track's packets. + * @deprecated Use {@link InputTrack.getCodec} instead. + */ + // eslint-disable-next-line @typescript-eslint/no-deprecated abstract get codec(): MediaCodec | null; /** Returns the full codec parameter string for this track. */ abstract getCodecParameterString(): Promise; @@ -98,6 +95,12 @@ export abstract class InputTrack { */ abstract determinePacketType(packet: EncodedPacket): Promise; /** Whether the track metadata says that this track only contains key packets. The actual packets may differ. */ + abstract getHasOnlyKeyPackets(): Promise; + /** + * Whether the track metadata says that this track only contains key packets. The actual packets may differ. + * @deprecated Use {@link InputTrack.getHasOnlyKeyPackets} instead. + */ + // eslint-disable-next-line @typescript-eslint/no-deprecated abstract get hasOnlyKeyPackets(): boolean; /** Returns true if and only if this track is a video track. */ @@ -128,72 +131,141 @@ export abstract class InputTrack { * The identifier of the codec used internally by the container. It is not homogenized by Mediabunny * and depends entirely on the container format. * - * This field can be used to determine the codec of a track in case Mediabunny doesn't know that codec. + * This method can be used to determine the codec of a track in case Mediabunny doesn't know that codec. * - * - For ISOBMFF files, this field returns the name of the Sample Description Box (e.g. `'avc1'`). - * - For Matroska files, this field returns the value of the `CodecID` element. - * - For WAVE files, this field returns the value of the format tag in the `'fmt '` chunk. - * - For ADTS files, this field contains the `MPEG-4 Audio Object Type`. - * - For MPEG-TS files, this field contains the `streamType` value from the Program Map Table. - * - In all other cases, this field is `null`. + * - For ISOBMFF files, this resolves to the name of the Sample Description Box (e.g. `'avc1'`). + * - For Matroska files, this resolves to the value of the `CodecID` element. + * - For WAVE files, this resolves to the value of the format tag in the `'fmt '` chunk. + * - For ADTS files, this resolves to the `MPEG-4 Audio Object Type`. + * - For MPEG-TS files, this resolves to the `streamType` value from the Program Map Table. + * - In all other cases, this resolves to `null`. */ - get internalCodecId() { + async getInternalCodecId() { return this._backing.getInternalCodecId(); } /** - * The ISO 639-2/T language code for this track. If the language is unknown, this field is `'und'` (undetermined). + * See {@link InputTrack.getInternalCodecId}. + * @deprecated Use {@link InputTrack.getInternalCodecId} instead. */ - get languageCode() { + get internalCodecId() { + return requireSync(this._backing.getInternalCodecId(), 'internalCodecId', 'getInternalCodecId'); + } + + /** + * The ISO 639-2/T language code for this track. If the language is unknown, this resolves to `'und'` + * (undetermined). + */ + async getLanguageCode() { return this._backing.getLanguageCode(); } + /** + * The ISO 639-2/T language code for this track. If the language is unknown, this field is `'und'` (undetermined). + * @deprecated Use {@link InputTrack.getLanguageCode} instead. + */ + get languageCode() { + return requireSync(this._backing.getLanguageCode(), 'languageCode', 'getLanguageCode'); + } + /** A user-defined name for this track. */ - get name() { + async getName() { return this._backing.getName(); } + /** + * A user-defined name for this track. + * @deprecated Use {@link InputTrack.getName} instead. + */ + get name() { + return requireSync(this._backing.getName(), 'name', 'getName'); + } + /** * A positive number x such that all timestamps and durations of all packets of this track are * integer multiples of 1/x. */ + async getTimeResolution() { + return this._backing.getTimeResolution(); + } + + /** + * A positive number x such that all timestamps and durations of all packets of this track are + * integer multiples of 1/x. + * @deprecated Use {@link InputTrack.getTimeResolution} instead. + */ get timeResolution() { - const value = this._backing.getTimeResolution(); - assert(value !== undefined); - return value; + return requireSync(this._backing.getTimeResolution(), 'timeResolution', 'getTimeResolution'); } /** * Whether the timestamps of this track are relative to the Unix epoch (January 1, 1970 00:00:00 UTC). When `true`, * each timestamp maps to a definitive point in time. */ + async getIsRelativeToUnixEpoch() { + return this._backing.isRelativeToUnixEpoch(); + } + + /** + * Whether the timestamps of this track are relative to the Unix epoch (January 1, 1970 00:00:00 UTC). When `true`, + * each timestamp maps to a definitive point in time. + * @deprecated Use {@link InputTrack.getIsRelativeToUnixEpoch} instead. + */ get isRelativeToUnixEpoch() { - const value = this._backing.isRelativeToUnixEpoch(); - assert(value !== undefined); - return value; + return requireSync( + this._backing.isRelativeToUnixEpoch(), + 'isRelativeToUnixEpoch', + 'getIsRelativeToUnixEpoch', + ); } /** The track's disposition, i.e. information about its intended usage. */ - get disposition() { + async getDisposition() { return this._backing.getDisposition(); } + /** + * The track's disposition, i.e. information about its intended usage. + * @deprecated Use {@link InputTrack.getDisposition} instead. + */ + get disposition() { + return requireSync(this._backing.getDisposition(), 'disposition', 'getDisposition'); + } + /** * The peak bitrate of the track, in bits per second, as specified in the track's metadata. This might not match the * actual media data's bitrate. */ - get bitrate() { + async getBitrate() { return this._backing.getBitrate(); } + /** + * The peak bitrate of the track, in bits per second, as specified in the track's metadata. This might not match the + * actual media data's bitrate. + * @deprecated Use {@link InputTrack.getBitrate} instead. + */ + get bitrate() { + return requireSync(this._backing.getBitrate(), 'bitrate', 'getBitrate'); + } + /** * The average bitrate of the track, in bits per second, as specified in the track's metadata. This might not match * the actual media data's bitrate. */ - get averageBitrate() { + async getAverageBitrate() { return this._backing.getAverageBitrate(); } + /** + * The average bitrate of the track, in bits per second, as specified in the track's metadata. This might not match + * the actual media data's bitrate. + * @deprecated Use {@link InputTrack.getAverageBitrate} instead. + */ + get averageBitrate() { + return requireSync(this._backing.getAverageBitrate(), 'averageBitrate', 'getAverageBitrate'); + } + /** * Returns the start timestamp of the first packet of this track, in seconds. While often near zero, this value * may be positive or even negative. A negative starting timestamp means the track's timing has been offset. Samples @@ -215,7 +287,7 @@ export abstract class InputTrack { const lastPacket = await this._backing.getPacket(Infinity, { metadataOnly: true, ...options }); const result = (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0); - return roundToDivisor(result, this.timeResolution); + return roundToDivisor(result, await this.getTimeResolution()); } /** @@ -304,9 +376,9 @@ export abstract class InputTrack { * * Returns `false` if `other` equals `this`. */ - canBePairedWith(other: InputTrack | InputTrackDescriptor) { - if (!(other instanceof InputTrack || other instanceof InputTrackDescriptor)) { - throw new TypeError('other must be an InputTrack or InputTrackDescriptor.'); + canBePairedWith(other: InputTrack) { + if (!(other instanceof InputTrack)) { + throw new TypeError('other must be an InputTrack.'); } if (this.input !== other.input || this === other) { @@ -320,123 +392,126 @@ export abstract class InputTrack { * Gets the list of other tracks that can be paired with this track. An optional query can be provided to narrow * down the results. */ - async getPairableTracks(query?: InputTrackDescriptorQuery) { - const descriptors = await this.input.getTrackDescriptors(mergeTrackDescriptorQueries({ - filter: d => d.canBePairedWith(this), + async getPairableTracks(query?: InputTrackQuery) { + return this.input.getTracks(mergeInputTrackQueries({ + filter: t => t.canBePairedWith(this), }, query)); - - return Promise.all(descriptors.map(d => d.getTrack())); - } - - /** Returns the first track that can be paired with this track, optionally steered by the provided query. */ - async pluckPairableTrack(query?: InputTrackDescriptorQuery) { - return (await this.getPairableTracks(query))[0] ?? null; } /** * Gets the list of other video tracks that can be paired with this track. An optional query can be provided to * narrow down the results. */ - async getPairableVideoTracks(query?: InputTrackDescriptorQuery) { - const descriptors = await this.input.getVideoTrackDescriptors(mergeTrackDescriptorQueries({ - filter: d => d.canBePairedWith(this), + async getPairableVideoTracks(query?: InputTrackQuery) { + return this.input.getVideoTracks(mergeInputTrackQueries({ + filter: t => t.canBePairedWith(this), }, query)); - - return Promise.all(descriptors.map(d => d.getTrack())); - } - - /** Returns the first video track that can be paired with this track, optionally steered by the provided query. */ - async pluckPairableVideoTrack(query?: InputTrackDescriptorQuery) { - return (await this.getPairableVideoTracks(query))[0] ?? null; } /** * Gets the list of other audio tracks that can be paired with this track. An optional query can be provided to * narrow down the results. */ - async getPairableAudioTracks(query?: InputTrackDescriptorQuery) { - const descriptors = await this.input.getAudioTrackDescriptors(mergeTrackDescriptorQueries({ - filter: d => d.canBePairedWith(this), - }, query)); - - return Promise.all(descriptors.map(d => d.getTrack())); - } - - /** Returns the first audio track that can be paired with this track, optionally steered by the provided query. */ - async pluckPairableAudioTrack(query?: InputTrackDescriptorQuery) { - return (await this.getPairableAudioTracks(query))[0] ?? null; - } - - /** Returns the primary track that can be paired with this track, optionally steered by the provided query. */ - async getPrimaryPairableVideoTrack(query?: InputTrackDescriptorQuery) { - return this.input.getPrimaryVideoTrack(mergeTrackDescriptorQueries({ - filter: d => d.canBePairedWith(this), + async getPairableAudioTracks(query?: InputTrackQuery) { + return this.input.getAudioTracks(mergeInputTrackQueries({ + filter: t => t.canBePairedWith(this), }, query)); } /** Returns the primary track that can be paired with this track, optionally steered by the provided query. */ - async getPrimaryPairableAudioTrack(query?: InputTrackDescriptorQuery) { - return this.input.getPrimaryAudioTrack(mergeTrackDescriptorQueries({ - filter: d => d.canBePairedWith(this), + async getPrimaryPairableVideoTrack(query?: InputTrackQuery) { + return this.input.getPrimaryVideoTrack(mergeInputTrackQueries({ + filter: t => t.canBePairedWith(this), + }, query)); + } + + /** Returns the primary track that can be paired with this track, optionally steered by the provided query. */ + async getPrimaryPairableAudioTrack(query?: InputTrackQuery) { + return this.input.getPrimaryAudioTrack(mergeInputTrackQueries({ + filter: t => t.canBePairedWith(this), }, query)); } /** Returns `true` if there is another track that can be paired with this track. */ - hasPairableTrack(predicate?: (descriptor: InputTrackDescriptor) => boolean) { + async hasPairableTrack(predicate?: (track: InputTrack) => MaybePromise): Promise { predicate &&= toValidatedPredicate(predicate); - const descriptors = [...this.input._backingToDescriptor.values()]; - return descriptors.some(x => - this.canBePairedWith(x) && (!predicate || predicate(x)), - ); + const tracks = await this.input.getTracks(); + for (const track of tracks) { + if (!this.canBePairedWith(track)) { + continue; + } + if (!predicate || await predicate(track)) { + return true; + } + } + + return false; } /** Returns `true` if there is a video track that can be paired with this track. */ - hasPairableVideoTrack(predicate?: (descriptor: InputVideoTrackDescriptor) => boolean) { + hasPairableVideoTrack(predicate?: (track: InputVideoTrack) => MaybePromise): Promise { predicate &&= toValidatedPredicate(predicate); - return this.hasPairableTrack(x => - x.isVideoTrackDescriptor() && (!predicate || predicate(x)), + return this.hasPairableTrack(async x => + x.isVideoTrack() && (!predicate || await predicate(x)), ); } /** Returns `true` if there is an audio track that can be paired with this track. */ - hasPairableAudioTrack(predicate?: (descriptor: InputAudioTrackDescriptor) => boolean) { + hasPairableAudioTrack(predicate?: (track: InputAudioTrack) => MaybePromise): Promise { predicate &&= toValidatedPredicate(predicate); - return this.hasPairableTrack(x => - x.isAudioTrackDescriptor() && (!predicate || predicate(x)), + return this.hasPairableTrack(async x => + x.isAudioTrack() && (!predicate || await predicate(x)), ); } } -const toValidatedPredicate = (predicate?: (descriptor: T) => boolean) => { +const requireSync = (value: MaybePromise, getterName: string, asyncName: string): T => { + if (value instanceof Promise) { + throw new Error( + `'${getterName}' is not available synchronously for this track. Use '${asyncName}()' instead.`, + ); + } + return value; +}; + +const toValidatedPredicate = ( + predicate?: (track: T) => MaybePromise, +) => { if (predicate !== undefined && typeof predicate !== 'function') { throw new TypeError('predicate, when provided, must be a function.'); } return predicate - ? (desc: T) => { - const result = predicate(desc); - if (typeof result !== 'boolean') { - throw new TypeError('predicate must return a boolean value.'); - } + ? (track: T) => { + const handle = (result: boolean) => { + if (typeof result !== 'boolean') { + throw new TypeError('predicate must return or resolve to a boolean value.'); + } + return result; + }; - return result; + const result = predicate(track); + if (result instanceof Promise) { + return result.then(handle); + } + return handle(result); } : undefined; }; export interface InputVideoTrackBacking extends InputTrackBacking { getType(): 'video'; - getCodec(): VideoCodec | null; - getCodedWidth(): number | undefined; - getCodedHeight(): number | undefined; - getSquarePixelWidth(): number | undefined; - getSquarePixelHeight(): number | undefined; - getMetadataDisplayWidth?(): number | null; - getMetadataDisplayHeight?(): number | null; - getRotation(): Rotation | undefined; + getCodec(): MaybePromise; + getCodedWidth(): MaybePromise; + getCodedHeight(): MaybePromise; + getSquarePixelWidth(): MaybePromise; + getSquarePixelHeight(): MaybePromise; + getMetadataDisplayWidth?(): MaybePromise; + getMetadataDisplayHeight?(): MaybePromise; + getRotation(): MaybePromise; getColorSpace(): Promise; canBeTransparent(): Promise; getDecoderConfig(): Promise; @@ -464,68 +539,188 @@ export class InputVideoTrack extends InputTrack { return 'video'; } - get codec(): VideoCodec | null { + /** The codec of the track's packets. */ + async getCodec(): Promise { return this._backing.getCodec(); } + /** + * The codec of the track's packets. + * @deprecated Use {@link InputVideoTrack.getCodec} instead. + */ + get codec(): VideoCodec | null { + return requireSync(this._backing.getCodec(), 'codec', 'getCodec'); + } + + /** Whether the track metadata says that this track only contains key packets. The actual packets may differ. */ + async getHasOnlyKeyPackets() { + return (await this._backing.getHasOnlyKeyPackets?.()) ?? false; + } + + /** + * Whether the track metadata says that this track only contains key packets. The actual packets may differ. + * @deprecated Use {@link InputVideoTrack.getHasOnlyKeyPackets} instead. + */ get hasOnlyKeyPackets() { - return this._backing.getHasOnlyKeyPackets?.() ?? false; + const raw = this._backing.getHasOnlyKeyPackets?.(); + if (raw === undefined) { + return false; + } + return requireSync(raw, 'hasOnlyKeyPackets', 'getHasOnlyKeyPackets') ?? false; } /** The width in pixels of the track's coded samples, before any transformations or rotations. */ + async getCodedWidth() { + return this._backing.getCodedWidth(); + } + + /** + * The width in pixels of the track's coded samples, before any transformations or rotations. + * @deprecated Use {@link InputVideoTrack.getCodedWidth} instead. + */ get codedWidth() { - const value = this._backing.getCodedWidth(); - assert(value !== undefined); - return value; + return requireSync(this._backing.getCodedWidth(), 'codedWidth', 'getCodedWidth'); } /** The height in pixels of the track's coded samples, before any transformations or rotations. */ + async getCodedHeight() { + return this._backing.getCodedHeight(); + } + + /** + * The height in pixels of the track's coded samples, before any transformations or rotations. + * @deprecated Use {@link InputVideoTrack.getCodedHeight} instead. + */ get codedHeight() { - const value = this._backing.getCodedHeight(); - assert(value !== undefined); - return value; + return requireSync(this._backing.getCodedHeight(), 'codedHeight', 'getCodedHeight'); } /** The angle in degrees by which the track's frames should be rotated (clockwise). */ + async getRotation() { + return this._backing.getRotation(); + } + + /** + * The angle in degrees by which the track's frames should be rotated (clockwise). + * @deprecated Use {@link InputVideoTrack.getRotation} instead. + */ get rotation() { - const value = this._backing.getRotation(); - assert(value !== undefined); - return value; + return requireSync(this._backing.getRotation(), 'rotation', 'getRotation'); } /** The width of the track's frames in square pixels, adjusted for pixel aspect ratio but before rotation. */ + async getSquarePixelWidth() { + return this._backing.getSquarePixelWidth(); + } + + /** + * The width of the track's frames in square pixels, adjusted for pixel aspect ratio but before rotation. + * @deprecated Use {@link InputVideoTrack.getSquarePixelWidth} instead. + */ get squarePixelWidth() { - const value = this._backing.getSquarePixelWidth(); - assert(value !== undefined); - return value; + return requireSync(this._backing.getSquarePixelWidth(), 'squarePixelWidth', 'getSquarePixelWidth'); } /** The height of the track's frames in square pixels, adjusted for pixel aspect ratio but before rotation. */ + async getSquarePixelHeight() { + return this._backing.getSquarePixelHeight(); + } + + /** + * The height of the track's frames in square pixels, adjusted for pixel aspect ratio but before rotation. + * @deprecated Use {@link InputVideoTrack.getSquarePixelHeight} instead. + */ get squarePixelHeight() { - const value = this._backing.getSquarePixelHeight(); - assert(value !== undefined); - return value; + return requireSync(this._backing.getSquarePixelHeight(), 'squarePixelHeight', 'getSquarePixelHeight'); } /** * The pixel aspect ratio of the track's frames, as a rational number in its reduced form. Most videos use * square pixels (1:1). */ + async getPixelAspectRatio() { + // Potential minor async race condition here if called twice, but doesn't matter since the computation is + // so cheap + return this._pixelAspectRatioCache ??= simplifyRational({ + num: (await this.getSquarePixelWidth()) * (await this.getCodedHeight()), + den: (await this.getSquarePixelHeight()) * (await this.getCodedWidth()), + }); + } + + /** + * The pixel aspect ratio of the track's frames, as a rational number in its reduced form. Most videos use + * square pixels (1:1). + * @deprecated Use {@link InputVideoTrack.getPixelAspectRatio} instead. + */ get pixelAspectRatio() { return this._pixelAspectRatioCache ??= simplifyRational({ - num: this.squarePixelWidth * this.codedHeight, - den: this.squarePixelHeight * this.codedWidth, + num: requireSync(this._backing.getSquarePixelWidth(), 'pixelAspectRatio', 'getPixelAspectRatio') + * requireSync(this._backing.getCodedHeight(), 'pixelAspectRatio', 'getPixelAspectRatio'), + den: requireSync(this._backing.getSquarePixelHeight(), 'pixelAspectRatio', 'getPixelAspectRatio') + * requireSync(this._backing.getCodedWidth(), 'pixelAspectRatio', 'getPixelAspectRatio'), }); } /** The display width of the track's frames in pixels, after aspect ratio adjustment and rotation. */ + async getDisplayWidth() { + const metadata = await this._backing.getMetadataDisplayWidth?.(); + if (metadata != null) { + return metadata; + } + + const rotation = await this.getRotation(); + return rotation % 180 === 0 ? this.getSquarePixelWidth() : this.getSquarePixelHeight(); + } + + /** + * The display width of the track's frames in pixels, after aspect ratio adjustment and rotation. + * @deprecated Use {@link InputVideoTrack.getDisplayWidth} instead. + */ get displayWidth() { - return this.rotation % 180 === 0 ? this.squarePixelWidth : this.squarePixelHeight; + const metadataRaw = this._backing.getMetadataDisplayWidth?.(); + if (metadataRaw !== undefined) { + const metadata = requireSync(metadataRaw, 'displayWidth', 'getDisplayWidth'); + if (metadata !== null) { + return metadata; + } + } + + const rotation = requireSync(this._backing.getRotation(), 'displayWidth', 'getDisplayWidth'); + const value = rotation % 180 === 0 + ? this._backing.getSquarePixelWidth() + : this._backing.getSquarePixelHeight(); + return requireSync(value, 'displayWidth', 'getDisplayWidth'); } /** The display height of the track's frames in pixels, after aspect ratio adjustment and rotation. */ + async getDisplayHeight() { + const metadata = await this._backing.getMetadataDisplayHeight?.(); + if (metadata != null) { + return metadata; + } + + const rotation = await this.getRotation(); + return rotation % 180 === 0 ? this.getSquarePixelHeight() : this.getSquarePixelWidth(); + } + + /** + * The display height of the track's frames in pixels, after aspect ratio adjustment and rotation. + * @deprecated Use {@link InputVideoTrack.getDisplayHeight} instead. + */ get displayHeight() { - return this.rotation % 180 === 0 ? this.squarePixelHeight : this.squarePixelWidth; + const metadataRaw = this._backing.getMetadataDisplayHeight?.(); + if (metadataRaw !== undefined) { + const metadata = requireSync(metadataRaw, 'displayHeight', 'getDisplayHeight'); + if (metadata !== null) { + return metadata; + } + } + + const rotation = requireSync(this._backing.getRotation(), 'displayHeight', 'getDisplayHeight'); + const value = rotation % 180 === 0 + ? this._backing.getSquarePixelHeight() + : this._backing.getSquarePixelWidth(); + return requireSync(value, 'displayHeight', 'getDisplayHeight'); } /** Returns the color space of the track's samples. */ @@ -557,6 +752,11 @@ export class InputVideoTrack extends InputTrack { } async getCodecParameterString() { + const fromMetadata = await this._backing.getMetadataCodecParameterString?.(); + if (fromMetadata != null) { + return fromMetadata; + } + const decoderConfig = await this._backing.getDecoderConfig(); return decoderConfig?.codec ?? null; } @@ -568,7 +768,7 @@ export class InputVideoTrack extends InputTrack { return false; } - const codec = this._backing.getCodec(); + const codec = await this._backing.getCodec(); assert(codec !== null); if (customVideoDecoders.some(x => x.supports(codec, decoderConfig))) { @@ -595,22 +795,23 @@ export class InputVideoTrack extends InputTrack { throw new TypeError('packet must not be metadata-only to determine its type.'); } - if (this.codec === null) { + const codec = await this.getCodec(); + if (codec === null) { return null; } const decoderConfig = await this.getDecoderConfig(); assert(decoderConfig); - return determineVideoPacketType(this.codec, decoderConfig, packet.data); + return determineVideoPacketType(codec, decoderConfig, packet.data); } } export interface InputAudioTrackBacking extends InputTrackBacking { getType(): 'audio'; - getCodec(): AudioCodec | null; - getNumberOfChannels(): number | undefined; - getSampleRate(): number | undefined; + getCodec(): MaybePromise; + getNumberOfChannels(): MaybePromise; + getSampleRate(): MaybePromise; getDecoderConfig(): Promise; } @@ -634,26 +835,60 @@ export class InputAudioTrack extends InputTrack { return 'audio'; } - get codec(): AudioCodec | null { + /** The codec of the track's packets. */ + async getCodec(): Promise { return this._backing.getCodec(); } + /** + * The codec of the track's packets. + * @deprecated Use {@link InputAudioTrack.getCodec} instead. + */ + get codec(): AudioCodec | null { + return requireSync(this._backing.getCodec(), 'codec', 'getCodec'); + } + + /** Whether the track metadata says that this track only contains key packets. The actual packets may differ. */ + async getHasOnlyKeyPackets() { + return (await this._backing.getHasOnlyKeyPackets?.()) ?? true; + } + + /** + * Whether the track metadata says that this track only contains key packets. The actual packets may differ. + * @deprecated Use {@link InputAudioTrack.getHasOnlyKeyPackets} instead. + */ get hasOnlyKeyPackets() { - return this._backing.getHasOnlyKeyPackets?.() ?? true; + const raw = this._backing.getHasOnlyKeyPackets?.(); + if (raw === undefined) { + return true; + } + return requireSync(raw, 'hasOnlyKeyPackets', 'getHasOnlyKeyPackets') ?? true; } /** The number of audio channels in the track. */ + async getNumberOfChannels() { + return this._backing.getNumberOfChannels(); + } + + /** + * The number of audio channels in the track. + * @deprecated Use {@link InputAudioTrack.getNumberOfChannels} instead. + */ get numberOfChannels() { - const value = this._backing.getNumberOfChannels(); - assert(value !== undefined); - return value; + return requireSync(this._backing.getNumberOfChannels(), 'numberOfChannels', 'getNumberOfChannels'); } /** The track's audio sample rate in hertz. */ + async getSampleRate() { + return this._backing.getSampleRate(); + } + + /** + * The track's audio sample rate in hertz. + * @deprecated Use {@link InputAudioTrack.getSampleRate} instead. + */ get sampleRate() { - const value = this._backing.getSampleRate(); - assert(value !== undefined); - return value; + return requireSync(this._backing.getSampleRate(), 'sampleRate', 'getSampleRate'); } /** @@ -666,6 +901,11 @@ export class InputAudioTrack extends InputTrack { } async getCodecParameterString() { + const fromMetadata = await this._backing.getMetadataCodecParameterString?.(); + if (fromMetadata != null) { + return fromMetadata; + } + const decoderConfig = await this._backing.getDecoderConfig(); return decoderConfig?.codec ?? null; } @@ -677,7 +917,7 @@ export class InputAudioTrack extends InputTrack { return false; } - const codec = this._backing.getCodec(); + const codec = await this._backing.getCodec(); assert(codec !== null); if (customAudioDecoders.some(x => x.supports(codec, decoderConfig))) { @@ -705,10 +945,227 @@ export class InputAudioTrack extends InputTrack { throw new TypeError('packet must be an EncodedPacket.'); } - if (this.codec === null) { + if ((await this.getCodec()) === null) { return null; } return 'key'; // No audio codec with delta packets } } + +/** + * Defines a query for input tracks. Can be used to query tracks tersely and expressively, which is especially useful + * for media inputs with many tracks, such as HLS manifests. + * + * @group Input files & tracks + * @public + */ +export type InputTrackQuery = { + /** + * A filter predicate function called for every track. Returning or resolving to `false` excludes the track from + * the result. + */ + filter?: (track: T) => MaybePromise; + /** + * A function called for every track, used to define a track ordering. Tracks are ordered in ascending order using + * the value returned by this function. When the function returns an array of numbers `arr`, tracks will be sorted + * by `arr[0]` unless they have the same value, in which case they will be sorted by `arr[1]`, and so on. This + * allows you to construct a list of ordering criteria, sorted by importance. + * + * To help construct complex ordering criteria, the {@link asc}, {@link desc}, and {@link prefer} helper functions + * can be used. + */ + sortBy?: (track: T) => MaybePromise; +}; + +/** + * Helper function for use in {@link InputTrackQuery.sortBy}, used to describe sorting tracks by a numeric property in + * ascending order. `null` and `undefined` are accepted too and are last in the order (sorted to the end). + * + * @group Input files & tracks + * @public + */ +export const asc = (value: number | null | undefined) => { + return value ?? Infinity; // nulls and undefined last +}; + +/** + * Helper function for use in {@link InputTrackQuery.sortBy}, used to describe sorting tracks by a numeric property in + * descending order. `null` and `undefined` are accepted too and are last in the order (sorted to the end). + * + * @group Input files & tracks + * @public + */ +export const desc = (value: number | null | undefined) => { + return -(value ?? -Infinity); // nulls and undefined last +}; + +/** + * Helper function for use in {@link InputTrackQuery.sortBy}, used to sort tracks by boolean properties. `true` is + * sorted to the start, `false` to the end. Useful for expressing soft preferences (e.g., "I'd prefer 1080p, but other + * resolutions are fine too") as opposed to {@link InputTrackQuery.filter} which expresses hard requirements for + * tracks. + * + * @group Input files & tracks + * @public + */ +export const prefer = (value: boolean) => { + return -value; +}; + +export const toValidatedInputTrackQuery = ( + query: InputTrackQuery, +): InputTrackQuery => { + if (typeof query !== 'object' || !query) { + throw new TypeError('query must be an object.'); + } + if (query.filter !== undefined && typeof query.filter !== 'function') { + throw new TypeError('query.filter, when provided, must be a function.'); + } + if (query.sortBy !== undefined && typeof query.sortBy !== 'function') { + throw new TypeError('query.sortBy, when provided, must be a function.'); + } + + // Instead of validating the return types of the functions everywhere the query is used, simply return a new query + // which wraps the old one while validating it. + return { + filter: query.filter + ? (track) => { + const handle = (bool: boolean) => { + if (typeof bool !== 'boolean') { + throw new TypeError('query.filter must return or resolve to a boolean.'); + } + + return bool; + }; + + const result = query.filter!(track); + if (result instanceof Promise) { + return result.then(handle); + } else { + return handle(result); + } + } + : undefined, + sortBy: query.sortBy + ? (track) => { + const handle = (value: number | number[]) => { + if ( + typeof value !== 'number' + && (!Array.isArray(value) || !value.every(x => typeof x === 'number')) + ) { + throw new TypeError( + 'query.sortBy must return or resolve to a number or an array of numbers.', + ); + } + + return value; + }; + + const result = query.sortBy!(track); + if (result instanceof Promise) { + return result.then(handle); + } else { + return handle(result); + } + } + : undefined, + }; +}; + +export const mergeInputTrackQueries = ( + queryA: InputTrackQuery | undefined, + queryB: InputTrackQuery | undefined, +): InputTrackQuery => { + return { + filter: queryA?.filter || queryB?.filter + ? (track) => { + const resultA = queryA?.filter?.(track) ?? true; + const handleResultA = (resultA: boolean) => { + if (resultA === false) { + return false; + } + + return queryB?.filter?.(track) ?? true; + }; + + if (resultA instanceof Promise) { + return resultA.then(handleResultA); + } else { + return handleResultA(resultA); + } + } + : undefined, + sortBy: queryA?.sortBy || queryB?.sortBy + ? (track) => { + const resultA = queryA?.sortBy?.(track) ?? []; + const resultB = queryB?.sortBy?.(track) ?? []; + + type Result = Awaited; + const join = (resultA: Result, resultB: Result) => { + return [ + ...(Array.isArray(resultA) ? resultA : [resultA]), + ...(Array.isArray(resultB) ? resultB : [resultB]), + ]; + }; + + if (resultA instanceof Promise || resultB instanceof Promise) { + return Promise.all([resultA, resultB]).then(([resultA, resultB]) => { + return join(resultA, resultB); + }); + } else { + return join(resultA, resultB); + } + } + : undefined, + }; +}; + +export const queryInputTracks = async ( + tracks: T[], + query?: InputTrackQuery, +): Promise => { + let matched = tracks; + if (query?.filter) { + const filterMatches = tracks.map(t => query.filter!(t)); + const hasAsyncFilter = filterMatches.some(x => x instanceof Promise); + if (hasAsyncFilter) { + // eslint-disable-next-line @typescript-eslint/await-thenable + const resolvedFilterMatches = await Promise.all(filterMatches); + matched = tracks.filter((_, i) => resolvedFilterMatches[i]); + } else { + matched = tracks.filter((_, i) => filterMatches[i] as boolean); + } + } + + if (!query?.sortBy) { + return matched; + } + + const sortValues = matched.map(t => query.sortBy!(t)); + const hasAsyncSort = sortValues.some(x => x instanceof Promise); + const resolvedSortValues = hasAsyncSort + // eslint-disable-next-line @typescript-eslint/await-thenable + ? await Promise.all(sortValues) + : sortValues as (number | number[])[]; + + return matched + .map((track, i) => ({ track, sortValue: resolvedSortValues[i] })) + .sort((a, b) => { + const aValues = Array.isArray(a.sortValue) ? a.sortValue : [a.sortValue]; + const bValues = Array.isArray(b.sortValue) ? b.sortValue : [b.sortValue]; + const maxLength = Math.max(aValues.length, bValues.length); + + for (let i = 0; i < maxLength; i++) { + const aValue = aValues[i] ?? 0; + const bValue = bValues[i] ?? 0; + if (aValue === bValue) { + continue; + } + return aValue - bValue; + } + + return 0; + }) + .map(x => x.track); +}; diff --git a/src/input.ts b/src/input.ts index 35e4c23..d161dca 100644 --- a/src/input.ts +++ b/src/input.ts @@ -15,18 +15,13 @@ import { InputTrackBacking, InputVideoTrack, InputVideoTrackBacking, -} from './input-track'; -import { - InputAudioTrackDescriptor, - InputTrackDescriptor, - InputVideoTrackDescriptor, - mergeTrackDescriptorQueries, - queryTrackDescriptors, - toValidatedTrackDescriptorQuery, - InputTrackDescriptorQuery, + InputTrackQuery, + mergeInputTrackQueries, + queryInputTracks, + toValidatedInputTrackQuery, prefer, desc, -} from './input-track-descriptor'; +} from './input-track'; import { PacketRetrievalOptions } from './media-sink'; import { arrayArgmin, @@ -145,10 +140,6 @@ export class Input extends EventEmitter /** @internal */ _backingToTrack = new Map(); /** @internal */ - _backingToDescriptor = new Map(); - /** @internal */ - _hydrationPromises = new Map>(); - /** @internal */ _disposed = false; /** @internal */ _nextSourceCacheAge = 0; @@ -511,51 +502,30 @@ export class Input extends EventEmitter * Returns the list of all tracks of this input file in the order in which they appear in the file. An optional * query can be provided. */ - async getTracks( - query?: InputTrackDescriptorQuery, - ): Promise { - const descriptors = await this.getTrackDescriptors(query); - return Promise.all(descriptors.map(x => x.getTrack())); - } + async getTracks(query?: InputTrackQuery): Promise { + query &&= toValidatedInputTrackQuery(query); - /** Returns the first track in this input file, optionally steered by the provided query. */ - async pluckTrack( - query?: InputTrackDescriptorQuery, - ): Promise { - const descriptors = await this.getTrackDescriptors(query); - return descriptors[0]?.getTrack() ?? null; + const backings = await this._getTrackBackings(); + const tracks = backings.map(backing => this._wrapBackingAsTrack(backing)); + return queryInputTracks(tracks, query); } /** Returns the list of all video tracks of this input file. An optional query can be provided. */ - async getVideoTracks( - query?: InputTrackDescriptorQuery, - ): Promise { - const descriptors = await this.getVideoTrackDescriptors(query); - return Promise.all(descriptors.map(x => x.getTrack())); - } + async getVideoTracks(query?: InputTrackQuery): Promise { + query &&= toValidatedInputTrackQuery(query); - /** Returns the first video track in this input file, optionally steered by the provided query. */ - async pluckVideoTrack( - query?: InputTrackDescriptorQuery, - ): Promise { - const descriptors = await this.getVideoTrackDescriptors(query); - return descriptors[0]?.getTrack() ?? null; + const tracks = await this.getTracks(); + const videoTracks = tracks.filter((x): x is InputVideoTrack => x.isVideoTrack()); + return queryInputTracks(videoTracks, query); } /** Returns the list of all audio tracks of this input file. An optional query can be provided. */ - async getAudioTracks( - query?: InputTrackDescriptorQuery, - ): Promise { - const descriptors = await this.getAudioTrackDescriptors(query); - return Promise.all(descriptors.map(x => x.getTrack())); - } + async getAudioTracks(query?: InputTrackQuery): Promise { + query &&= toValidatedInputTrackQuery(query); - /** Returns the first audio track in this input file, optionally steered by the provided query. */ - async pluckAudioTrack( - query?: InputTrackDescriptorQuery, - ): Promise { - const descriptors = await this.getAudioTrackDescriptors(query); - return descriptors[0]?.getTrack() ?? null; + const tracks = await this.getTracks(); + const audioTracks = tracks.filter((x): x is InputAudioTrack => x.isAudioTrack()); + return queryInputTracks(audioTracks, query); } /** @@ -565,12 +535,21 @@ export class Input extends EventEmitter * bitrate (higher bitrate is preferred), and if it can be paired with an audio track. */ async getPrimaryVideoTrack( - query?: InputTrackDescriptorQuery, + query?: InputTrackQuery, ): Promise { - query &&= toValidatedTrackDescriptorQuery(query); + query &&= toValidatedInputTrackQuery(query); - const descriptor = await this.getPrimaryVideoDescriptor(query); - return descriptor?.getTrack() ?? null; + const merged = mergeInputTrackQueries(query, { + sortBy: async t => [ + prefer((await t.getDisposition()).default), + prefer(await t.hasPairableAudioTrack()), + prefer(!(await t.getHasOnlyKeyPackets())), + desc(await t.getBitrate()), + ], + }); + + const sorted = await this.getVideoTracks(merged); + return sorted[0] ?? null; } /** @@ -580,94 +559,21 @@ export class Input extends EventEmitter * bitrate (higher bitrate is preferred), and if it can be paired with the primary video track. */ async getPrimaryAudioTrack( - query?: InputTrackDescriptorQuery, + query?: InputTrackQuery, ): Promise { - query &&= toValidatedTrackDescriptorQuery(query); + query &&= toValidatedInputTrackQuery(query); - const descriptor = await this.getPrimaryAudioDescriptor(query); - return descriptor?.getTrack() ?? null; - } + const primaryVideoTrack = await this.getPrimaryVideoTrack(); - /** - * Returns lightweight track descriptors without loading full media data. Useful for querying and filtering - * tracks (e.g. from an HLS master playlist) before committing to loading any specific track. - */ - async getTrackDescriptors(query?: InputTrackDescriptorQuery) { - query &&= toValidatedTrackDescriptorQuery(query); - - const backings = await this._getTrackBackings(); - const descriptors = backings.map(backing => this._wrapBackingAsDescriptor(backing)); - return queryTrackDescriptors(descriptors, query); - } - - /** - * Returns lightweight video track descriptors without loading full media data. Useful for querying and filtering - * video tracks (e.g. from an HLS master playlist) before committing to loading any specific track. - */ - async getVideoTrackDescriptors( - query?: InputTrackDescriptorQuery, - ): Promise { - query &&= toValidatedTrackDescriptorQuery(query); - - const descriptors = await this.getTrackDescriptors(); - const videoDescriptors = descriptors.filter( - (x): x is InputVideoTrackDescriptor => x.isVideoTrackDescriptor(), - ); - return queryTrackDescriptors(videoDescriptors, query); - } - - /** - * Returns lightweight audio track descriptors without loading full media data. Useful for querying and filtering - * audio tracks (e.g. from an HLS master playlist) before committing to loading any specific track. - */ - async getAudioTrackDescriptors( - query?: InputTrackDescriptorQuery, - ): Promise { - query &&= toValidatedTrackDescriptorQuery(query); - - const descriptors = await this.getTrackDescriptors(); - const audioDescriptors = descriptors.filter( - (x): x is InputAudioTrackDescriptor => x.isAudioTrackDescriptor(), - ); - return queryTrackDescriptors(audioDescriptors, query); - } - - /** Returns the primary video track descriptor of this input file, or null if there are no video tracks. */ - async getPrimaryVideoDescriptor( - query?: InputTrackDescriptorQuery, - ): Promise { - query &&= toValidatedTrackDescriptorQuery(query); - - const merged = mergeTrackDescriptorQueries(query, { - sortBy: d => [ - prefer(d.disposition?.default ?? false), - prefer(d.hasPairableAudioTrack()), - prefer(!d.hasOnlyKeyPackets), - desc(d.bitrate ?? null), + const merged = mergeInputTrackQueries(query, { + sortBy: async t => [ + prefer(!primaryVideoTrack || t.canBePairedWith(primaryVideoTrack)), + prefer((await t.getDisposition()).default), + desc(await t.getBitrate()), ], }); - const sorted = await this.getVideoTrackDescriptors(merged); - return sorted[0] ?? null; - } - - /** Returns the primary audio track descriptor of this input file, or null if there are no audio tracks. */ - async getPrimaryAudioDescriptor( - query?: InputTrackDescriptorQuery, - ): Promise { - query &&= toValidatedTrackDescriptorQuery(query); - - const primaryVideoDescriptor = await this.getPrimaryVideoDescriptor(); - - const merged = mergeTrackDescriptorQueries(query, { - sortBy: d => [ - prefer(!primaryVideoDescriptor || d.canBePairedWith(primaryVideoDescriptor)), - prefer(d.disposition?.default ?? false), - desc(d.bitrate ?? null), - ], - }); - - const sorted = await this.getAudioTrackDescriptors(merged); + const sorted = await this.getAudioTracks(merged); return sorted[0] ?? null; } @@ -693,43 +599,6 @@ export class Input extends EventEmitter return track; } - /** @internal */ - _wrapBackingAsDescriptor(backing: InputTrackBacking): InputTrackDescriptor { - const existing = this._backingToDescriptor.get(backing); - if (existing) { - return existing; - } - - const type = backing.getType(); - const descriptor = type === 'video' - ? new InputVideoTrackDescriptor(this, backing as InputVideoTrackBacking) - : new InputAudioTrackDescriptor(this, backing as InputAudioTrackBacking); - - this._backingToDescriptor.set(backing, descriptor); - return descriptor; - } - - /** @internal */ - _hydrateBacking(backing: InputTrackBacking): Promise { - if (!backing.hydrate || (backing.isHydrated?.() ?? true)) { - return Promise.resolve(); - } - - let promise = this._hydrationPromises.get(backing); - if (!promise) { - promise = backing.hydrate(); - this._hydrationPromises.set(backing, promise); - } - - return promise; - } - - /** @internal */ - async _getTrackForBacking(backing: InputTrackBacking): Promise { - await this._hydrateBacking(backing); - return this._wrapBackingAsTrack(backing); - } - /** Returns the full MIME type of this input file, including track codecs. */ async getMimeType() { const demuxer = await this._getDemuxer(); diff --git a/src/media-sink.ts b/src/media-sink.ts index 9814906..89141ab 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -245,7 +245,7 @@ export class EncodedPacketSink { const determinedType = await this._track.determinePacketType(packet); if (determinedType === 'delta') { // Try returning the previous key packet (in hopes that it's actually a key packet) - return this.getKeyPacket(packet.timestamp - 1 / this._track.timeResolution, options); + return this.getKeyPacket(packet.timestamp - 1 / await this._track.getTimeResolution(), options); } return packet; @@ -1432,10 +1432,10 @@ export class VideoSampleSink extends BaseMediaSampleSink { ); } - const codec = this._track.codec; - const rotation = this._track.rotation; + const codec = await this._track.getCodec(); + const rotation = await this._track.getRotation(); const decoderConfig = await this._track.getDecoderConfig(); - const timeResolution = this._track.timeResolution; + const timeResolution = await this._track.getTimeResolution(); assert(codec && decoderConfig); return new VideoDecoderWrapper(onSample, onError, codec, decoderConfig, rotation, timeResolution); @@ -1569,16 +1569,20 @@ export class CanvasSink { /** @internal */ _alpha: boolean; /** @internal */ - _width: number; + _width!: number; /** @internal */ - _height: number; + _height!: number; + /** @internal */ + _options: CanvasSinkOptions; /** @internal */ _fit: 'fill' | 'contain' | 'cover'; /** @internal */ - _rotation: Rotation; + _rotation: Rotation = 0; /** @internal */ _crop?: { left: number; top: number; width: number; height: number }; /** @internal */ + _initPromise: Promise | null = null; + /** @internal */ _videoSampleSink: VideoSampleSink; /** @internal */ _canvasPool: (HTMLCanvasElement | OffscreenCanvas | null)[]; @@ -1627,47 +1631,61 @@ export class CanvasSink { throw new TypeError('poolSize must be a non-negative integer.'); } - const rotation = options.rotation ?? videoTrack.rotation; - - const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 - ? [videoTrack.squarePixelWidth, videoTrack.squarePixelHeight] - : [videoTrack.squarePixelHeight, videoTrack.squarePixelWidth]; - - let crop = options.crop; - if (crop) { - crop = clampCropRectangle(crop, rotatedWidth, rotatedHeight); - } - - let [width, height] = crop - ? [crop.width, crop.height] - : [rotatedWidth, rotatedHeight]; - const originalAspectRatio = width / height; - - // If width and height aren't defined together, deduce the missing value using the aspect ratio - if (options.width !== undefined && options.height === undefined) { - width = options.width; - height = Math.round(width / originalAspectRatio); - } else if (options.width === undefined && options.height !== undefined) { - height = options.height; - width = Math.round(height * originalAspectRatio); - } else if (options.width !== undefined && options.height !== undefined) { - width = options.width; - height = options.height; - } - this._videoTrack = videoTrack; this._alpha = options.alpha ?? false; - this._width = width; - this._height = height; - this._rotation = rotation; - this._crop = crop; + this._options = options; this._fit = options.fit ?? 'fill'; this._videoSampleSink = new VideoSampleSink(videoTrack); this._canvasPool = Array.from({ length: options.poolSize ?? 0 }, () => null); } + /** @internal */ + _ensureInit() { + return this._initPromise ??= (async () => { + const options = this._options; + const videoTrack = this._videoTrack; + + const rotation = options.rotation ?? await videoTrack.getRotation(); + const squarePixelWidth = await videoTrack.getSquarePixelWidth(); + const squarePixelHeight = await videoTrack.getSquarePixelHeight(); + + const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 + ? [squarePixelWidth, squarePixelHeight] + : [squarePixelHeight, squarePixelWidth]; + + let crop = options.crop; + if (crop) { + crop = clampCropRectangle(crop, rotatedWidth, rotatedHeight); + } + + let [width, height] = crop + ? [crop.width, crop.height] + : [rotatedWidth, rotatedHeight]; + const originalAspectRatio = width / height; + + // If width and height aren't defined together, deduce the missing value using the aspect ratio + if (options.width !== undefined && options.height === undefined) { + width = options.width; + height = Math.round(width / originalAspectRatio); + } else if (options.width === undefined && options.height !== undefined) { + height = options.height; + width = Math.round(height * originalAspectRatio); + } else if (options.width !== undefined && options.height !== undefined) { + width = options.width; + height = options.height; + } + + this._width = width; + this._height = height; + this._rotation = rotation; + this._crop = crop; + })(); + } + /** @internal */ _videoSampleToWrappedCanvas(sample: VideoSample): WrappedCanvas { + const width = this._width; + const height = this._height; let canvas = this._canvasPool[this._nextCanvasIndex]; let canvasIsNew = false; @@ -1675,10 +1693,10 @@ export class CanvasSink { if (typeof document !== 'undefined') { // Prefer an HTMLCanvasElement canvas = document.createElement('canvas'); - canvas.width = this._width; - canvas.height = this._height; + canvas.width = width; + canvas.height = height; } else { - canvas = new OffscreenCanvas(this._width, this._height); + canvas = new OffscreenCanvas(width, height); } if (this._canvasPool.length > 0) { @@ -1702,9 +1720,9 @@ export class CanvasSink { if (!canvasIsNew) { if (!this._alpha && isFirefox()) { context.fillStyle = 'black'; - context.fillRect(0, 0, this._width, this._height); + context.fillRect(0, 0, width, height); } else { - context.clearRect(0, 0, this._width, this._height); + context.clearRect(0, 0, width, height); } } @@ -1734,6 +1752,7 @@ export class CanvasSink { */ async getCanvas(timestamp: number, options?: PacketRetrievalOptions) { validateTimestamp(timestamp); + await this._ensureInit(); const sample = await this._videoSampleSink.getSample(timestamp, options); return sample && this._videoSampleToWrappedCanvas(sample); @@ -1747,8 +1766,9 @@ export class CanvasSink { * @param endTimestamp - The timestamp in seconds at which to stop yielding canvases (exclusive). * @param options - Options used for the underlying packet retrieval. */ - canvases(startTimestamp = 0, endTimestamp = Infinity, options?: PacketRetrievalOptions) { - return mapAsyncGenerator( + async* canvases(startTimestamp = 0, endTimestamp = Infinity, options?: PacketRetrievalOptions) { + await this._ensureInit(); + yield* mapAsyncGenerator( this._videoSampleSink.samples(startTimestamp, endTimestamp, options), sample => this._videoSampleToWrappedCanvas(sample), ); @@ -1763,8 +1783,9 @@ export class CanvasSink { * @param timestamps - An iterable or async iterable of timestamps in seconds. * @param options - Options used for the underlying packet retrieval. */ - canvasesAtTimestamps(timestamps: AnyIterable, options?: PacketRetrievalOptions) { - return mapAsyncGenerator( + async* canvasesAtTimestamps(timestamps: AnyIterable, options?: PacketRetrievalOptions) { + await this._ensureInit(); + yield* mapAsyncGenerator( this._videoSampleSink.samplesAtTimestamps(timestamps, options), sample => sample && this._videoSampleToWrappedCanvas(sample), ); @@ -2111,7 +2132,7 @@ export class AudioSampleSink extends BaseMediaSampleSink { ); } - const codec = this._track.codec; + const codec = await this._track.getCodec(); const decoderConfig = await this._track.getDecoderConfig(); assert(codec && decoderConfig); diff --git a/src/media-source.ts b/src/media-source.ts index 9a48e7b..d7a0939 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -2417,6 +2417,7 @@ export class MediaStreamAudioTrackSource extends AudioSource { /** @internal */ private _audioContext: AudioContext | null = null; /** @internal */ + // eslint-disable-next-line @typescript-eslint/no-deprecated private _scriptProcessorNode: ScriptProcessorNode | null = null; // Deprecated but goated /** @internal */ private _promiseWithResolvers = promiseWithResolvers(); @@ -2550,6 +2551,7 @@ export class MediaStreamAudioTrackSource extends AudioSource { this._audioContext = new AudioContext({ sampleRate: this._track.getSettings().sampleRate }); const sourceNode = this._audioContext.createMediaStreamSource(new MediaStream([this._track])); + // eslint-disable-next-line @typescript-eslint/no-deprecated this._scriptProcessorNode = this._audioContext.createScriptProcessor(4096); if (this._audioContext.state === 'suspended') { @@ -2561,8 +2563,11 @@ export class MediaStreamAudioTrackSource extends AudioSource { let totalDuration = 0; + // eslint-disable-next-line @typescript-eslint/no-deprecated this._scriptProcessorNode.onaudioprocess = (event) => { + // eslint-disable-next-line @typescript-eslint/no-deprecated const iterator = AudioSample._fromAudioBuffer(event.inputBuffer, totalDuration); + // eslint-disable-next-line @typescript-eslint/no-deprecated totalDuration += event.inputBuffer.duration; for (const audioSample of iterator) { diff --git a/src/misc.ts b/src/misc.ts index 57b8896..aa38593 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -636,6 +636,7 @@ export const isWebKit = () => { return isWebKitCache = !!( typeof navigator !== 'undefined' && ( + // eslint-disable-next-line @typescript-eslint/no-deprecated navigator.vendor?.match(/apple/i) // Or, in workers: || (/AppleWebKit/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent)) @@ -661,6 +662,7 @@ export const isChromium = () => { return isChromiumCache = !!( typeof navigator !== 'undefined' + // eslint-disable-next-line @typescript-eslint/no-deprecated && (navigator.vendor?.includes('Google Inc') || /Chrome/.test(navigator.userAgent)) ); }; diff --git a/src/segmented-input.ts b/src/segmented-input.ts index 77f01fe..58b3781 100644 --- a/src/segmented-input.ts +++ b/src/segmented-input.ts @@ -7,7 +7,7 @@ */ import { TrackType } from './output'; -import { AudioCodec, MediaCodec, VideoCodec } from './codec'; +import { MediaCodec } from './codec'; import { Demuxer, DurationMetadataRequestOptions } from './demuxer'; import { Input } from './input'; import { VirtualInputFormat } from './input-format'; @@ -20,8 +20,8 @@ import { InputVideoTrackBacking, } from './input-track'; import { PacketRetrievalOptions } from './media-sink'; -import { MetadataTags, TrackDisposition } from './metadata'; -import { arrayCount, assert, Rotation, roundToDivisor } from './misc'; +import { MetadataTags } from './metadata'; +import { arrayCount, assert, roundToDivisor } from './misc'; import { EncodedPacket } from './packet'; import { NullSource } from './source'; @@ -218,7 +218,7 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking { } getHasOnlyKeyPackets() { - return this.firstInputTrack.hasOnlyKeyPackets; + return this.firstInputTrack.getHasOnlyKeyPackets(); } getId(): number { @@ -233,40 +233,40 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking { return this.number; } - getCodec(): MediaCodec | null { + getCodec() { return this.firstInputTrack._backing.getCodec(); } - getInternalCodecId(): string | number | Uint8Array | null { + getInternalCodecId() { return this.firstInputTrack._backing.getInternalCodecId(); } - getDisposition(): TrackDisposition { + getDisposition() { return this.firstInputTrack._backing.getDisposition(); } - getLanguageCode(): string { + getLanguageCode() { return this.firstInputTrack._backing.getLanguageCode(); } - getName(): string | null { + getName() { return this.firstInputTrack._backing.getName(); } - getTimeResolution(): number { - return this.firstInputTrack._backing.getTimeResolution()!; + getTimeResolution() { + return this.firstInputTrack._backing.getTimeResolution(); } - isRelativeToUnixEpoch(): boolean { + isRelativeToUnixEpoch() { assert(this.demuxer.firstSegment); return this.demuxer.firstSegment.relativeToUnixEpoch; } - getBitrate(): number | null { + getBitrate() { return this.firstInputTrack._backing.getBitrate(); } - getAverageBitrate(): number | null { + getAverageBitrate() { return this.firstInputTrack._backing.getAverageBitrate(); } @@ -289,7 +289,7 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking { const modified = packet.clone({ timestamp: roundToDivisor( packet.timestamp + mediaOffset, - track.timeResolution, + await track.getTimeResolution(), ), // The 1e8 assumes a max of 100 MB per second, highly unlikely to be hit, so this should guarantee // monotonically increasing sequence numbers across segments. @@ -434,28 +434,28 @@ class SegmentedInputInputVideoTrackBacking return 'video' as const; } - override getCodec(): VideoCodec | null { + override getCodec() { return this.firstInputTrack._backing.getCodec(); } - getCodedWidth(): number { - return this.firstInputTrack._backing.getCodedWidth()!; + getCodedWidth() { + return this.firstInputTrack._backing.getCodedWidth(); } - getCodedHeight(): number { - return this.firstInputTrack._backing.getCodedHeight()!; + getCodedHeight() { + return this.firstInputTrack._backing.getCodedHeight(); } - getSquarePixelWidth(): number { - return this.firstInputTrack._backing.getSquarePixelWidth()!; + getSquarePixelWidth() { + return this.firstInputTrack._backing.getSquarePixelWidth(); } - getSquarePixelHeight(): number { - return this.firstInputTrack._backing.getSquarePixelHeight()!; + getSquarePixelHeight() { + return this.firstInputTrack._backing.getSquarePixelHeight(); } - getRotation(): Rotation { - return this.firstInputTrack._backing.getRotation()!; + getRotation() { + return this.firstInputTrack._backing.getRotation(); } getColorSpace(): Promise { @@ -480,16 +480,16 @@ class SegmentedInputInputAudioTrackBacking return 'audio' as const; } - override getCodec(): AudioCodec | null { + override getCodec() { return this.firstInputTrack._backing.getCodec(); } - getNumberOfChannels(): number { - return this.firstInputTrack._backing.getNumberOfChannels()!; + getNumberOfChannels() { + return this.firstInputTrack._backing.getNumberOfChannels(); } - getSampleRate(): number { - return this.firstInputTrack._backing.getSampleRate()!; + getSampleRate() { + return this.firstInputTrack._backing.getSampleRate(); } override getDecoderConfig(): Promise { diff --git a/src/source.ts b/src/source.ts index 11404fc..0ae06d3 100644 --- a/src/source.ts +++ b/src/source.ts @@ -154,6 +154,7 @@ export abstract class Source extends EventEmitter { /** @internal */ _dispatchRead(start: number, end: number) { + // eslint-disable-next-line @typescript-eslint/no-deprecated this.onread?.(start, end); this._emit('read', { start, end }); } diff --git a/src/target.ts b/src/target.ts index ed2f25a..14b83bf 100644 --- a/src/target.ts +++ b/src/target.ts @@ -74,12 +74,14 @@ export abstract class Target extends EventEmitter { /** @internal */ _dispatchWrite(start: number, end: number) { + // eslint-disable-next-line @typescript-eslint/no-deprecated this.onwrite?.(start, end); this._emit('write', { start, end }); } /** @internal */ _dispatchFinalized() { + // eslint-disable-next-line @typescript-eslint/no-deprecated this.onfinalized?.(); this._emit('finalized'); } diff --git a/test/browser/adts-demuxing.test.ts b/test/browser/adts-demuxing.test.ts index db94bad..02905c8 100644 --- a/test/browser/adts-demuxing.test.ts +++ b/test/browser/adts-demuxing.test.ts @@ -16,9 +16,9 @@ test('ADTS demuxing', async () => { const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); - expect(audioTrack.codec).toBe('aac'); - expect(audioTrack.sampleRate).toBe(44100); - expect(audioTrack.numberOfChannels).toBe(2); + expect(await audioTrack.getCodec()).toBe('aac'); + expect(await audioTrack.getSampleRate()).toBe(44100); + expect(await audioTrack.getNumberOfChannels()).toBe(2); const decoderConfig = await audioTrack.getDecoderConfig(); assert(decoderConfig); diff --git a/test/browser/adts-muxing.test.ts b/test/browser/adts-muxing.test.ts index 51f98b1..33e5a5a 100644 --- a/test/browser/adts-muxing.test.ts +++ b/test/browser/adts-muxing.test.ts @@ -79,7 +79,7 @@ test('ADTS with metadata over StreamTarget', async () => { const outputAudioTrack = await outputAsInput.getPrimaryAudioTrack(); assert(outputAudioTrack); - expect(outputAudioTrack.codec).toBe('aac'); + expect(await outputAudioTrack.getCodec()).toBe('aac'); }); // Previously, write handler rejections were silently swallowed and surfaced as diff --git a/test/browser/conversion.test.ts b/test/browser/conversion.test.ts index da0cbfb..d4847d2 100644 --- a/test/browser/conversion.test.ts +++ b/test/browser/conversion.test.ts @@ -17,11 +17,11 @@ test('Rotation is baked-in when rerendering', async () => { const ogTrack = await input.getPrimaryVideoTrack(); assert(ogTrack); - expect(ogTrack.rotation).toBe(90); - expect(ogTrack.codedWidth).toBe(1920); - expect(ogTrack.codedHeight).toBe(1080); - expect(ogTrack.displayWidth).toBe(1080); - expect(ogTrack.displayHeight).toBe(1920); + expect(await ogTrack.getRotation()).toBe(90); + expect(await ogTrack.getCodedWidth()).toBe(1920); + expect(await ogTrack.getCodedHeight()).toBe(1080); + expect(await ogTrack.getDisplayWidth()).toBe(1080); + expect(await ogTrack.getDisplayHeight()).toBe(1920); const output = new Output({ format: new Mp4OutputFormat(), @@ -41,9 +41,9 @@ test('Rotation is baked-in when rerendering', async () => { const track = await newInput.getPrimaryVideoTrack(); assert(track); - expect(track.codedWidth).toBe(320); - expect(track.codedHeight).toBe(570); - expect(track.displayWidth).toBe(320); - expect(track.displayHeight).toBe(570); - expect(track.rotation).toBe(0); + expect(await track.getCodedWidth()).toBe(320); + expect(await track.getCodedHeight()).toBe(570); + expect(await track.getDisplayWidth()).toBe(320); + expect(await track.getDisplayHeight()).toBe(570); + expect(await track.getRotation()).toBe(0); }); diff --git a/test/browser/flac.test.ts b/test/browser/flac.test.ts index ef57adb..c4ccf5d 100644 --- a/test/browser/flac.test.ts +++ b/test/browser/flac.test.ts @@ -62,7 +62,7 @@ test('can convert a .flac to .wav', async () => { IPRD: 'Samples files', ITRK: '4', }); - expect(inputTrack.sampleRate).toBe(outputTrack.sampleRate); - expect(inputTrack.numberOfChannels).toBe(outputTrack.numberOfChannels); - expect(inputTrack.timeResolution).toBe(outputTrack.timeResolution); + expect(await inputTrack.getSampleRate()).toBe(await outputTrack.getSampleRate()); + expect(await inputTrack.getNumberOfChannels()).toBe(await outputTrack.getNumberOfChannels()); + expect(await inputTrack.getTimeResolution()).toBe(await outputTrack.getTimeResolution()); }); diff --git a/test/browser/media-sources.test.ts b/test/browser/media-sources.test.ts index 0a70eb4..316c209 100644 --- a/test/browser/media-sources.test.ts +++ b/test/browser/media-sources.test.ts @@ -106,8 +106,8 @@ test( ); const { input, track } = await readBackTrack(buffer); - expect(track.codedWidth).toBe(100); - expect(track.codedHeight).toBe(100); + expect(await track.getCodedWidth()).toBe(100); + expect(await track.getCodedHeight()).toBe(100); input.dispose(); } }, @@ -120,8 +120,8 @@ test('VideoSampleSource, same-sized frames with width and height set', async () ); const { input, track } = await readBackTrack(buffer); - expect(track.codedWidth).toBe(50); - expect(track.codedHeight).toBe(80); + expect(await track.getCodedWidth()).toBe(50); + expect(await track.getCodedHeight()).toBe(80); input.dispose(); }); @@ -132,8 +132,8 @@ test('VideoSampleSource, same-sized frames with rotation set to 90', async () => ); const { input, track } = await readBackTrack(buffer); - expect(track.codedWidth).toBe(100); - expect(track.codedHeight).toBe(200); + expect(await track.getCodedWidth()).toBe(100); + expect(await track.getCodedHeight()).toBe(200); input.dispose(); }); @@ -144,8 +144,8 @@ test('VideoSampleSource, same-sized frames with rotation, width and height', asy ); const { input, track } = await readBackTrack(buffer); - expect(track.codedWidth).toBe(50); - expect(track.codedHeight).toBe(80); + expect(await track.getCodedWidth()).toBe(50); + expect(await track.getCodedHeight()).toBe(80); input.dispose(); }); @@ -254,8 +254,8 @@ test('VideoSampleSource, transform.process manual resize', async () => { ); const { input, track } = await readBackTrack(buffer); - expect(track.codedWidth).toBe(60); - expect(track.codedHeight).toBe(40); + expect(await track.getCodedWidth()).toBe(60); + expect(await track.getCodedHeight()).toBe(40); input.dispose(); }); @@ -289,8 +289,8 @@ test('VideoSampleSource, transform.process receives pre-transformed frames', asy } const { input, track } = await readBackTrack(buffer); - expect(track.codedWidth).toBe(50); - expect(track.codedHeight).toBe(80); + expect(await track.getCodedWidth()).toBe(50); + expect(await track.getCodedHeight()).toBe(80); input.dispose(); }); @@ -492,8 +492,8 @@ test('VideoSampleSource, transform.frameRate works with process', async () => { } const { input, track } = await readBackTrack(buffer); - expect(track.codedWidth).toBe(60); - expect(track.codedHeight).toBe(40); + expect(await track.getCodedWidth()).toBe(60); + expect(await track.getCodedHeight()).toBe(40); input.dispose(); }); @@ -576,8 +576,8 @@ test('AudioSampleSource, normal usage', async () => { const buffer = await encodeAudio({ codec: 'pcm-s16' }, sample); const { input, track } = await readBackAudioTrack(buffer); - expect(track.numberOfChannels).toBe(2); - expect(track.sampleRate).toBe(48000); + expect(await track.getNumberOfChannels()).toBe(2); + expect(await track.getSampleRate()).toBe(48000); expect(await track.computeDuration()).toBe(1); input.dispose(); }); @@ -590,8 +590,8 @@ test('AudioSampleSource, remixed to mono', async () => { ); const { input, track } = await readBackAudioTrack(buffer); - expect(track.numberOfChannels).toBe(1); - expect(track.sampleRate).toBe(48000); + expect(await track.getNumberOfChannels()).toBe(1); + expect(await track.getSampleRate()).toBe(48000); expect(await track.computeDuration()).toBe(1); input.dispose(); }); @@ -604,8 +604,8 @@ test('AudioSampleSource, resampled to 44100 Hz', async () => { ); const { input, track } = await readBackAudioTrack(buffer); - expect(track.numberOfChannels).toBe(2); - expect(track.sampleRate).toBe(44100); + expect(await track.getNumberOfChannels()).toBe(2); + expect(await track.getSampleRate()).toBe(44100); expect(await track.computeDuration()).toBe(1); input.dispose(); }); @@ -618,8 +618,8 @@ test('AudioSampleSource, resampled stereo with non-zero start timestamp', async ); const { input, track } = await readBackAudioTrack(buffer); - expect(track.numberOfChannels).toBe(2); - expect(track.sampleRate).toBe(48000); + expect(await track.getNumberOfChannels()).toBe(2); + expect(await track.getSampleRate()).toBe(48000); expect(await track.getFirstTimestamp()).toBe(1); expect(await track.computeDuration()).toBe(2); diff --git a/test/browser/mpeg-ts-muxing.test.ts b/test/browser/mpeg-ts-muxing.test.ts index d99848f..2d54762 100644 --- a/test/browser/mpeg-ts-muxing.test.ts +++ b/test/browser/mpeg-ts-muxing.test.ts @@ -119,9 +119,9 @@ test('MPEG-TS muxing with AVC and AAC', async () => { assert(videoTrack); expect(videoTrack.id).toBe(0x100); - expect(videoTrack.codec).toBe('avc'); - expect(videoTrack.displayWidth).toBe(640); - expect(videoTrack.displayHeight).toBe(480); + expect(await videoTrack.getCodec()).toBe('avc'); + expect(await videoTrack.getDisplayWidth()).toBe(640); + expect(await videoTrack.getDisplayHeight()).toBe(480); const videoDecoderConfig = await videoTrack.getDecoderConfig(); assert(videoDecoderConfig); @@ -135,9 +135,9 @@ test('MPEG-TS muxing with AVC and AAC', async () => { assert(audioTrack); expect(audioTrack.id).toBe(0x101); - expect(audioTrack.codec).toBe('aac'); - expect(audioTrack.numberOfChannels).toBe(2); - expect(audioTrack.sampleRate).toBe(48000); + expect(await audioTrack.getCodec()).toBe('aac'); + expect(await audioTrack.getNumberOfChannels()).toBe(2); + expect(await audioTrack.getSampleRate()).toBe(48000); const audioDecoderConfig = await audioTrack.getDecoderConfig(); assert(audioDecoderConfig); @@ -289,7 +289,7 @@ test('MPEG-TS muxing with HEVC and MP3', async () => { assert(videoTrack); expect(videoTrack.id).toBe(0x100); - expect(videoTrack.codec).toBe('hevc'); + expect(await videoTrack.getCodec()).toBe('hevc'); const videoDecoderConfig = await videoTrack.getDecoderConfig(); assert(videoDecoderConfig); @@ -301,9 +301,9 @@ test('MPEG-TS muxing with HEVC and MP3', async () => { assert(audioTrack); expect(audioTrack.id).toBe(0x101); - expect(audioTrack.codec).toBe('mp3'); - expect(audioTrack.numberOfChannels).toBe(2); - expect(audioTrack.sampleRate).toBe(24000); + expect(await audioTrack.getCodec()).toBe('mp3'); + expect(await audioTrack.getNumberOfChannels()).toBe(2); + expect(await audioTrack.getSampleRate()).toBe(24000); const audioDecoderConfig = await audioTrack.getDecoderConfig(); assert(audioDecoderConfig); @@ -407,7 +407,7 @@ test('MPEG-TS muxing with video only', async () => { const videoTrack = await input.getPrimaryVideoTrack(); assert(videoTrack); - expect(videoTrack.codec).toBe('avc'); + expect(await videoTrack.getCodec()).toBe('avc'); const audioTrack = await input.getPrimaryAudioTrack(); expect(audioTrack).toBeNull(); @@ -470,7 +470,7 @@ test('MPEG-TS muxing with audio only', async () => { const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); - expect(audioTrack.codec).toBe('aac'); + expect(await audioTrack.getCodec()).toBe('aac'); const audioSink = new EncodedPacketSink(audioTrack); let audioPacketCount = 0; @@ -531,8 +531,8 @@ test('MPEG-TS muxing with two video tracks', async () => { const videoTracks = tracks.filter(t => t.type === 'video'); expect(videoTracks.length).toBe(2); - expect(videoTracks[0]!.codec).toBe('hevc'); - expect(videoTracks[1]!.codec).toBe('hevc'); + expect(await videoTracks[0]!.getCodec()).toBe('hevc'); + expect(await videoTracks[1]!.getCodec()).toBe('hevc'); expect(videoTracks[0]!.id).toBe(0x100); expect(videoTracks[1]!.id).toBe(0x101); }); @@ -599,8 +599,8 @@ test('MPEG-TS muxing with two audio tracks', async () => { const audioTracks = tracks.filter(t => t.type === 'audio'); expect(audioTracks.length).toBe(2); - expect(audioTracks[0]!.codec).toBe('aac'); - expect(audioTracks[1]!.codec).toBe('aac'); + expect(await audioTracks[0]!.getCodec()).toBe('aac'); + expect(await audioTracks[1]!.getCodec()).toBe('aac'); expect(audioTracks[0]!.id).toBe(0x100); expect(audioTracks[1]!.id).toBe(0x101); }); @@ -646,8 +646,8 @@ test('MPEG-TS transmux (Annex B and ADTS passthrough)', async () => { assert(outputAudioTrack); // Codecs should match - expect(outputVideoTrack.codec).toBe(inputVideoTrack.codec); - expect(outputAudioTrack.codec).toBe(inputAudioTrack.codec); + expect(await outputVideoTrack.getCodec()).toBe(await inputVideoTrack.getCodec()); + expect(await outputAudioTrack.getCodec()).toBe(await inputAudioTrack.getCodec()); // Verify video packets are Annex B const videoSink = new EncodedPacketSink(outputVideoTrack); @@ -728,7 +728,7 @@ test('MPEG-TS muxing with StreamTarget', async () => { const videoTrack = await input.getPrimaryVideoTrack(); assert(videoTrack); - expect(videoTrack.codec).toBe('avc'); + expect(await videoTrack.getCodec()).toBe('avc'); const videoSink = new EncodedPacketSink(videoTrack); let videoPacketCount = 0; diff --git a/test/browser/par.test.ts b/test/browser/par.test.ts index 0f04c53..edf7600 100644 --- a/test/browser/par.test.ts +++ b/test/browser/par.test.ts @@ -3,7 +3,7 @@ import { Conversion } from '../../src/conversion.js'; import { ALL_FORMATS, MATROSKA, MP4, MPEG_TS } from '../../src/input-format.js'; import { Input } from '../../src/input.js'; import { VideoSampleSink } from '../../src/media-sink.js'; -import { assert } from '../../src/misc.js'; +import { assert, Rational } from '../../src/misc.js'; import { Output } from '../../src/output.js'; import { MkvOutputFormat, Mp4OutputFormat, MpegTsOutputFormat } from '../../src/output-format.js'; import { BufferSource, UrlSource } from '../../src/source.js'; @@ -22,13 +22,13 @@ test('Pixel aspect ratio reading', async () => { const videoTrack = await input.getPrimaryVideoTrack(); assert(videoTrack); - expect(videoTrack.rotation).toBe(0); - expectPar2x1Geometry(videoTrack); + expect(await videoTrack.getRotation()).toBe(0); + expectPar2x1Geometry(await snapshotTrack(videoTrack)); const decoderConfig = await videoTrack.getDecoderConfig(); assert(decoderConfig); - expect(decoderConfig.displayAspectWidth).toBe(videoTrack.squarePixelWidth); - expect(decoderConfig.displayAspectHeight).toBe(videoTrack.squarePixelHeight); + expect(decoderConfig.displayAspectWidth).toBe(await videoTrack.getSquarePixelWidth()); + expect(decoderConfig.displayAspectHeight).toBe(await videoTrack.getSquarePixelHeight()); const sink = new VideoSampleSink(videoTrack); using sample = (await sink.getSample(await videoTrack.getFirstTimestamp()))!; @@ -36,11 +36,11 @@ test('Pixel aspect ratio reading', async () => { expect(sample.rotation).toBe(0); expect(sample.visibleRect.width).toBe(sample.codedWidth); expect(sample.visibleRect.height).toBe(sample.codedHeight); - expect(sample.codedWidth).toBe(videoTrack.codedWidth); - expect(sample.codedHeight).toBe(videoTrack.codedHeight); + expect(sample.codedWidth).toBe(await videoTrack.getCodedWidth()); + expect(sample.codedHeight).toBe(await videoTrack.getCodedHeight()); expectPar2x1Geometry(sample); - expect(sample.squarePixelWidth).toBe(videoTrack.squarePixelWidth); - expect(sample.squarePixelHeight).toBe(videoTrack.squarePixelHeight); + expect(sample.squarePixelWidth).toBe(await videoTrack.getSquarePixelWidth()); + expect(sample.squarePixelHeight).toBe(await videoTrack.getSquarePixelHeight()); }); test('Pixel aspect ratio copy conversion', async () => { @@ -103,7 +103,7 @@ const expectPar2x1Geometry = (value: { squarePixelHeight: number; displayWidth: number; displayHeight: number; - pixelAspectRatio: { num: number; den: number }; + pixelAspectRatio: Rational; }) => { expect(value.pixelAspectRatio).toEqual({ num: 2, den: 1 }); expect(value.squarePixelWidth).toBe(value.codedWidth * 2); @@ -113,28 +113,48 @@ const expectPar2x1Geometry = (value: { }; const snapshotTrack = async (track: { - codedWidth: number; - codedHeight: number; - squarePixelWidth: number; - squarePixelHeight: number; - displayWidth: number; - displayHeight: number; - rotation: number; - pixelAspectRatio: { num: number; den: number }; + getCodedWidth(): Promise; + getCodedHeight(): Promise; + getSquarePixelWidth(): Promise; + getSquarePixelHeight(): Promise; + getDisplayWidth(): Promise; + getDisplayHeight(): Promise; + getRotation(): Promise; + getPixelAspectRatio(): Promise; getDecoderConfig(): Promise; }) => { - const decoderConfig = await track.getDecoderConfig(); + const [ + decoderConfig, + pixelAspectRatio, + codedWidth, + codedHeight, + squarePixelWidth, + squarePixelHeight, + displayWidth, + displayHeight, + rotation, + ] = await Promise.all([ + track.getDecoderConfig(), + track.getPixelAspectRatio(), + track.getCodedWidth(), + track.getCodedHeight(), + track.getSquarePixelWidth(), + track.getSquarePixelHeight(), + track.getDisplayWidth(), + track.getDisplayHeight(), + track.getRotation(), + ]); assert(decoderConfig); return { - codedWidth: track.codedWidth, - codedHeight: track.codedHeight, - squarePixelWidth: track.squarePixelWidth, - squarePixelHeight: track.squarePixelHeight, - displayWidth: track.displayWidth, - displayHeight: track.displayHeight, - rotation: track.rotation, - pixelAspectRatio: track.pixelAspectRatio, + codedWidth, + codedHeight, + squarePixelWidth, + squarePixelHeight, + displayWidth, + displayHeight, + rotation, + pixelAspectRatio, decoderDisplayAspectWidth: decoderConfig.displayAspectWidth, decoderDisplayAspectHeight: decoderConfig.displayAspectHeight, }; diff --git a/test/browser/transparency.test.ts b/test/browser/transparency.test.ts index 1469732..22ca520 100644 --- a/test/browser/transparency.test.ts +++ b/test/browser/transparency.test.ts @@ -367,7 +367,7 @@ test.skip('Can reencode transparent video, keeping alpha', async () => { const videoTrack = (await outputInput.getPrimaryVideoTrack())!; expect(await videoTrack.canBeTransparent()).toBe(true); - expect(videoTrack.displayWidth).toBe(320); + expect(await videoTrack.getDisplayWidth()).toBe(320); const sink = new VideoSampleSink(videoTrack); using sample = (await sink.getSample(await videoTrack.getFirstTimestamp()))!; diff --git a/test/node/aac-encoder-extension.test.ts b/test/node/aac-encoder-extension.test.ts index 9a1bb0c..c569356 100644 --- a/test/node/aac-encoder-extension.test.ts +++ b/test/node/aac-encoder-extension.test.ts @@ -66,9 +66,9 @@ test('AAC encoding', async () => { }); const track = (await input.getPrimaryAudioTrack())!; - expect(track.codec).toBe('aac'); - expect(track.sampleRate).toBe(sampleRate); - expect(track.numberOfChannels).toBe(channels); + expect(await track.getCodec()).toBe('aac'); + expect(await track.getSampleRate()).toBe(sampleRate); + expect(await track.getNumberOfChannels()).toBe(channels); const sink = new EncodedPacketSink(track); let packetCount = 0; diff --git a/test/node/ac3.test.ts b/test/node/ac3.test.ts index 0989b89..f3fd262 100644 --- a/test/node/ac3.test.ts +++ b/test/node/ac3.test.ts @@ -26,7 +26,7 @@ test('reads AC-3 from MP4', async () => { const audioTrack = (await input.getPrimaryAudioTrack())!; const decoderConfig = (await audioTrack.getDecoderConfig())!; - expect(audioTrack.codec).toBe('ac3'); + expect(await audioTrack.getCodec()).toBe('ac3'); expect(decoderConfig.description).toBeUndefined(); }); @@ -39,7 +39,7 @@ test('reads E-AC-3 from MP4', async () => { const audioTrack = (await input.getPrimaryAudioTrack())!; const decoderConfig = (await audioTrack.getDecoderConfig())!; - expect(audioTrack.codec).toBe('eac3'); + expect(await audioTrack.getCodec()).toBe('eac3'); expect(decoderConfig.description).toBeUndefined(); }); @@ -51,8 +51,8 @@ test('reads and writes AC-3 in MPEG-TS', async () => { const originalAudioTrack = (await originalInput.getPrimaryAudioTrack())!; - expect(originalAudioTrack.codec).toBe('ac3'); - expect(originalAudioTrack.internalCodecId).toBe(0x81); + expect(await originalAudioTrack.getCodec()).toBe('ac3'); + expect(await originalAudioTrack.getInternalCodecId()).toBe(0x81); const output = new Output({ format: new MpegTsOutputFormat(), @@ -69,10 +69,10 @@ test('reads and writes AC-3 in MPEG-TS', async () => { const newAudioTrack = (await newInput.getPrimaryAudioTrack())!; - expect(newAudioTrack.codec).toBe('ac3'); - expect(newAudioTrack.internalCodecId).toBe(0x81); - expect(newAudioTrack.numberOfChannels).toBe(originalAudioTrack.numberOfChannels); - expect(newAudioTrack.sampleRate).toBe(originalAudioTrack.sampleRate); + expect(await newAudioTrack.getCodec()).toBe('ac3'); + expect(await newAudioTrack.getInternalCodecId()).toBe(0x81); + expect(await newAudioTrack.getNumberOfChannels()).toBe(await originalAudioTrack.getNumberOfChannels()); + expect(await newAudioTrack.getSampleRate()).toBe(await originalAudioTrack.getSampleRate()); // Verify registration_descriptor is present const buffer = new Uint8Array(output.target.buffer!); @@ -94,8 +94,8 @@ test('reads and writes E-AC-3 in MPEG-TS', async () => { const originalAudioTrack = (await originalInput.getPrimaryAudioTrack())!; - expect(originalAudioTrack.codec).toBe('eac3'); - expect(originalAudioTrack.internalCodecId).toBe(0x87); + expect(await originalAudioTrack.getCodec()).toBe('eac3'); + expect(await originalAudioTrack.getInternalCodecId()).toBe(0x87); const output = new Output({ format: new MpegTsOutputFormat(), @@ -112,10 +112,10 @@ test('reads and writes E-AC-3 in MPEG-TS', async () => { const newAudioTrack = (await newInput.getPrimaryAudioTrack())!; - expect(newAudioTrack.codec).toBe('eac3'); - expect(newAudioTrack.internalCodecId).toBe(0x87); - expect(newAudioTrack.numberOfChannels).toBe(originalAudioTrack.numberOfChannels); - expect(newAudioTrack.sampleRate).toBe(originalAudioTrack.sampleRate); + expect(await newAudioTrack.getCodec()).toBe('eac3'); + expect(await newAudioTrack.getInternalCodecId()).toBe(0x87); + expect(await newAudioTrack.getNumberOfChannels()).toBe(await originalAudioTrack.getNumberOfChannels()); + expect(await newAudioTrack.getSampleRate()).toBe(await originalAudioTrack.getSampleRate()); // Verify registration_descriptor is present const buffer = new Uint8Array(output.target.buffer!); @@ -168,6 +168,8 @@ test('AC-3 decoding', async () => { const track = (await input.getPrimaryAudioTrack())!; const { packetCount } = await track.computePacketStats(); + const trackNumberOfChannels = await track.getNumberOfChannels(); + const trackSampleRate = await track.getSampleRate(); const sink = new AudioSampleSink(track); let sampleCount = 0; @@ -177,8 +179,8 @@ test('AC-3 decoding', async () => { expect(sample.timestamp).toBeCloseTo(nextTimestamp); expect(sample.duration).toBe(0.032); expect(sample.format).toBe('f32-planar'); - expect(sample.numberOfChannels).toBe(track.numberOfChannels); - expect(sample.sampleRate).toBe(track.sampleRate); + expect(sample.numberOfChannels).toBe(trackNumberOfChannels); + expect(sample.sampleRate).toBe(trackSampleRate); nextTimestamp += sample.duration; sampleCount++; @@ -197,6 +199,8 @@ test('E-AC-3 decoding', async () => { const track = (await input.getPrimaryAudioTrack())!; const { packetCount } = await track.computePacketStats(); + const trackNumberOfChannels = await track.getNumberOfChannels(); + const trackSampleRate = await track.getSampleRate(); const sink = new AudioSampleSink(track); let sampleCount = 0; @@ -206,8 +210,8 @@ test('E-AC-3 decoding', async () => { expect(sample.timestamp).toBeCloseTo(nextTimestamp); expect(sample.duration).toBe(0.032); expect(sample.format).toBe('f32-planar'); - expect(sample.numberOfChannels).toBe(track.numberOfChannels); - expect(sample.sampleRate).toBe(track.sampleRate); + expect(sample.numberOfChannels).toBe(trackNumberOfChannels); + expect(sample.sampleRate).toBe(trackSampleRate); nextTimestamp += sample.duration; sampleCount++; @@ -263,9 +267,9 @@ test('AC-3 encoding', async () => { }); const track = (await input.getPrimaryAudioTrack())!; - expect(track.codec).toBe('ac3'); - expect(track.sampleRate).toBe(sampleRate); - expect(track.numberOfChannels).toBe(channels); + expect(await track.getCodec()).toBe('ac3'); + expect(await track.getSampleRate()).toBe(sampleRate); + expect(await track.getNumberOfChannels()).toBe(channels); const sink = new EncodedPacketSink(track); let packetCount = 0; @@ -311,9 +315,9 @@ test('E-AC-3 encoding', async () => { }); const track = (await input.getPrimaryAudioTrack())!; - expect(track.codec).toBe('eac3'); - expect(track.sampleRate).toBe(sampleRate); - expect(track.numberOfChannels).toBe(channels); + expect(await track.getCodec()).toBe('eac3'); + expect(await track.getSampleRate()).toBe(sampleRate); + expect(await track.getNumberOfChannels()).toBe(channels); const sink = new EncodedPacketSink(track); let packetCount = 0; diff --git a/test/node/adts-muxer.test.ts b/test/node/adts-muxer.test.ts index d155414..bb06377 100644 --- a/test/node/adts-muxer.test.ts +++ b/test/node/adts-muxer.test.ts @@ -42,9 +42,9 @@ test('ADTS muxer with raw AAC input', async () => { const outputTrack = await outputAsInput.getPrimaryAudioTrack(); assert(outputTrack); - expect(outputTrack.codec).toBe('aac'); - expect(outputTrack.sampleRate).toBe(audioTrack.sampleRate); - expect(outputTrack.numberOfChannels).toBe(audioTrack.numberOfChannels); + expect(await outputTrack.getCodec()).toBe('aac'); + expect(await outputTrack.getSampleRate()).toBe(await audioTrack.getSampleRate()); + expect(await outputTrack.getNumberOfChannels()).toBe(await audioTrack.getNumberOfChannels()); const outputDecoderConfig = await outputTrack.getDecoderConfig(); expect(outputDecoderConfig!.description).toBeUndefined(); // ADTS has no description diff --git a/test/node/annex-b-conversion.test.ts b/test/node/annex-b-conversion.test.ts index cd35f8b..43561c4 100644 --- a/test/node/annex-b-conversion.test.ts +++ b/test/node/annex-b-conversion.test.ts @@ -20,7 +20,7 @@ test('Annex B to length-prefixed conversion, MP4', async () => { const originalVideoTrack = (await originalInput.getPrimaryVideoTrack())!; const originalDecoderConfig = (await originalVideoTrack.getDecoderConfig())!; expect(originalDecoderConfig.description).toBeUndefined(); - expect(originalVideoTrack.codec).toBe('avc'); + expect(await originalVideoTrack.getCodec()).toBe('avc'); const originalSink = new EncodedPacketSink(originalVideoTrack); const originalFirstPacket = await originalSink.getFirstPacket(); @@ -44,7 +44,7 @@ test('Annex B to length-prefixed conversion, MP4', async () => { const newVideoTrack = (await newInput.getPrimaryVideoTrack())!; const newDecoderConfig = (await newVideoTrack.getDecoderConfig())!; expect(newDecoderConfig.description).toBeDefined(); - expect(newVideoTrack.codec).toBe('avc'); + expect(await newVideoTrack.getCodec()).toBe('avc'); const newSink = new EncodedPacketSink(newVideoTrack); const newFirstPacket = await newSink.getFirstPacket(); diff --git a/test/node/disposition.test.ts b/test/node/disposition.test.ts index faa4871..f0caab0 100644 --- a/test/node/disposition.test.ts +++ b/test/node/disposition.test.ts @@ -35,7 +35,7 @@ test('Default track disposition', async () => { const track = (await input.getPrimaryVideoTrack())!; - expect(track.disposition).toEqual({ + expect(await track.getDisposition()).toEqual({ default: true, forced: false, original: false, @@ -82,7 +82,7 @@ test('Customized track disposition', async () => { const track = (await input.getPrimaryVideoTrack())!; - expect(track.disposition).toEqual({ + expect(await track.getDisposition()).toEqual({ default: false, forced: true, original: true, diff --git a/test/node/flac-encoder-extension.test.ts b/test/node/flac-encoder-extension.test.ts index 6d7dc85..2c785dd 100644 --- a/test/node/flac-encoder-extension.test.ts +++ b/test/node/flac-encoder-extension.test.ts @@ -66,9 +66,9 @@ test('FLAC encoding', async () => { }); const track = (await input.getPrimaryAudioTrack())!; - expect(track.codec).toBe('flac'); - expect(track.sampleRate).toBe(sampleRate); - expect(track.numberOfChannels).toBe(channels); + expect(await track.getCodec()).toBe('flac'); + expect(await track.getSampleRate()).toBe(sampleRate); + expect(await track.getNumberOfChannels()).toBe(channels); const sink = new EncodedPacketSink(track); let packetCount = 0; diff --git a/test/node/flac.test.ts b/test/node/flac.test.ts index f00befd..df3742c 100644 --- a/test/node/flac.test.ts +++ b/test/node/flac.test.ts @@ -33,7 +33,7 @@ test('can loop over all samples', async () => { ]), }); expect(await track.getCodecParameterString()).toEqual('flac'); - expect(track.timeResolution).toEqual(44100); + expect(await track.getTimeResolution()).toEqual(44100); expect(await input.getMimeType()).toEqual('audio/flac'); const sink = new EncodedPacketSink(track); @@ -179,9 +179,9 @@ test('can re-mux a .flac', async () => { const inputTrack = await input.getPrimaryAudioTrack(); assert(inputTrack); - expect(inputTrack.sampleRate).toBe(outputTrack.sampleRate); - expect(inputTrack.numberOfChannels).toBe(outputTrack.numberOfChannels); - expect(inputTrack.timeResolution).toBe(outputTrack.timeResolution); + expect(await inputTrack.getSampleRate()).toBe(await outputTrack.getSampleRate()); + expect(await inputTrack.getNumberOfChannels()).toBe(await outputTrack.getNumberOfChannels()); + expect(await inputTrack.getTimeResolution()).toBe(await outputTrack.getTimeResolution()); const outputMetadataTags = await outputAsInput.getMetadataTags(); diff --git a/test/node/hls-input.test.ts b/test/node/hls-input.test.ts index 688fb40..ae31bd1 100644 --- a/test/node/hls-input.test.ts +++ b/test/node/hls-input.test.ts @@ -22,109 +22,95 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(sourceCount).toBe(1); expect(rootReadCount).toBeGreaterThan(0); - // Test descriptors (unhydrated metadata from master playlist) - const descriptors = await input.getTrackDescriptors(); - const videoDescriptors = descriptors.filter(x => x.isVideoTrackDescriptor()); - const audioDescriptors = descriptors.filter(x => x.isAudioTrackDescriptor()); + // Test tracks directly (metadata comes from the master playlist before segments are read) + const tracks = await input.getTracks(); + const videoTracks = tracks.filter((x): x is InputVideoTrack => x.isVideoTrack()); + const audioTracks = tracks.filter((x): x is InputAudioTrack => x.isAudioTrack()); - expect(videoDescriptors).toHaveLength(5); - expect(audioDescriptors).toHaveLength(5); + expect(videoTracks).toHaveLength(5); + expect(audioTracks).toHaveLength(5); - expect(videoDescriptors[0]!.codec).toBe('avc'); - expect(videoDescriptors[0]!.codecParameterString).toBe('avc1.64001f'); - expect(videoDescriptors[0]!.displayWidth).toBe(1280); - expect(videoDescriptors[0]!.displayHeight).toBe(720); - expect(videoDescriptors[0]!.bitrate).toBe(2149280); - expect(videoDescriptors[0]!.name).toBe('720'); - expect(videoDescriptors[0]!.id).toBe(2); + expect(await videoTracks[0]!.getCodec()).toBe('avc'); + expect(await videoTracks[0]!.getDisplayWidth()).toBe(1280); + expect(await videoTracks[0]!.getDisplayHeight()).toBe(720); + expect(await videoTracks[0]!.getBitrate()).toBe(2149280); + expect(await videoTracks[0]!.getName()).toBe('720'); + expect(videoTracks[0]!.id).toBe(2); - expect(videoDescriptors[1]!.codec).toBe('avc'); - expect(videoDescriptors[1]!.codecParameterString).toBe('avc1.42000d'); - expect(videoDescriptors[1]!.displayWidth).toBe(320); - expect(videoDescriptors[1]!.displayHeight).toBe(184); - expect(videoDescriptors[1]!.bitrate).toBe(246440); - expect(videoDescriptors[1]!.name).toBe('240'); - expect(videoDescriptors[1]!.id).toBe(4); + expect(await videoTracks[1]!.getCodec()).toBe('avc'); + expect(await videoTracks[1]!.getDisplayWidth()).toBe(320); + expect(await videoTracks[1]!.getDisplayHeight()).toBe(184); + expect(await videoTracks[1]!.getBitrate()).toBe(246440); + expect(await videoTracks[1]!.getName()).toBe('240'); + expect(videoTracks[1]!.id).toBe(4); - expect(videoDescriptors[2]!.codec).toBe('avc'); - expect(videoDescriptors[2]!.codecParameterString).toBe('avc1.420016'); - expect(videoDescriptors[2]!.displayWidth).toBe(512); - expect(videoDescriptors[2]!.displayHeight).toBe(288); - expect(videoDescriptors[2]!.bitrate).toBe(460560); - expect(videoDescriptors[2]!.name).toBe('380'); - expect(videoDescriptors[2]!.id).toBe(6); + expect(await videoTracks[2]!.getCodec()).toBe('avc'); + expect(await videoTracks[2]!.getDisplayWidth()).toBe(512); + expect(await videoTracks[2]!.getDisplayHeight()).toBe(288); + expect(await videoTracks[2]!.getBitrate()).toBe(460560); + expect(await videoTracks[2]!.getName()).toBe('380'); + expect(videoTracks[2]!.id).toBe(6); - expect(videoDescriptors[3]!.codec).toBe('avc'); - expect(videoDescriptors[3]!.codecParameterString).toBe('avc1.64001f'); - expect(videoDescriptors[3]!.displayWidth).toBe(848); - expect(videoDescriptors[3]!.displayHeight).toBe(480); - expect(videoDescriptors[3]!.bitrate).toBe(836280); - expect(videoDescriptors[3]!.name).toBe('480'); - expect(videoDescriptors[3]!.id).toBe(8); + expect(await videoTracks[3]!.getCodec()).toBe('avc'); + expect(await videoTracks[3]!.getDisplayWidth()).toBe(848); + expect(await videoTracks[3]!.getDisplayHeight()).toBe(480); + expect(await videoTracks[3]!.getBitrate()).toBe(836280); + expect(await videoTracks[3]!.getName()).toBe('480'); + expect(videoTracks[3]!.id).toBe(8); - expect(videoDescriptors[4]!.codec).toBe('avc'); - expect(videoDescriptors[4]!.codecParameterString).toBe('avc1.640028'); - expect(videoDescriptors[4]!.displayWidth).toBe(1920); - expect(videoDescriptors[4]!.displayHeight).toBe(1080); - expect(videoDescriptors[4]!.bitrate).toBe(6221600); - expect(videoDescriptors[4]!.name).toBe('1080'); - expect(videoDescriptors[4]!.id).toBe(10); + expect(await videoTracks[4]!.getCodec()).toBe('avc'); + expect(await videoTracks[4]!.getDisplayWidth()).toBe(1920); + expect(await videoTracks[4]!.getDisplayHeight()).toBe(1080); + expect(await videoTracks[4]!.getBitrate()).toBe(6221600); + expect(await videoTracks[4]!.getName()).toBe('1080'); + expect(videoTracks[4]!.id).toBe(10); - expect(audioDescriptors[0]!.codec).toBe('aac'); - expect(audioDescriptors[0]!.codecParameterString).toBe('mp4a.40.2'); - expect(audioDescriptors[0]!.bitrate).toBe(2149280); - expect(audioDescriptors[0]!.name).toBe('720'); - expect(audioDescriptors[0]!.id).toBe(1); - expect(audioDescriptors[0]!.numberOfChannels).toBeUndefined(); - expect(audioDescriptors[0]!.sampleRate).toBeUndefined(); + expect(await audioTracks[0]!.getCodec()).toBe('aac'); + expect(await audioTracks[0]!.getBitrate()).toBe(2149280); + expect(await audioTracks[0]!.getName()).toBe('720'); + expect(audioTracks[0]!.id).toBe(1); - expect(audioDescriptors[1]!.codec).toBe('aac'); - expect(audioDescriptors[1]!.codecParameterString).toBe('mp4a.40.5'); - expect(audioDescriptors[1]!.bitrate).toBe(246440); - expect(audioDescriptors[1]!.name).toBe('240'); - expect(audioDescriptors[1]!.id).toBe(3); + expect(await audioTracks[1]!.getCodec()).toBe('aac'); + expect(await audioTracks[1]!.getBitrate()).toBe(246440); + expect(await audioTracks[1]!.getName()).toBe('240'); + expect(audioTracks[1]!.id).toBe(3); - expect(audioDescriptors[2]!.codec).toBe('aac'); - expect(audioDescriptors[2]!.codecParameterString).toBe('mp4a.40.5'); - expect(audioDescriptors[2]!.bitrate).toBe(460560); - expect(audioDescriptors[2]!.name).toBe('380'); - expect(audioDescriptors[2]!.id).toBe(5); + expect(await audioTracks[2]!.getCodec()).toBe('aac'); + expect(await audioTracks[2]!.getBitrate()).toBe(460560); + expect(await audioTracks[2]!.getName()).toBe('380'); + expect(audioTracks[2]!.id).toBe(5); - expect(audioDescriptors[3]!.codec).toBe('aac'); - expect(audioDescriptors[3]!.codecParameterString).toBe('mp4a.40.2'); - expect(audioDescriptors[3]!.bitrate).toBe(836280); - expect(audioDescriptors[3]!.name).toBe('480'); - expect(audioDescriptors[3]!.id).toBe(7); + expect(await audioTracks[3]!.getCodec()).toBe('aac'); + expect(await audioTracks[3]!.getBitrate()).toBe(836280); + expect(await audioTracks[3]!.getName()).toBe('480'); + expect(audioTracks[3]!.id).toBe(7); - expect(audioDescriptors[4]!.codec).toBe('aac'); - expect(audioDescriptors[4]!.codecParameterString).toBe('mp4a.40.2'); - expect(audioDescriptors[4]!.bitrate).toBe(6221600); - expect(audioDescriptors[4]!.name).toBe('1080'); - expect(audioDescriptors[4]!.id).toBe(9); + expect(await audioTracks[4]!.getCodec()).toBe('aac'); + expect(await audioTracks[4]!.getBitrate()).toBe(6221600); + expect(await audioTracks[4]!.getName()).toBe('1080'); + expect(audioTracks[4]!.id).toBe(9); - for (let i = 0; i < videoDescriptors.length - 1; i++) { - for (let j = i + 1; j < videoDescriptors.length; j++) { - expect(videoDescriptors[i]!.canBePairedWith(videoDescriptors[j]!)).toBe(false); + for (let i = 0; i < videoTracks.length - 1; i++) { + for (let j = i + 1; j < videoTracks.length; j++) { + expect(videoTracks[i]!.canBePairedWith(videoTracks[j]!)).toBe(false); } } - for (let i = 0; i < videoDescriptors.length; i++) { - for (let j = 0; j < audioDescriptors.length; j++) { - expect(videoDescriptors[i]!.canBePairedWith(audioDescriptors[j]!)).toBe(i === j); + for (let i = 0; i < videoTracks.length; i++) { + for (let j = 0; j < audioTracks.length; j++) { + expect(videoTracks[i]!.canBePairedWith(audioTracks[j]!)).toBe(i === j); } } expect(sourceCount).toBe(1); - // Hydrate all tracks - const tracks = await input.getTracks(); - const videoTracks = tracks.filter(x => x.isVideoTrack()); - const audioTracks = tracks.filter(x => x.isAudioTrack()); + // Force hydration of all tracks by loading actual media data + for (const track of tracks) { + expect(await track.getIsRelativeToUnixEpoch()).toBe(false); + } expect(sourceCount).toBe(1 + 5 + 5); - expect(tracks.every(x => !x.isRelativeToUnixEpoch)).toBe(true); - for (const track of tracks) { expect(await track.isLive()).toBe(false); } @@ -132,28 +118,28 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(await videoTracks[0]!.getDurationFromMetadata()).toBe(634.584); expect(await audioTracks[0]!.getDurationFromMetadata()).toBe(634.584); - expect(videoTracks[0]!.codedWidth).toBe(1280); - expect(videoTracks[0]!.codedHeight).toBe(720); - expect(videoTracks[1]!.codedWidth).toBe(320); - expect(videoTracks[1]!.codedHeight).toBe(184); - expect(videoTracks[2]!.codedWidth).toBe(512); - expect(videoTracks[2]!.codedHeight).toBe(288); - expect(videoTracks[3]!.codedWidth).toBe(848); - expect(videoTracks[3]!.codedHeight).toBe(480); - expect(videoTracks[4]!.codedWidth).toBe(1920); - expect(videoTracks[4]!.codedHeight).toBe(1080); + expect(await videoTracks[0]!.getCodedWidth()).toBe(1280); + expect(await videoTracks[0]!.getCodedHeight()).toBe(720); + expect(await videoTracks[1]!.getCodedWidth()).toBe(320); + expect(await videoTracks[1]!.getCodedHeight()).toBe(184); + expect(await videoTracks[2]!.getCodedWidth()).toBe(512); + expect(await videoTracks[2]!.getCodedHeight()).toBe(288); + expect(await videoTracks[3]!.getCodedWidth()).toBe(848); + expect(await videoTracks[3]!.getCodedHeight()).toBe(480); + expect(await videoTracks[4]!.getCodedWidth()).toBe(1920); + expect(await videoTracks[4]!.getCodedHeight()).toBe(1080); - // Ensure that these are still avaiable even after track backing hydration - expect(videoDescriptors[0]!.displayWidth).toBe(1280); - expect(videoDescriptors[0]!.displayHeight).toBe(720); - expect(videoDescriptors[1]!.displayWidth).toBe(320); - expect(videoDescriptors[1]!.displayHeight).toBe(184); - expect(videoDescriptors[2]!.displayWidth).toBe(512); - expect(videoDescriptors[2]!.displayHeight).toBe(288); - expect(videoDescriptors[3]!.displayWidth).toBe(848); - expect(videoDescriptors[3]!.displayHeight).toBe(480); - expect(videoDescriptors[4]!.displayWidth).toBe(1920); - expect(videoDescriptors[4]!.displayHeight).toBe(1080); + // Ensure metadata display dimensions still match even after hydration (they come from the manifest hint) + expect(await videoTracks[0]!.getDisplayWidth()).toBe(1280); + expect(await videoTracks[0]!.getDisplayHeight()).toBe(720); + expect(await videoTracks[1]!.getDisplayWidth()).toBe(320); + expect(await videoTracks[1]!.getDisplayHeight()).toBe(184); + expect(await videoTracks[2]!.getDisplayWidth()).toBe(512); + expect(await videoTracks[2]!.getDisplayHeight()).toBe(288); + expect(await videoTracks[3]!.getDisplayWidth()).toBe(848); + expect(await videoTracks[3]!.getDisplayHeight()).toBe(480); + expect(await videoTracks[4]!.getDisplayWidth()).toBe(1920); + expect(await videoTracks[4]!.getDisplayHeight()).toBe(1080); expect(await videoTracks[0]!.getCodecParameterString()).toEqual('avc1.64001f'); expect(await videoTracks[1]!.getCodecParameterString()).toEqual('avc1.42c00d'); // Slightly altered @@ -161,16 +147,16 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(await videoTracks[3]!.getCodecParameterString()).toEqual('avc1.64001f'); expect(await videoTracks[4]!.getCodecParameterString()).toEqual('avc1.640028'); - expect(audioTracks[0]!.numberOfChannels).toBe(2); - expect(audioTracks[0]!.sampleRate).toBe(44100); - expect(audioTracks[1]!.numberOfChannels).toBe(2); - expect(audioTracks[1]!.sampleRate).toBe(22050); - expect(audioTracks[2]!.numberOfChannels).toBe(2); - expect(audioTracks[2]!.sampleRate).toBe(22050); - expect(audioTracks[3]!.numberOfChannels).toBe(2); - expect(audioTracks[3]!.sampleRate).toBe(44100); - expect(audioTracks[4]!.numberOfChannels).toBe(2); - expect(audioTracks[4]!.sampleRate).toBe(44100); + expect(await audioTracks[0]!.getNumberOfChannels()).toBe(2); + expect(await audioTracks[0]!.getSampleRate()).toBe(44100); + expect(await audioTracks[1]!.getNumberOfChannels()).toBe(2); + expect(await audioTracks[1]!.getSampleRate()).toBe(22050); + expect(await audioTracks[2]!.getNumberOfChannels()).toBe(2); + expect(await audioTracks[2]!.getSampleRate()).toBe(22050); + expect(await audioTracks[3]!.getNumberOfChannels()).toBe(2); + expect(await audioTracks[3]!.getSampleRate()).toBe(44100); + expect(await audioTracks[4]!.getNumberOfChannels()).toBe(2); + expect(await audioTracks[4]!.getSampleRate()).toBe(44100); for (const audioTrack of audioTracks) { // Actual data always contains object type 2 @@ -218,6 +204,30 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(await input.getDurationFromMetadata()).not.toBe(null); }); +test.concurrent('Big Buck Bunny codec parameter strings from master playlist', { timeout: 15_000 }, async () => { + using input = createInputFrom('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', ALL_FORMATS); + + let sourceCount = 0; + input.on('source', () => sourceCount++); + + const tracks = await input.getTracks(); + expect(tracks).toHaveLength(10); + + // Every CODECS attribute lives in the master playlist, so reading the codec parameter string should never force us + // to fetch any segment source + const codecParameterStrings = await Promise.all(tracks.map(t => t.getCodecParameterString())); + expect(codecParameterStrings).toEqual(expect.arrayContaining([ + 'mp4a.40.2', + 'mp4a.40.5', + 'avc1.64001f', + 'avc1.42000d', + 'avc1.420016', + 'avc1.640028', + ])); + + expect(sourceCount).toBe(1); +}); + test.concurrent('Single-variant Big Buck Bunny', { timeout: 15_000 }, async () => { using input = createInputFrom('https://test-streams.mux.dev/x36xhzz/url_6/193039199_mp4_h264_aac_hq_7.m3u8', ALL_FORMATS); @@ -226,20 +236,20 @@ test.concurrent('Single-variant Big Buck Bunny', { timeout: 15_000 }, async () = const audioTrack = tracks[0]! as InputAudioTrack; expect(audioTrack.isAudioTrack()).toBe(true); - expect(audioTrack.codec).toBe('aac'); + expect(await audioTrack.getCodec()).toBe('aac'); const videoTrack = tracks[1] as InputVideoTrack; expect(videoTrack.isVideoTrack()).toBe(true); - expect(videoTrack.codec).toBe('avc'); + expect(await videoTrack.getCodec()).toBe('avc'); }); test.concurrent('Codec-less (underspecified) master playlist', { timeout: 15_000 }, async () => { using input = createInputFrom('https://test-streams.mux.dev/test_001/stream.m3u8', ALL_FORMATS); - const descriptors = await input.getTrackDescriptors(); - expect(descriptors).toHaveLength(12); + const tracks = await input.getTracks(); + expect(tracks).toHaveLength(12); - const codecs = new Set(descriptors.map(x => x.codec)); + const codecs = new Set(await Promise.all(tracks.map(x => x.getCodec()))); expect(codecs.size).toBe(2); expect(codecs.has('avc')).toBe(true); expect(codecs.has('aac')).toBe(true); @@ -313,19 +323,21 @@ test.concurrent('Custom IV', { timeout: 15_000 }, async () => { test.concurrent('Out-of-band audio track via ADTS', { timeout: 15_000 }, async () => { using input = createInputFrom('https://playertest.longtailvideo.com/adaptive/aes-with-tracks/master.m3u8', ALL_FORMATS); - const descriptors = await input.getTrackDescriptors(); - expect(descriptors.some(x => x.isVideoTrackDescriptor() && x.hasOnlyKeyPackets)).toBe(true); + const tracks = await input.getTracks(); + const videoOnlyKeyPacketsFlags = await Promise.all( + tracks.filter((x): x is InputVideoTrack => x.isVideoTrack()).map(x => x.getHasOnlyKeyPackets()), + ); + expect(videoOnlyKeyPacketsFlags.some(x => x)).toBe(true); - const audioDescriptor = descriptors.find(x => x.isAudioTrackDescriptor()); - assert(audioDescriptor); - expect(audioDescriptor.hasOnlyKeyPackets).toBe(true); + const audioTrack = tracks.find((x): x is InputAudioTrack => x.isAudioTrack()); + assert(audioTrack); + expect(await audioTrack.getHasOnlyKeyPackets()).toBe(true); - expect(await audioDescriptor.getPairableVideoTrackDescriptors()).toHaveLength(1); // Since the I-frame one isn't pairable - const videoTrack = await (await audioDescriptor.getPairableVideoTrackDescriptors())[0]!.getTrack(); + expect(await audioTrack.getPairableVideoTracks()).toHaveLength(1); // Since the I-frame one isn't pairable + const videoTrack = (await audioTrack.getPairableVideoTracks())[0]!; assert(videoTrack); - expect(videoTrack.hasOnlyKeyPackets).toBe(false); + expect(await videoTrack.getHasOnlyKeyPackets()).toBe(false); - const audioTrack = await audioDescriptor.getTrack(); let lastTimestamp = -Infinity; const sink = new EncodedPacketSink(audioTrack); for await (const packet of sink.packets()) { @@ -341,9 +353,9 @@ test.concurrent('Out-of-band audio track via ADTS', { timeout: 15_000 }, async ( test.concurrent('MP3 audio only', { timeout: 15_000 }, async () => { using input = createInputFrom('https://pl.streamingvideoprovider.com/mp3-playlist/playlist.m3u8', ALL_FORMATS); - const audioDescriptor = (await input.getAudioTrackDescriptors())[0]; - assert(audioDescriptor); - expect(audioDescriptor.codec).toBe('mp3'); + const audioTrack = (await input.getAudioTracks())[0]; + assert(audioTrack); + expect(await audioTrack.getCodec()).toBe('mp3'); }); test.concurrent('fMP4', { timeout: 15_000 }, async () => { @@ -358,10 +370,10 @@ test.concurrent('fMP4', { timeout: 15_000 }, async () => { assert(videoTrack); assert(audioTrack); - expect(videoTrack.codedWidth).toBe(768); - expect(videoTrack.codedHeight).toBe(576); - expect(audioTrack.numberOfChannels).toBe(2); - expect(audioTrack.sampleRate).toBe(48000); + expect(await videoTrack.getCodedWidth()).toBe(768); + expect(await videoTrack.getCodedHeight()).toBe(576); + expect(await audioTrack.getNumberOfChannels()).toBe(2); + expect(await audioTrack.getSampleRate()).toBe(48000); expect(await videoTrack.getFirstTimestamp()).toBe(0); expect(await videoTrack.computeDuration()).toBe(60); @@ -375,37 +387,37 @@ test.concurrent('fMP4', { timeout: 15_000 }, async () => { test.concurrent('Track disposition & metadata', { timeout: 15_000 }, async () => { using input = createInputFrom('https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8', ALL_FORMATS); - const audioDescriptors = await input.getAudioTrackDescriptors(); + const audioTracks = await input.getAudioTracks(); - expect(audioDescriptors).toHaveLength(6); + expect(audioTracks).toHaveLength(6); - expect(audioDescriptors[0]!.languageCode).toBe('en'); - expect(audioDescriptors[1]!.languageCode).toBe('de'); - expect(audioDescriptors[2]!.languageCode).toBe('it'); - expect(audioDescriptors[3]!.languageCode).toBe('fr'); - expect(audioDescriptors[4]!.languageCode).toBe('es'); - expect(audioDescriptors[5]!.languageCode).toBe('en'); + expect(await audioTracks[0]!.getLanguageCode()).toBe('en'); + expect(await audioTracks[1]!.getLanguageCode()).toBe('de'); + expect(await audioTracks[2]!.getLanguageCode()).toBe('it'); + expect(await audioTracks[3]!.getLanguageCode()).toBe('fr'); + expect(await audioTracks[4]!.getLanguageCode()).toBe('es'); + expect(await audioTracks[5]!.getLanguageCode()).toBe('en'); - expect(audioDescriptors[0]!.disposition!.primary).toBe(true); - expect(audioDescriptors[1]!.disposition!.primary).toBe(false); - expect(audioDescriptors[2]!.disposition!.primary).toBe(false); - expect(audioDescriptors[3]!.disposition!.primary).toBe(false); - expect(audioDescriptors[4]!.disposition!.primary).toBe(false); - expect(audioDescriptors[5]!.disposition!.primary).toBe(false); + expect((await audioTracks[0]!.getDisposition()).primary).toBe(true); + expect((await audioTracks[1]!.getDisposition()).primary).toBe(false); + expect((await audioTracks[2]!.getDisposition()).primary).toBe(false); + expect((await audioTracks[3]!.getDisposition()).primary).toBe(false); + expect((await audioTracks[4]!.getDisposition()).primary).toBe(false); + expect((await audioTracks[5]!.getDisposition()).primary).toBe(false); - expect(audioDescriptors[0]!.disposition!.default).toBe(true); - expect(audioDescriptors[1]!.disposition!.default).toBe(true); - expect(audioDescriptors[2]!.disposition!.default).toBe(true); - expect(audioDescriptors[3]!.disposition!.default).toBe(true); - expect(audioDescriptors[4]!.disposition!.default).toBe(true); - expect(audioDescriptors[5]!.disposition!.default).toBe(false); + expect((await audioTracks[0]!.getDisposition()).default).toBe(true); + expect((await audioTracks[1]!.getDisposition()).default).toBe(true); + expect((await audioTracks[2]!.getDisposition()).default).toBe(true); + expect((await audioTracks[3]!.getDisposition()).default).toBe(true); + expect((await audioTracks[4]!.getDisposition()).default).toBe(true); + expect((await audioTracks[5]!.getDisposition()).default).toBe(false); - expect(audioDescriptors[0]!.name).toBe('stream_5'); - expect(audioDescriptors[1]!.name).toBe('stream_4'); - expect(audioDescriptors[2]!.name).toBe('stream_8'); - expect(audioDescriptors[3]!.name).toBe('stream_7'); - expect(audioDescriptors[4]!.name).toBe('stream_9'); - expect(audioDescriptors[5]!.name).toBe('stream_6'); + expect(await audioTracks[0]!.getName()).toBe('stream_5'); + expect(await audioTracks[1]!.getName()).toBe('stream_4'); + expect(await audioTracks[2]!.getName()).toBe('stream_8'); + expect(await audioTracks[3]!.getName()).toBe('stream_7'); + expect(await audioTracks[4]!.getName()).toBe('stream_9'); + expect(await audioTracks[5]!.getName()).toBe('stream_6'); }); test.concurrent('fMP4 Bitmovin', { timeout: 15_000 }, async () => { @@ -424,18 +436,23 @@ test.concurrent('fMP4 Bitmovin', { timeout: 15_000 }, async () => { let sourceCount = 0; input.on('source', () => sourceCount++); - const descriptors = await input.getTrackDescriptors(); - expect(descriptors.filter(x => x.isVideoTrackDescriptor())).toHaveLength(6); - expect(descriptors.filter(x => x.isAudioTrackDescriptor())).toHaveLength(1); + const tracks = await input.getTracks(); + const videoTracks = tracks.filter((x): x is InputVideoTrack => x.isVideoTrack()); + const audioTracks = tracks.filter((x): x is InputAudioTrack => x.isAudioTrack()); + expect(videoTracks).toHaveLength(6); + expect(audioTracks).toHaveLength(1); - expect(descriptors.some(x => x.isVideoTrackDescriptor() && x.displayWidth === 320 && x.displayHeight === 180)).toBe(true); - expect(descriptors.some(x => x.isVideoTrackDescriptor() && x.displayWidth === 480 && x.displayHeight === 270)).toBe(true); - expect(descriptors.some(x => x.isVideoTrackDescriptor() && x.displayWidth === 640 && x.displayHeight === 360)).toBe(true); - expect(descriptors.some(x => x.isVideoTrackDescriptor() && x.displayWidth === 960 && x.displayHeight === 540)).toBe(true); - expect(descriptors.some(x => x.isVideoTrackDescriptor() && x.displayWidth === 1280 && x.displayHeight === 720)).toBe(true); - expect(descriptors.some(x => x.isVideoTrackDescriptor() && x.displayWidth === 1920 && x.displayHeight === 1080)).toBe(true); + const videoDisplayDims = await Promise.all( + videoTracks.map(async t => ({ w: await t.getDisplayWidth(), h: await t.getDisplayHeight() })), + ); + expect(videoDisplayDims.some(d => d.w === 320 && d.h === 180)).toBe(true); + expect(videoDisplayDims.some(d => d.w === 480 && d.h === 270)).toBe(true); + expect(videoDisplayDims.some(d => d.w === 640 && d.h === 360)).toBe(true); + expect(videoDisplayDims.some(d => d.w === 960 && d.h === 540)).toBe(true); + expect(videoDisplayDims.some(d => d.w === 1280 && d.h === 720)).toBe(true); + expect(videoDisplayDims.some(d => d.w === 1920 && d.h === 1080)).toBe(true); - const videoTrack = await input.pluckVideoTrack(); + const videoTrack = (await input.getVideoTracks())[0]; assert(videoTrack); expect(await videoTrack.getFirstTimestamp()).toBe(4); @@ -448,7 +465,7 @@ test.concurrent('Single-value PDT', { timeout: 15_000 }, async () => { using input = createInputFrom('https://playertest.longtailvideo.com/adaptive/aviion/manifest.m3u8', ALL_FORMATS); const tracks = await input.getTracks(); - expect(tracks.every(x => x.isRelativeToUnixEpoch)).toBe(true); + expect((await Promise.all(tracks.map(x => x.getIsRelativeToUnixEpoch()))).every(x => x)).toBe(true); const track = tracks[0]!; const firstTimestamp = await track.getFirstTimestamp(); @@ -511,131 +528,118 @@ test.concurrent('PDT with bad values', { timeout: 15_000 }, async () => { const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); - expect(audioTrack.isRelativeToUnixEpoch).toBe(false); + expect(await audioTrack.getIsRelativeToUnixEpoch()).toBe(false); }); test.concurrent('Alternative audio only', { timeout: 15_000 }, async () => { using input = createInputFrom('https://playertest.longtailvideo.com/adaptive/alt-audio-no-video/sintel/playlist.m3u8', ALL_FORMATS); - const descriptors = await input.getTrackDescriptors(); - expect(descriptors).toHaveLength(2); + const tracks = await input.getTracks(); + expect(tracks).toHaveLength(2); - expect(descriptors[0]).toMatchObject({ type: 'audio', languageCode: 'en', name: 'English' }); - expect(descriptors[1]).toMatchObject({ type: 'audio', languageCode: 'dubbing', name: 'Dubbing' }); + expect(tracks[0]!.type).toBe('audio'); + expect(await tracks[0]!.getLanguageCode()).toBe('en'); + expect(await tracks[0]!.getName()).toBe('English'); + + expect(tracks[1]!.type).toBe('audio'); + expect(await tracks[1]!.getLanguageCode()).toBe('dubbing'); + expect(await tracks[1]!.getName()).toBe('Dubbing'); }); test.concurrent('Advanced Apple HLS', { timeout: 30_000 }, async () => { using input = createInputFrom('https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8', ALL_FORMATS); - const descriptors = await input.getTrackDescriptors(); - const videoDescriptors = descriptors.filter(x => x.isVideoTrackDescriptor()); - const audioDescriptors = descriptors.filter(x => x.isAudioTrackDescriptor()); + const tracks = await input.getTracks(); + const videoTracks = tracks.filter((x): x is InputVideoTrack => x.isVideoTrack()); + const audioTracks = tracks.filter((x): x is InputAudioTrack => x.isAudioTrack()); - expect(audioDescriptors).toHaveLength(3); - expect(videoDescriptors).toHaveLength(28); + expect(audioTracks).toHaveLength(3); + expect(videoTracks).toHaveLength(28); - expect(audioDescriptors[0]).toMatchObject({ codec: 'aac', name: 'English', languageCode: 'en' }); - expect(audioDescriptors[1]).toMatchObject({ codec: 'ac3', name: 'English', languageCode: 'en' }); - expect(audioDescriptors[2]).toMatchObject({ codec: 'eac3', name: 'English', languageCode: 'en' }); + const snapshotVideoTrack = async (t: InputVideoTrack) => ({ + codec: await t.getCodec(), + displayWidth: await t.getDisplayWidth(), + displayHeight: await t.getDisplayHeight(), + bitrate: await t.getBitrate(), + hasOnlyKeyPackets: await t.getHasOnlyKeyPackets(), + codecParameterString: await t.getCodecParameterString(), + }); + const snapshotAudioTrack = async (t: InputAudioTrack) => ({ + codec: await t.getCodec(), + name: await t.getName(), + languageCode: await t.getLanguageCode(), + codecParameterString: await t.getCodecParameterString(), + }); - expect(audioDescriptors[0]!.codecParameterString).toBe('mp4a.40.2'); - expect(audioDescriptors[1]!.codecParameterString).toBe('ac-3'); - expect(audioDescriptors[2]!.codecParameterString).toBe('ec-3'); + const audioSnapshots = await Promise.all(audioTracks.map(snapshotAudioTrack)); + const videoSnapshots = await Promise.all(videoTracks.map(snapshotVideoTrack)); - expect(await audioDescriptors[0]!.getPairableVideoTrackDescriptors()).toHaveLength(18); - expect(await audioDescriptors[1]!.getPairableVideoTrackDescriptors()).toHaveLength(18); - expect(await audioDescriptors[2]!.getPairableVideoTrackDescriptors()).toHaveLength(18); + expect(audioSnapshots[0]).toMatchObject({ codec: 'aac', name: 'English', languageCode: 'en', codecParameterString: 'mp4a.40.2' }); + expect(audioSnapshots[1]).toMatchObject({ codec: 'ac3', name: 'English', languageCode: 'en', codecParameterString: 'ac-3' }); + expect(audioSnapshots[2]).toMatchObject({ codec: 'eac3', name: 'English', languageCode: 'en', codecParameterString: 'ec-3' }); + + expect(await audioTracks[0]!.getPairableVideoTracks()).toHaveLength(18); + expect(await audioTracks[1]!.getPairableVideoTracks()).toHaveLength(18); + expect(await audioTracks[2]!.getPairableVideoTracks()).toHaveLength(18); // I-FRAME AVC - expect(videoDescriptors[0]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 1015727, hasOnlyKeyPackets: true }); - expect(videoDescriptors[1]).toMatchObject({ codec: 'avc', displayWidth: 1280, displayHeight: 720, bitrate: 760174, hasOnlyKeyPackets: true }); - expect(videoDescriptors[2]).toMatchObject({ codec: 'avc', displayWidth: 960, displayHeight: 540, bitrate: 520162, hasOnlyKeyPackets: true }); - expect(videoDescriptors[3]).toMatchObject({ codec: 'avc', displayWidth: 640, displayHeight: 360, bitrate: 186651, hasOnlyKeyPackets: true }); - expect(videoDescriptors[4]).toMatchObject({ codec: 'avc', displayWidth: 480, displayHeight: 270, bitrate: 95410, hasOnlyKeyPackets: true }); - - expect(videoDescriptors[0]!.codecParameterString).toBe('avc1.640028'); - expect(videoDescriptors[1]!.codecParameterString).toBe('avc1.64001f'); - expect(videoDescriptors[2]!.codecParameterString).toBe('avc1.64001f'); - expect(videoDescriptors[3]!.codecParameterString).toBe('avc1.64001f'); - expect(videoDescriptors[4]!.codecParameterString).toBe('avc1.64001f'); + expect(videoSnapshots[0]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 1015727, hasOnlyKeyPackets: true, codecParameterString: 'avc1.640028' }); + expect(videoSnapshots[1]).toMatchObject({ codec: 'avc', displayWidth: 1280, displayHeight: 720, bitrate: 760174, hasOnlyKeyPackets: true, codecParameterString: 'avc1.64001f' }); + expect(videoSnapshots[2]).toMatchObject({ codec: 'avc', displayWidth: 960, displayHeight: 540, bitrate: 520162, hasOnlyKeyPackets: true, codecParameterString: 'avc1.64001f' }); + expect(videoSnapshots[3]).toMatchObject({ codec: 'avc', displayWidth: 640, displayHeight: 360, bitrate: 186651, hasOnlyKeyPackets: true, codecParameterString: 'avc1.64001f' }); + expect(videoSnapshots[4]).toMatchObject({ codec: 'avc', displayWidth: 480, displayHeight: 270, bitrate: 95410, hasOnlyKeyPackets: true, codecParameterString: 'avc1.64001f' }); for (let i = 0; i <= 4; i++) { - const videoDescriptor = videoDescriptors[i]!; - expect(audioDescriptors.some(x => x.canBePairedWith(videoDescriptor))).toBe(false); + const videoTrack = videoTracks[i]!; + expect(audioTracks.some(x => x.canBePairedWith(videoTrack))).toBe(false); } // AVC - expect(videoDescriptors[5]).toMatchObject({ codec: 'avc', displayWidth: 960, displayHeight: 540, bitrate: 2746096, hasOnlyKeyPackets: false }); - expect(videoDescriptors[6]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 10095767, hasOnlyKeyPackets: false }); - expect(videoDescriptors[7]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 7540836, hasOnlyKeyPackets: false }); - expect(videoDescriptors[8]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 5644219, hasOnlyKeyPackets: false }); - expect(videoDescriptors[9]).toMatchObject({ codec: 'avc', displayWidth: 1280, displayHeight: 720, bitrate: 3833756, hasOnlyKeyPackets: false }); - expect(videoDescriptors[10]).toMatchObject({ codec: 'avc', displayWidth: 768, displayHeight: 432, bitrate: 1698402, hasOnlyKeyPackets: false }); - expect(videoDescriptors[11]).toMatchObject({ codec: 'avc', displayWidth: 640, displayHeight: 360, bitrate: 1240204, hasOnlyKeyPackets: false }); - expect(videoDescriptors[12]).toMatchObject({ codec: 'avc', displayWidth: 480, displayHeight: 270, bitrate: 805319, hasOnlyKeyPackets: false }); - expect(videoDescriptors[13]).toMatchObject({ codec: 'avc', displayWidth: 416, displayHeight: 234, bitrate: 561903, hasOnlyKeyPackets: false }); - - expect(videoDescriptors[5]!.codecParameterString).toBe('avc1.640020'); - expect(videoDescriptors[6]!.codecParameterString).toBe('avc1.64002a'); - expect(videoDescriptors[7]!.codecParameterString).toBe('avc1.64002a'); - expect(videoDescriptors[8]!.codecParameterString).toBe('avc1.64002a'); - expect(videoDescriptors[9]!.codecParameterString).toBe('avc1.640020'); - expect(videoDescriptors[10]!.codecParameterString).toBe('avc1.64001f'); - expect(videoDescriptors[11]!.codecParameterString).toBe('avc1.64001f'); - expect(videoDescriptors[12]!.codecParameterString).toBe('avc1.64001f'); - expect(videoDescriptors[13]!.codecParameterString).toBe('avc1.64001f'); + expect(videoSnapshots[5]).toMatchObject({ codec: 'avc', displayWidth: 960, displayHeight: 540, bitrate: 2746096, hasOnlyKeyPackets: false, codecParameterString: 'avc1.640020' }); + expect(videoSnapshots[6]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 10095767, hasOnlyKeyPackets: false, codecParameterString: 'avc1.64002a' }); + expect(videoSnapshots[7]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 7540836, hasOnlyKeyPackets: false, codecParameterString: 'avc1.64002a' }); + expect(videoSnapshots[8]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 5644219, hasOnlyKeyPackets: false, codecParameterString: 'avc1.64002a' }); + expect(videoSnapshots[9]).toMatchObject({ codec: 'avc', displayWidth: 1280, displayHeight: 720, bitrate: 3833756, hasOnlyKeyPackets: false, codecParameterString: 'avc1.640020' }); + expect(videoSnapshots[10]).toMatchObject({ codec: 'avc', displayWidth: 768, displayHeight: 432, bitrate: 1698402, hasOnlyKeyPackets: false, codecParameterString: 'avc1.64001f' }); + expect(videoSnapshots[11]).toMatchObject({ codec: 'avc', displayWidth: 640, displayHeight: 360, bitrate: 1240204, hasOnlyKeyPackets: false, codecParameterString: 'avc1.64001f' }); + expect(videoSnapshots[12]).toMatchObject({ codec: 'avc', displayWidth: 480, displayHeight: 270, bitrate: 805319, hasOnlyKeyPackets: false, codecParameterString: 'avc1.64001f' }); + expect(videoSnapshots[13]).toMatchObject({ codec: 'avc', displayWidth: 416, displayHeight: 234, bitrate: 561903, hasOnlyKeyPackets: false, codecParameterString: 'avc1.64001f' }); for (let i = 5; i <= 13; i++) { - const videoDescriptor = videoDescriptors[i]!; - expect(audioDescriptors.every(x => x.canBePairedWith(videoDescriptor))).toBe(true); + const videoTrack = videoTracks[i]!; + expect(audioTracks.every(x => x.canBePairedWith(videoTrack))).toBe(true); } // I-FRAME HEVC - expect(videoDescriptors[14]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 328352, hasOnlyKeyPackets: true }); - expect(videoDescriptors[15]).toMatchObject({ codec: 'hevc', displayWidth: 1280, displayHeight: 720, bitrate: 226274, hasOnlyKeyPackets: true }); - expect(videoDescriptors[16]).toMatchObject({ codec: 'hevc', displayWidth: 960, displayHeight: 540, bitrate: 159037, hasOnlyKeyPackets: true }); - expect(videoDescriptors[17]).toMatchObject({ codec: 'hevc', displayWidth: 640, displayHeight: 360, bitrate: 92800, hasOnlyKeyPackets: true }); - expect(videoDescriptors[18]).toMatchObject({ codec: 'hevc', displayWidth: 480, displayHeight: 270, bitrate: 51760, hasOnlyKeyPackets: true }); - - expect(videoDescriptors[14]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[15]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[16]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[17]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[18]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); + expect(videoSnapshots[14]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 328352, hasOnlyKeyPackets: true, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[15]).toMatchObject({ codec: 'hevc', displayWidth: 1280, displayHeight: 720, bitrate: 226274, hasOnlyKeyPackets: true, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[16]).toMatchObject({ codec: 'hevc', displayWidth: 960, displayHeight: 540, bitrate: 159037, hasOnlyKeyPackets: true, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[17]).toMatchObject({ codec: 'hevc', displayWidth: 640, displayHeight: 360, bitrate: 92800, hasOnlyKeyPackets: true, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[18]).toMatchObject({ codec: 'hevc', displayWidth: 480, displayHeight: 270, bitrate: 51760, hasOnlyKeyPackets: true, codecParameterString: 'hvc1.2.4.L123.B0' }); for (let i = 14; i <= 18; i++) { - const videoDescriptor = videoDescriptors[i]!; - expect(audioDescriptors.some(x => x.canBePairedWith(videoDescriptor))).toBe(false); + const videoTrack = videoTracks[i]!; + expect(audioTracks.some(x => x.canBePairedWith(videoTrack))).toBe(false); } // HEVC - expect(videoDescriptors[19]).toMatchObject({ codec: 'hevc', displayWidth: 960, displayHeight: 540, bitrate: 2386827, hasOnlyKeyPackets: false }); - expect(videoDescriptors[20]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 6886727, hasOnlyKeyPackets: false }); - expect(videoDescriptors[21]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 5650398, hasOnlyKeyPackets: false }); - expect(videoDescriptors[22]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 4302269, hasOnlyKeyPackets: false }); - expect(videoDescriptors[23]).toMatchObject({ codec: 'hevc', displayWidth: 1280, displayHeight: 720, bitrate: 2987200, hasOnlyKeyPackets: false }); - expect(videoDescriptors[24]).toMatchObject({ codec: 'hevc', displayWidth: 768, displayHeight: 432, bitrate: 1448754, hasOnlyKeyPackets: false }); - expect(videoDescriptors[25]).toMatchObject({ codec: 'hevc', displayWidth: 640, displayHeight: 360, bitrate: 1124269, hasOnlyKeyPackets: false }); - expect(videoDescriptors[26]).toMatchObject({ codec: 'hevc', displayWidth: 480, displayHeight: 270, bitrate: 771426, hasOnlyKeyPackets: false }); - expect(videoDescriptors[27]).toMatchObject({ codec: 'hevc', displayWidth: 416, displayHeight: 234, bitrate: 563212, hasOnlyKeyPackets: false }); - - expect(videoDescriptors[19]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[20]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[21]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[22]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[23]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[24]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[25]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[26]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); - expect(videoDescriptors[27]!.codecParameterString).toBe('hvc1.2.4.L123.B0'); + expect(videoSnapshots[19]).toMatchObject({ codec: 'hevc', displayWidth: 960, displayHeight: 540, bitrate: 2386827, hasOnlyKeyPackets: false, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[20]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 6886727, hasOnlyKeyPackets: false, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[21]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 5650398, hasOnlyKeyPackets: false, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[22]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 4302269, hasOnlyKeyPackets: false, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[23]).toMatchObject({ codec: 'hevc', displayWidth: 1280, displayHeight: 720, bitrate: 2987200, hasOnlyKeyPackets: false, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[24]).toMatchObject({ codec: 'hevc', displayWidth: 768, displayHeight: 432, bitrate: 1448754, hasOnlyKeyPackets: false, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[25]).toMatchObject({ codec: 'hevc', displayWidth: 640, displayHeight: 360, bitrate: 1124269, hasOnlyKeyPackets: false, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[26]).toMatchObject({ codec: 'hevc', displayWidth: 480, displayHeight: 270, bitrate: 771426, hasOnlyKeyPackets: false, codecParameterString: 'hvc1.2.4.L123.B0' }); + expect(videoSnapshots[27]).toMatchObject({ codec: 'hevc', displayWidth: 416, displayHeight: 234, bitrate: 563212, hasOnlyKeyPackets: false, codecParameterString: 'hvc1.2.4.L123.B0' }); for (let i = 19; i <= 27; i++) { - const videoDescriptor = videoDescriptors[i]!; - expect(audioDescriptors.every(x => x.canBePairedWith(videoDescriptor))).toBe(true); + const videoTrack = videoTracks[i]!; + expect(audioTracks.every(x => x.canBePairedWith(videoTrack))).toBe(true); } }); diff --git a/test/node/isobmff-muxer.test.ts b/test/node/isobmff-muxer.test.ts index 83d1d9e..b8eec61 100644 --- a/test/node/isobmff-muxer.test.ts +++ b/test/node/isobmff-muxer.test.ts @@ -42,9 +42,9 @@ test('ISOBMFF muxer internally converts ADTS to AAC', async () => { const outputTrack = await outputAsInput.getPrimaryAudioTrack(); assert(outputTrack); - expect(outputTrack.codec).toBe('aac'); - expect(outputTrack.sampleRate).toBe(inputTrack.sampleRate); - expect(outputTrack.numberOfChannels).toBe(inputTrack.numberOfChannels); + expect(await outputTrack.getCodec()).toBe('aac'); + expect(await outputTrack.getSampleRate()).toBe(await inputTrack.getSampleRate()); + expect(await outputTrack.getNumberOfChannels()).toBe(await inputTrack.getNumberOfChannels()); const outputDecoderConfig = await outputTrack.getDecoderConfig(); expect(outputDecoderConfig!.description).toBeDefined(); diff --git a/test/node/matroska-muxer.test.ts b/test/node/matroska-muxer.test.ts index f42f3c3..3b94e85 100644 --- a/test/node/matroska-muxer.test.ts +++ b/test/node/matroska-muxer.test.ts @@ -42,9 +42,9 @@ test('Matroska muxer internally converts ADTS to AAC', async () => { const outputTrack = await outputAsInput.getPrimaryAudioTrack(); assert(outputTrack); - expect(outputTrack.codec).toBe('aac'); - expect(outputTrack.sampleRate).toBe(inputTrack.sampleRate); - expect(outputTrack.numberOfChannels).toBe(inputTrack.numberOfChannels); + expect(await outputTrack.getCodec()).toBe('aac'); + expect(await outputTrack.getSampleRate()).toBe(await inputTrack.getSampleRate()); + expect(await outputTrack.getNumberOfChannels()).toBe(await inputTrack.getNumberOfChannels()); const outputDecoderConfig = await outputTrack.getDecoderConfig(); expect(outputDecoderConfig!.description).toBeDefined(); diff --git a/test/node/mp3-encoder-extension.test.ts b/test/node/mp3-encoder-extension.test.ts index 6a28f69..e1b6c0b 100644 --- a/test/node/mp3-encoder-extension.test.ts +++ b/test/node/mp3-encoder-extension.test.ts @@ -66,9 +66,9 @@ test('MP3 encoding', async () => { }); const track = (await input.getPrimaryAudioTrack())!; - expect(track.codec).toBe('mp3'); - expect(track.sampleRate).toBe(sampleRate); - expect(track.numberOfChannels).toBe(channels); + expect(await track.getCodec()).toBe('mp3'); + expect(await track.getSampleRate()).toBe(sampleRate); + expect(await track.getNumberOfChannels()).toBe(channels); const sink = new EncodedPacketSink(track); let packetCount = 0; diff --git a/test/node/mpeg-ts-demuxing.test.ts b/test/node/mpeg-ts-demuxing.test.ts index dbff147..e178b74 100644 --- a/test/node/mpeg-ts-demuxing.test.ts +++ b/test/node/mpeg-ts-demuxing.test.ts @@ -35,11 +35,11 @@ test('MPEG-TS metadata reading', async () => { assert(videoTrack); expect(videoTrack.id).toBe(0x100); - expect(videoTrack.codec).toBe('avc'); - expect(videoTrack.internalCodecId).toBe(0x1b); - expect(videoTrack.displayWidth).toEqual(720); - expect(videoTrack.displayHeight).toEqual(720); - expect(videoTrack.timeResolution).toBe(90_000); + expect(await videoTrack.getCodec()).toBe('avc'); + expect(await videoTrack.getInternalCodecId()).toBe(0x1b); + expect(await videoTrack.getDisplayWidth()).toEqual(720); + expect(await videoTrack.getDisplayHeight()).toEqual(720); + expect(await videoTrack.getTimeResolution()).toBe(90_000); const videoDecoderConfig = await videoTrack.getDecoderConfig(); expect(videoDecoderConfig).toEqual({ @@ -61,9 +61,9 @@ test('MPEG-TS metadata reading', async () => { assert(audioTrack); expect(audioTrack.id).toBe(0x101); - expect(audioTrack.codec).toBe('aac'); - expect(audioTrack.numberOfChannels).toBe(2); - expect(audioTrack.sampleRate).toBe(48000); + expect(await audioTrack.getCodec()).toBe('aac'); + expect(await audioTrack.getNumberOfChannels()).toBe(2); + expect(await audioTrack.getSampleRate()).toBe(48000); const audioDecoderConfig = await audioTrack.getDecoderConfig(); expect(audioDecoderConfig).toEqual({ @@ -549,10 +549,10 @@ test('MPEG-TS with HEVC video', async () => { const videoTrack = await input.getPrimaryVideoTrack(); assert(videoTrack); - expect(videoTrack.codec).toBe('hevc'); - expect(videoTrack.internalCodecId).toBe(0x24); - expect(videoTrack.displayWidth).toBe(1920); - expect(videoTrack.displayHeight).toBe(1080); + expect(await videoTrack.getCodec()).toBe('hevc'); + expect(await videoTrack.getInternalCodecId()).toBe(0x24); + expect(await videoTrack.getDisplayWidth()).toBe(1920); + expect(await videoTrack.getDisplayHeight()).toBe(1080); const videoDecoderConfig = await videoTrack.getDecoderConfig(); expect(videoDecoderConfig).toEqual({ @@ -588,8 +588,8 @@ test('MPEG-TS with MP3 audio', async () => { const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); - expect(audioTrack.codec).toBe('mp3'); - expect(audioTrack.internalCodecId).toBe(0x03); + expect(await audioTrack.getCodec()).toBe('mp3'); + expect(await audioTrack.getInternalCodecId()).toBe(0x03); const audioDecoderConfig = await audioTrack.getDecoderConfig(); expect(audioDecoderConfig).toEqual({ @@ -622,8 +622,8 @@ test('MPEG-TS with AC-3 audio (System A)', async () => { const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); - expect(audioTrack.codec).toBe('ac3'); - expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.AC3_SYSTEM_A); + expect(await audioTrack.getCodec()).toBe('ac3'); + expect(await audioTrack.getInternalCodecId()).toBe(MpegTsStreamType.AC3_SYSTEM_A); const audioDecoderConfig = await audioTrack.getDecoderConfig(); expect(audioDecoderConfig).toEqual({ @@ -657,8 +657,8 @@ test('MPEG-TS with AC-3 audio (System B)', async () => { const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); - expect(audioTrack.codec).toBe('ac3'); - expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.PRIVATE_DATA); + expect(await audioTrack.getCodec()).toBe('ac3'); + expect(await audioTrack.getInternalCodecId()).toBe(MpegTsStreamType.PRIVATE_DATA); const audioDecoderConfig = await audioTrack.getDecoderConfig(); expect(audioDecoderConfig).toEqual({ @@ -692,8 +692,8 @@ test('MPEG-TS with E-AC-3 audio (System A)', async () => { const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); - expect(audioTrack.codec).toBe('eac3'); - expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.EAC3_SYSTEM_A); + expect(await audioTrack.getCodec()).toBe('eac3'); + expect(await audioTrack.getInternalCodecId()).toBe(MpegTsStreamType.EAC3_SYSTEM_A); const audioDecoderConfig = await audioTrack.getDecoderConfig(); expect(audioDecoderConfig).toEqual({ @@ -727,8 +727,8 @@ test('MPEG-TS with E-AC-3 audio (System B)', async () => { const audioTrack = await input.getPrimaryAudioTrack(); assert(audioTrack); - expect(audioTrack.codec).toBe('eac3'); - expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.PRIVATE_DATA); + expect(await audioTrack.getCodec()).toBe('eac3'); + expect(await audioTrack.getInternalCodecId()).toBe(MpegTsStreamType.PRIVATE_DATA); const audioDecoderConfig = await audioTrack.getDecoderConfig(); expect(audioDecoderConfig).toEqual({