From 157582498ffc069c69343b4cdf22b19f6ea49a9b Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:54:00 +0100 Subject: [PATCH] Remove InputTrack.groupId, hide pairing mask, change HLS track merging behavior --- src/adts/adts-demuxer.ts | 4 - src/flac/flac-demuxer.ts | 4 - src/hls/hls-demuxer.ts | 49 +++---- src/hls/hls-muxer.ts | 38 ++---- src/input-track.ts | 17 +-- src/isobmff/isobmff-demuxer.ts | 5 +- src/matroska/matroska-demuxer.ts | 5 +- src/mp3/mp3-demuxer.ts | 4 - src/mpeg-ts/mpeg-ts-demuxer.ts | 5 +- src/ogg/ogg-demuxer.ts | 4 - src/output.ts | 30 +++++ src/segmented-input.ts | 7 +- src/wave/wave-demuxer.ts | 4 - test/node/hls-input.test.ts | 220 ++++++++++--------------------- 14 files changed, 148 insertions(+), 248 deletions(-) diff --git a/src/adts/adts-demuxer.ts b/src/adts/adts-demuxer.ts index c447f70..53ca81f 100644 --- a/src/adts/adts-demuxer.ts +++ b/src/adts/adts-demuxer.ts @@ -215,10 +215,6 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking { return false; } - getGroupId() { - return this.getId(); - } - getPairingMask() { return 1n; } diff --git a/src/flac/flac-demuxer.ts b/src/flac/flac-demuxer.ts index f4c00e9..848580b 100644 --- a/src/flac/flac-demuxer.ts +++ b/src/flac/flac-demuxer.ts @@ -602,10 +602,6 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking { return false; } - getGroupId() { - return this.getId(); - } - getPairingMask() { return 1n; } diff --git a/src/hls/hls-demuxer.ts b/src/hls/hls-demuxer.ts index 4282161..549ce4d 100644 --- a/src/hls/hls-demuxer.ts +++ b/src/hls/hls-demuxer.ts @@ -30,7 +30,6 @@ type InternalTrack = { fullPath: string; fullCodecString: string; - groupId: number; pairingMask: bigint; peakBitrate: number | null; averageBitrate: number | null; @@ -179,7 +178,7 @@ export class HlsDemuxer extends Demuxer { // Now, let's process & resolve all variant streams in parallel, mapping each of them to tracks. const internalTracksByVariant = await Promise.all(variantStreams.map(async (variantStream, i) => { - const result: { track: InternalTrack; canMerge: boolean }[] = []; + const result: InternalTrack[] = []; const codecsList = variantStream.attributes.get('codecs'); let codecStrings: string[]; @@ -334,7 +333,7 @@ export class HlsDemuxer extends Demuxer { } } - result.push({ track: { + result.push({ id: -1, demuxer: this, inputTrack: null, @@ -345,7 +344,6 @@ export class HlsDemuxer extends Demuxer { lineNumber: variantStream.lineNumber, fullPath: variantStream.fullPath, fullCodecString: videoCodecString, - groupId: 1, pairingMask: 1n << BigInt(i), peakBitrate: bandwidth, averageBitrate: averageBandwidth, @@ -356,7 +354,7 @@ export class HlsDemuxer extends Demuxer { width, height, }, - }, canMerge: false }); + }); } else { if (!videoGroupIds.includes(videoGroupId)) { throw new Error( @@ -386,7 +384,7 @@ export class HlsDemuxer extends Demuxer { } } - result.push({ track: { + result.push({ id: -1, demuxer: this, inputTrack: null, @@ -399,7 +397,6 @@ export class HlsDemuxer extends Demuxer { lineNumber: mediaTag.lineNumber, fullPath: mediaTag.fullPath ?? variantStream.fullPath, fullCodecString: videoCodecString, - groupId: 3 + videoGroupIds.indexOf(groupId), pairingMask: 1n << BigInt(i), peakBitrate: null, averageBitrate: null, @@ -410,7 +407,7 @@ export class HlsDemuxer extends Demuxer { width, height, }, - }, canMerge: true }); + }); } } } else if (AUDIO_CODECS.includes(inferredCodec as AudioCodec)) { @@ -431,7 +428,7 @@ export class HlsDemuxer extends Demuxer { ? Number(channels) : null; - result.push({ track: { + result.push({ id: -1, demuxer: this, inputTrack: null, @@ -442,7 +439,6 @@ export class HlsDemuxer extends Demuxer { lineNumber: variantStream.lineNumber, fullPath: variantStream.fullPath, fullCodecString: audioCodecString, - groupId: 2, pairingMask: 1n << BigInt(i), peakBitrate: bandwidth, averageBitrate: averageBandwidth, @@ -457,7 +453,7 @@ export class HlsDemuxer extends Demuxer { ? parsedChannels : null, }, - }, canMerge: false }); + }); } else { if (!audioGroupIds.includes(audioGroupId)) { throw new Error( @@ -480,7 +476,7 @@ export class HlsDemuxer extends Demuxer { ? Number(channels) : null; - result.push({ track: { + result.push({ id: -1, demuxer: this, inputTrack: null, @@ -493,7 +489,6 @@ export class HlsDemuxer extends Demuxer { lineNumber: mediaTag.lineNumber, fullPath: mediaTag.fullPath ?? variantStream.fullPath, fullCodecString: audioCodecString, - groupId: 3 + videoGroupIds.length + audioGroupIds.indexOf(groupId), pairingMask: 1n << BigInt(i), peakBitrate: null, averageBitrate: null, @@ -508,7 +503,7 @@ export class HlsDemuxer extends Demuxer { ? parsedChannels : null, }, - }, canMerge: true }); + }); } } } @@ -518,16 +513,30 @@ export class HlsDemuxer extends Demuxer { })); const internalTracks: InternalTrack[] = []; - const addInternalTrack = (track: InternalTrack, canMerge: boolean) => { + const addInternalTrack = (track: InternalTrack) => { const existingTrack = internalTracks.find(x => - x.fullPath === track.fullPath && x.info.type === track.info.type && x.groupId === track.groupId, + x.fullPath === track.fullPath && x.info.type === track.info.type, ); - if (existingTrack && canMerge) { + if (existingTrack) { existingTrack.pairingMask |= track.pairingMask; existingTrack.default ||= track.default; + existingTrack.autoselect ||= track.autoselect; existingTrack.lineNumber = Math.min(existingTrack.lineNumber, track.lineNumber); + if (track.peakBitrate !== null) { + existingTrack.peakBitrate = Math.max( + existingTrack.peakBitrate ?? -Infinity, + track.peakBitrate, + ); + } + if (track.averageBitrate !== null) { + existingTrack.averageBitrate = Math.max( + existingTrack.averageBitrate ?? -Infinity, + track.averageBitrate, + ); + } + if (existingTrack.languageCode === UNDETERMINED_LANGUAGE) { existingTrack.languageCode = track.languageCode; } @@ -539,7 +548,7 @@ export class HlsDemuxer extends Demuxer { for (const variantInternalTracks of internalTracksByVariant) { for (const trackEntry of variantInternalTracks) { - addInternalTrack(trackEntry.track, trackEntry.canMerge); + addInternalTrack(trackEntry); } } @@ -653,10 +662,6 @@ abstract class HlsInputTrackBacking implements InputTrackBacking { return this.internalTrack.id; } - getGroupId(): number { - return this.internalTrack.groupId; - } - getPairingMask(): bigint { return this.internalTrack.pairingMask; } diff --git a/src/hls/hls-muxer.ts b/src/hls/hls-muxer.ts index 2fae58a..ec55751 100644 --- a/src/hls/hls-muxer.ts +++ b/src/hls/hls-muxer.ts @@ -2,7 +2,15 @@ import { MediaCodec, validateAudioChunkMetadata, validateVideoChunkMetadata } fr import { EncodedAudioPacketSource, EncodedVideoPacketSource, MediaSource } from '../media-source'; import { assert, joinPaths, textEncoder, UNDETERMINED_LANGUAGE } from '../misc'; import { Muxer } from '../muxer'; -import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack, TrackType } from '../output'; +import { + Output, + OutputAudioTrack, + OutputSubtitleTrack, + OutputTrack, + outputTracksArePairable, + OutputVideoTrack, + TrackType, +} from '../output'; import { HlsOutputFormat, HlsOutputFormatOptions, OutputFormat } from '../output-format'; import { EncodedPacket } from '../packet'; import { SubtitleCue, SubtitleMetadata } from '../subtitles'; @@ -101,38 +109,13 @@ export class HlsMuxer extends Muxer { } const pairableGroups = new Map(); - const trackGroups = Array.isArray(track.metadata.group) - ? track.metadata.group - : [track.metadata.group!]; for (const otherTrack of this.output._tracks) { if (track === otherTrack) { continue; } - let pairable = false; - const otherTrackGroups = Array.isArray(otherTrack.metadata.group) - ? otherTrack.metadata.group - : [otherTrack.metadata.group!]; - - for (const group of trackGroups) { - const pairableInSameGroup = track.type !== otherTrack.type - && otherTrackGroups.some(otherGroup => group === otherGroup); - if (pairableInSameGroup) { - pairable = true; - break; - } - - const pairableAcrossGroups = otherTrackGroups.some( - otherGroup => group._pairedGroups.has(otherGroup), - ); - if (pairableAcrossGroups) { - pairable = true; - break; - } - } - - if (!pairable) { + if (!outputTracksArePairable(track, otherTrack)) { continue; } @@ -828,6 +811,7 @@ export class HlsMuxer extends Muxer { } } + // Write all playlists in parallel const playlistPromises = this.playlists.map(async (playlist) => { let targetDuration = this.targetSegmentDuration; for (const segment of playlist.writtenSegments) { diff --git a/src/input-track.ts b/src/input-track.ts index 77a1223..c4289a4 100644 --- a/src/input-track.ts +++ b/src/input-track.ts @@ -41,7 +41,6 @@ export interface InputTrackBacking { getTimeResolution(): number; getTimestampsAreRelativeToUnixEpoch(): boolean; getDisposition(): TrackDisposition; - getGroupId(): number; getPairingMask(): bigint; getBitrate(): number | null; getAverageBitrate(): number | null; @@ -118,14 +117,6 @@ export abstract class InputTrack { return this._backing.getNumber(); } - get groupId() { - return this._backing.getGroupId(); - } - - get pairingMask() { - return this._backing.getPairingMask(); - } - /** * The identifier of the codec used internally by the container. It is not homogenized by Mediabunny * and depends entirely on the container format. @@ -310,9 +301,11 @@ export abstract class InputTrack { return true; } - return this.input === otherTrack.input - && this.groupId !== otherTrack.groupId // This also prevents the track from being paired with itself - && (this.pairingMask & otherTrack.pairingMask) !== 0n; + if (this.input !== otherTrack.input || this === otherTrack) { + return false; + } + + return (this._backing.getPairingMask() & otherTrack._backing.getPairingMask()) !== 0n; } async getPairableTracks(query?: TrackQuery) { diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index a77b40d..05e740e 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -39,6 +39,7 @@ import { InputAudioTrackBacking, InputTrack, InputTrackBacking, + InputVideoTrack, InputVideoTrackBacking, } from '../input-track'; @@ -2565,10 +2566,6 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { return this.internalTrack.disposition; } - getGroupId() { - return this.getId(); - } - getPairingMask() { return 1n; } diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index 84c3bbc..34b6efd 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -28,6 +28,7 @@ import { InputAudioTrackBacking, InputTrack, InputTrackBacking, + InputVideoTrack, InputVideoTrackBacking, } from '../input-track'; @@ -1978,10 +1979,6 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { return this.internalTrack.disposition; } - getGroupId() { - return this.getId(); - } - getPairingMask() { return 1n; } diff --git a/src/mp3/mp3-demuxer.ts b/src/mp3/mp3-demuxer.ts index cae1524..23e6c3c 100644 --- a/src/mp3/mp3-demuxer.ts +++ b/src/mp3/mp3-demuxer.ts @@ -293,10 +293,6 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking { return false; } - getGroupId() { - return this.getId(); - } - getPairingMask() { return 1n; } diff --git a/src/mpeg-ts/mpeg-ts-demuxer.ts b/src/mpeg-ts/mpeg-ts-demuxer.ts index 48709c2..02f01e8 100644 --- a/src/mpeg-ts/mpeg-ts-demuxer.ts +++ b/src/mpeg-ts/mpeg-ts-demuxer.ts @@ -45,6 +45,7 @@ import { InputAudioTrackBacking, InputTrack, InputTrackBacking, + InputVideoTrack, InputVideoTrackBacking, } from '../input-track'; @@ -1099,10 +1100,6 @@ abstract class MpegTsTrackBacking implements InputTrackBacking { return false; } - getGroupId() { - return this.getId(); - } - getPairingMask() { return 1n; } diff --git a/src/ogg/ogg-demuxer.ts b/src/ogg/ogg-demuxer.ts index 52fa2e6..dd171b9 100644 --- a/src/ogg/ogg-demuxer.ts +++ b/src/ogg/ogg-demuxer.ts @@ -452,10 +452,6 @@ class OggAudioTrackBacking implements InputAudioTrackBacking { return false; } - getGroupId() { - return this.getId(); - } - getPairingMask() { return 1n; } diff --git a/src/output.ts b/src/output.ts index 3feae77..59e90ec 100644 --- a/src/output.ts +++ b/src/output.ts @@ -83,6 +83,36 @@ export class OutputTrackGroup { } } +export const outputTracksArePairable = (a: OutputTrack, b: OutputTrack) => { + if (a === b) { + return false; + } + + const aGroups = Array.isArray(a.metadata.group) + ? a.metadata.group + : [a.metadata.group!]; + const bGroups = Array.isArray(b.metadata.group) + ? b.metadata.group + : [b.metadata.group!]; + + for (const aGroup of aGroups) { + const pairableInSameGroup = a.type !== b.type + && bGroups.some(bGroup => aGroup === bGroup); + if (pairableInSameGroup) { + return true; + } + + const pairableAcrossGroups = bGroups.some( + bGroup => aGroup._pairedGroups.has(bGroup), + ); + if (pairableAcrossGroups) { + return true; + } + } + + return false; +}; + /** * Base track metadata, applicable to all tracks. * @group Output files diff --git a/src/segmented-input.ts b/src/segmented-input.ts index 2c4df88..a553897 100644 --- a/src/segmented-input.ts +++ b/src/segmented-input.ts @@ -15,6 +15,7 @@ import { InputAudioTrackBacking, InputTrack, InputTrackBacking, + InputVideoTrack, InputVideoTrackBacking, } from './input-track'; @@ -222,11 +223,7 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking { return this.firstInputTrack._backing.getId(); } - getGroupId(): number { - return this.firstInputTrack._backing.getGroupId(); - } - - getPairingMask(): bigint { + getPairingMask() { return this.firstInputTrack._backing.getPairingMask(); } diff --git a/src/wave/wave-demuxer.ts b/src/wave/wave-demuxer.ts index 946d0f2..4376ee3 100644 --- a/src/wave/wave-demuxer.ts +++ b/src/wave/wave-demuxer.ts @@ -405,10 +405,6 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking { return false; } - getGroupId() { - return this.getId(); - } - getPairingMask() { return 1n; } diff --git a/test/node/hls-input.test.ts b/test/node/hls-input.test.ts index 3db2d0b..58680b8 100644 --- a/test/node/hls-input.test.ts +++ b/test/node/hls-input.test.ts @@ -36,8 +36,6 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(videoTracks[0]!.displayHeight).toBe(720); expect(videoTracks[0]!.bitrate).toBe(2149280); expect(videoTracks[0]!.name).toBe('720'); - expect(videoTracks[0]!.groupId).toBe(1); - expect(videoTracks[0]!.pairingMask).toBe(1n << 0n); expect(videoTracks[0]!.isHydrated).toBe(false); expect(videoTracks[0]!.id).toBe(2); expect(() => videoTracks[0]?.codedWidth).toThrow(); @@ -48,8 +46,6 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(videoTracks[1]!.displayHeight).toBe(184); expect(videoTracks[1]!.bitrate).toBe(246440); expect(videoTracks[1]!.name).toBe('240'); - expect(videoTracks[1]!.groupId).toBe(1); - expect(videoTracks[1]!.pairingMask).toBe(1n << 1n); expect(videoTracks[1]!.isHydrated).toBe(false); expect(videoTracks[1]!.id).toBe(4); @@ -59,8 +55,6 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(videoTracks[2]!.displayHeight).toBe(288); expect(videoTracks[2]!.bitrate).toBe(460560); expect(videoTracks[2]!.name).toBe('380'); - expect(videoTracks[2]!.groupId).toBe(1); - expect(videoTracks[2]!.pairingMask).toBe(1n << 2n); expect(videoTracks[2]!.isHydrated).toBe(false); expect(videoTracks[2]!.id).toBe(6); @@ -70,8 +64,6 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(videoTracks[3]!.displayHeight).toBe(480); expect(videoTracks[3]!.bitrate).toBe(836280); expect(videoTracks[3]!.name).toBe('480'); - expect(videoTracks[3]!.groupId).toBe(1); - expect(videoTracks[3]!.pairingMask).toBe(1n << 3n); expect(videoTracks[3]!.isHydrated).toBe(false); expect(videoTracks[3]!.id).toBe(8); @@ -81,8 +73,6 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(videoTracks[4]!.displayHeight).toBe(1080); expect(videoTracks[4]!.bitrate).toBe(6221600); expect(videoTracks[4]!.name).toBe('1080'); - expect(videoTracks[4]!.groupId).toBe(1); - expect(videoTracks[4]!.pairingMask).toBe(1n << 4n); expect(videoTracks[4]!.isHydrated).toBe(false); expect(videoTracks[4]!.id).toBe(10); @@ -90,8 +80,6 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(await audioTracks[0]!.getCodecParameterString()).toBe('mp4a.40.2'); expect(audioTracks[0]!.bitrate).toBe(2149280); expect(audioTracks[0]!.name).toBe('720'); - expect(audioTracks[0]!.groupId).toBe(2); - expect(audioTracks[0]!.pairingMask).toBe(1n << 0n); expect(audioTracks[0]!.isHydrated).toBe(false); expect(audioTracks[0]!.id).toBe(1); expect(() => audioTracks[0]?.numberOfChannels).toThrow(); @@ -101,8 +89,6 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(await audioTracks[1]!.getCodecParameterString()).toBe('mp4a.40.5'); expect(audioTracks[1]!.bitrate).toBe(246440); expect(audioTracks[1]!.name).toBe('240'); - expect(audioTracks[1]!.groupId).toBe(2); - expect(audioTracks[1]!.pairingMask).toBe(1n << 1n); expect(audioTracks[1]!.isHydrated).toBe(false); expect(audioTracks[1]!.id).toBe(3); @@ -110,8 +96,6 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(await audioTracks[2]!.getCodecParameterString()).toBe('mp4a.40.5'); expect(audioTracks[2]!.bitrate).toBe(460560); expect(audioTracks[2]!.name).toBe('380'); - expect(audioTracks[2]!.groupId).toBe(2); - expect(audioTracks[2]!.pairingMask).toBe(1n << 2n); expect(audioTracks[2]!.isHydrated).toBe(false); expect(audioTracks[2]!.id).toBe(5); @@ -119,8 +103,6 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(await audioTracks[3]!.getCodecParameterString()).toBe('mp4a.40.2'); expect(audioTracks[3]!.bitrate).toBe(836280); expect(audioTracks[3]!.name).toBe('480'); - expect(audioTracks[3]!.groupId).toBe(2); - expect(audioTracks[3]!.pairingMask).toBe(1n << 3n); expect(audioTracks[3]!.isHydrated).toBe(false); expect(audioTracks[3]!.id).toBe(7); @@ -128,14 +110,20 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => { expect(await audioTracks[4]!.getCodecParameterString()).toBe('mp4a.40.2'); expect(audioTracks[4]!.bitrate).toBe(6221600); expect(audioTracks[4]!.name).toBe('1080'); - expect(audioTracks[4]!.groupId).toBe(2); - expect(audioTracks[4]!.pairingMask).toBe(1n << 4n); expect(audioTracks[4]!.isHydrated).toBe(false); expect(audioTracks[4]!.id).toBe(9); - expect(videoTracks[0]!.canBePairedWith(videoTracks[1]!)).toBe(false); - expect(videoTracks[0]!.canBePairedWith(audioTracks[0]!)).toBe(true); - expect(videoTracks[0]!.canBePairedWith(audioTracks[1]!)).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 < videoTracks.length; i++) { + for (let j = 0; j < audioTracks.length; j++) { + expect(videoTracks[i]!.canBePairedWith(audioTracks[j]!)).toBe(i === j); + } + } const primaryVideoTrack = await input.getPrimaryVideoTrack(); const primaryAudioTrack = await input.getPrimaryAudioTrack(); @@ -608,7 +596,7 @@ test.concurrent('Advanced Apple HLS', { timeout: 30_000 }, async () => { const audioTracks = tracks.filter(x => x.isAudioTrack()); expect(audioTracks).toHaveLength(3); - expect(videoTracks).toHaveLength(64); + expect(videoTracks).toHaveLength(28); expect(audioTracks[0]).toMatchObject({ codec: 'aac', name: 'English', languageCode: 'en' }); expect(audioTracks[1]).toMatchObject({ codec: 'ac3', name: 'English', languageCode: 'en' }); @@ -622,7 +610,7 @@ test.concurrent('Advanced Apple HLS', { timeout: 30_000 }, async () => { expect(await audioTracks[1]!.getPairableVideoTracks()).toHaveLength(18); expect(await audioTracks[2]!.getPairableVideoTracks()).toHaveLength(18); - // I-FRAME AVC (5 tracks) + // I-FRAME AVC expect(videoTracks[0]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 1015727, hasOnlyKeyPackets: true }); expect(videoTracks[1]).toMatchObject({ codec: 'avc', displayWidth: 1280, displayHeight: 720, bitrate: 760174, hasOnlyKeyPackets: true }); @@ -636,17 +624,22 @@ test.concurrent('Advanced Apple HLS', { timeout: 30_000 }, async () => { expect(await videoTracks[3]!.getCodecParameterString()).toBe('avc1.64001f'); expect(await videoTracks[4]!.getCodecParameterString()).toBe('avc1.64001f'); - // AVC + audio group a1 (9 tracks) + for (let i = 0; i <= 4; i++) { + const videoTrack = videoTracks[i]!; + expect(audioTracks.some(x => x.canBePairedWith(videoTrack))).toBe(false); + } - expect(videoTracks[5]).toMatchObject({ codec: 'avc', displayWidth: 960, displayHeight: 540, bitrate: 2523597, hasOnlyKeyPackets: false }); - expect(videoTracks[6]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 9873268, hasOnlyKeyPackets: false }); - expect(videoTracks[7]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 7318337, hasOnlyKeyPackets: false }); - expect(videoTracks[8]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 5421720, hasOnlyKeyPackets: false }); - expect(videoTracks[9]).toMatchObject({ codec: 'avc', displayWidth: 1280, displayHeight: 720, bitrate: 3611257, hasOnlyKeyPackets: false }); - expect(videoTracks[10]).toMatchObject({ codec: 'avc', displayWidth: 768, displayHeight: 432, bitrate: 1475903, hasOnlyKeyPackets: false }); - expect(videoTracks[11]).toMatchObject({ codec: 'avc', displayWidth: 640, displayHeight: 360, bitrate: 1017705, hasOnlyKeyPackets: false }); - expect(videoTracks[12]).toMatchObject({ codec: 'avc', displayWidth: 480, displayHeight: 270, bitrate: 582820, hasOnlyKeyPackets: false }); - expect(videoTracks[13]).toMatchObject({ codec: 'avc', displayWidth: 416, displayHeight: 234, bitrate: 339404, hasOnlyKeyPackets: false }); + // AVC + + expect(videoTracks[5]).toMatchObject({ codec: 'avc', displayWidth: 960, displayHeight: 540, bitrate: 2746096, hasOnlyKeyPackets: false }); + expect(videoTracks[6]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 10095767, hasOnlyKeyPackets: false }); + expect(videoTracks[7]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 7540836, hasOnlyKeyPackets: false }); + expect(videoTracks[8]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 5644219, hasOnlyKeyPackets: false }); + expect(videoTracks[9]).toMatchObject({ codec: 'avc', displayWidth: 1280, displayHeight: 720, bitrate: 3833756, hasOnlyKeyPackets: false }); + expect(videoTracks[10]).toMatchObject({ codec: 'avc', displayWidth: 768, displayHeight: 432, bitrate: 1698402, hasOnlyKeyPackets: false }); + expect(videoTracks[11]).toMatchObject({ codec: 'avc', displayWidth: 640, displayHeight: 360, bitrate: 1240204, hasOnlyKeyPackets: false }); + expect(videoTracks[12]).toMatchObject({ codec: 'avc', displayWidth: 480, displayHeight: 270, bitrate: 805319, hasOnlyKeyPackets: false }); + expect(videoTracks[13]).toMatchObject({ codec: 'avc', displayWidth: 416, displayHeight: 234, bitrate: 561903, hasOnlyKeyPackets: false }); expect(await videoTracks[5]!.getCodecParameterString()).toBe('avc1.640020'); expect(await videoTracks[6]!.getCodecParameterString()).toBe('avc1.64002a'); @@ -658,129 +651,56 @@ test.concurrent('Advanced Apple HLS', { timeout: 30_000 }, async () => { expect(await videoTracks[12]!.getCodecParameterString()).toBe('avc1.64001f'); expect(await videoTracks[13]!.getCodecParameterString()).toBe('avc1.64001f'); - // AVC + audio group a2 (9 tracks) + for (let i = 5; i <= 13; i++) { + const videoTrack = videoTracks[i]!; + expect(audioTracks.every(x => x.canBePairedWith(videoTrack))).toBe(true); + } - expect(videoTracks[14]).toMatchObject({ codec: 'avc', displayWidth: 960, displayHeight: 540, bitrate: 2746096, hasOnlyKeyPackets: false }); - expect(videoTracks[15]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 10095767, hasOnlyKeyPackets: false }); - expect(videoTracks[16]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 7540836, hasOnlyKeyPackets: false }); - expect(videoTracks[17]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 5644219, hasOnlyKeyPackets: false }); - expect(videoTracks[18]).toMatchObject({ codec: 'avc', displayWidth: 1280, displayHeight: 720, bitrate: 3833756, hasOnlyKeyPackets: false }); - expect(videoTracks[19]).toMatchObject({ codec: 'avc', displayWidth: 768, displayHeight: 432, bitrate: 1698402, hasOnlyKeyPackets: false }); - expect(videoTracks[20]).toMatchObject({ codec: 'avc', displayWidth: 640, displayHeight: 360, bitrate: 1240204, hasOnlyKeyPackets: false }); - expect(videoTracks[21]).toMatchObject({ codec: 'avc', displayWidth: 480, displayHeight: 270, bitrate: 805319, hasOnlyKeyPackets: false }); - expect(videoTracks[22]).toMatchObject({ codec: 'avc', displayWidth: 416, displayHeight: 234, bitrate: 561903, hasOnlyKeyPackets: false }); + // I-FRAME HEVC - expect(await videoTracks[14]!.getCodecParameterString()).toBe('avc1.640020'); - expect(await videoTracks[15]!.getCodecParameterString()).toBe('avc1.64002a'); - expect(await videoTracks[16]!.getCodecParameterString()).toBe('avc1.64002a'); - expect(await videoTracks[17]!.getCodecParameterString()).toBe('avc1.64002a'); - expect(await videoTracks[18]!.getCodecParameterString()).toBe('avc1.640020'); - expect(await videoTracks[19]!.getCodecParameterString()).toBe('avc1.64001f'); - expect(await videoTracks[20]!.getCodecParameterString()).toBe('avc1.64001f'); - expect(await videoTracks[21]!.getCodecParameterString()).toBe('avc1.64001f'); - expect(await videoTracks[22]!.getCodecParameterString()).toBe('avc1.64001f'); + expect(videoTracks[14]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 328352, hasOnlyKeyPackets: true }); + expect(videoTracks[15]).toMatchObject({ codec: 'hevc', displayWidth: 1280, displayHeight: 720, bitrate: 226274, hasOnlyKeyPackets: true }); + expect(videoTracks[16]).toMatchObject({ codec: 'hevc', displayWidth: 960, displayHeight: 540, bitrate: 159037, hasOnlyKeyPackets: true }); + expect(videoTracks[17]).toMatchObject({ codec: 'hevc', displayWidth: 640, displayHeight: 360, bitrate: 92800, hasOnlyKeyPackets: true }); + expect(videoTracks[18]).toMatchObject({ codec: 'hevc', displayWidth: 480, displayHeight: 270, bitrate: 51760, hasOnlyKeyPackets: true }); - // AVC + audio group a3 (9 tracks) + expect(await videoTracks[14]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[15]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[16]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[17]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[18]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(videoTracks[23]).toMatchObject({ codec: 'avc', displayWidth: 960, displayHeight: 540, bitrate: 2554096, hasOnlyKeyPackets: false }); - expect(videoTracks[24]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 9903767, hasOnlyKeyPackets: false }); - expect(videoTracks[25]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 7348836, hasOnlyKeyPackets: false }); - expect(videoTracks[26]).toMatchObject({ codec: 'avc', displayWidth: 1920, displayHeight: 1080, bitrate: 5452219, hasOnlyKeyPackets: false }); - expect(videoTracks[27]).toMatchObject({ codec: 'avc', displayWidth: 1280, displayHeight: 720, bitrate: 3641756, hasOnlyKeyPackets: false }); - expect(videoTracks[28]).toMatchObject({ codec: 'avc', displayWidth: 768, displayHeight: 432, bitrate: 1506402, hasOnlyKeyPackets: false }); - expect(videoTracks[29]).toMatchObject({ codec: 'avc', displayWidth: 640, displayHeight: 360, bitrate: 1048204, hasOnlyKeyPackets: false }); - expect(videoTracks[30]).toMatchObject({ codec: 'avc', displayWidth: 480, displayHeight: 270, bitrate: 613319, hasOnlyKeyPackets: false }); - expect(videoTracks[31]).toMatchObject({ codec: 'avc', displayWidth: 416, displayHeight: 234, bitrate: 369903, hasOnlyKeyPackets: false }); + for (let i = 14; i <= 18; i++) { + const videoTrack = videoTracks[i]!; + expect(audioTracks.some(x => x.canBePairedWith(videoTrack))).toBe(false); + } - expect(await videoTracks[23]!.getCodecParameterString()).toBe('avc1.640020'); - expect(await videoTracks[24]!.getCodecParameterString()).toBe('avc1.64002a'); - expect(await videoTracks[25]!.getCodecParameterString()).toBe('avc1.64002a'); - expect(await videoTracks[26]!.getCodecParameterString()).toBe('avc1.64002a'); - expect(await videoTracks[27]!.getCodecParameterString()).toBe('avc1.640020'); - expect(await videoTracks[28]!.getCodecParameterString()).toBe('avc1.64001f'); - expect(await videoTracks[29]!.getCodecParameterString()).toBe('avc1.64001f'); - expect(await videoTracks[30]!.getCodecParameterString()).toBe('avc1.64001f'); - expect(await videoTracks[31]!.getCodecParameterString()).toBe('avc1.64001f'); + // HEVC - // I-FRAME HEVC (5 tracks) + expect(videoTracks[19]).toMatchObject({ codec: 'hevc', displayWidth: 960, displayHeight: 540, bitrate: 2386827, hasOnlyKeyPackets: false }); + expect(videoTracks[20]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 6886727, hasOnlyKeyPackets: false }); + expect(videoTracks[21]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 5650398, hasOnlyKeyPackets: false }); + expect(videoTracks[22]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 4302269, hasOnlyKeyPackets: false }); + expect(videoTracks[23]).toMatchObject({ codec: 'hevc', displayWidth: 1280, displayHeight: 720, bitrate: 2987200, hasOnlyKeyPackets: false }); + expect(videoTracks[24]).toMatchObject({ codec: 'hevc', displayWidth: 768, displayHeight: 432, bitrate: 1448754, hasOnlyKeyPackets: false }); + expect(videoTracks[25]).toMatchObject({ codec: 'hevc', displayWidth: 640, displayHeight: 360, bitrate: 1124269, hasOnlyKeyPackets: false }); + expect(videoTracks[26]).toMatchObject({ codec: 'hevc', displayWidth: 480, displayHeight: 270, bitrate: 771426, hasOnlyKeyPackets: false }); + expect(videoTracks[27]).toMatchObject({ codec: 'hevc', displayWidth: 416, displayHeight: 234, bitrate: 563212, hasOnlyKeyPackets: false }); - expect(videoTracks[32]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 328352, hasOnlyKeyPackets: true }); - expect(videoTracks[33]).toMatchObject({ codec: 'hevc', displayWidth: 1280, displayHeight: 720, bitrate: 226274, hasOnlyKeyPackets: true }); - expect(videoTracks[34]).toMatchObject({ codec: 'hevc', displayWidth: 960, displayHeight: 540, bitrate: 159037, hasOnlyKeyPackets: true }); - expect(videoTracks[35]).toMatchObject({ codec: 'hevc', displayWidth: 640, displayHeight: 360, bitrate: 92800, hasOnlyKeyPackets: true }); - expect(videoTracks[36]).toMatchObject({ codec: 'hevc', displayWidth: 480, displayHeight: 270, bitrate: 51760, hasOnlyKeyPackets: true }); + expect(await videoTracks[19]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[20]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[21]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[22]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[23]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[24]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[25]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[26]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + expect(await videoTracks[27]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[32]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[33]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[34]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[35]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[36]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - - // HEVC + audio group a1 (9 tracks) - - expect(videoTracks[37]).toMatchObject({ codec: 'hevc', displayWidth: 960, displayHeight: 540, bitrate: 2164328, hasOnlyKeyPackets: false }); - expect(videoTracks[38]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 6664228, hasOnlyKeyPackets: false }); - expect(videoTracks[39]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 5427899, hasOnlyKeyPackets: false }); - expect(videoTracks[40]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 4079770, hasOnlyKeyPackets: false }); - expect(videoTracks[41]).toMatchObject({ codec: 'hevc', displayWidth: 1280, displayHeight: 720, bitrate: 2764701, hasOnlyKeyPackets: false }); - expect(videoTracks[42]).toMatchObject({ codec: 'hevc', displayWidth: 768, displayHeight: 432, bitrate: 1226255, hasOnlyKeyPackets: false }); - expect(videoTracks[43]).toMatchObject({ codec: 'hevc', displayWidth: 640, displayHeight: 360, bitrate: 901770, hasOnlyKeyPackets: false }); - expect(videoTracks[44]).toMatchObject({ codec: 'hevc', displayWidth: 480, displayHeight: 270, bitrate: 548927, hasOnlyKeyPackets: false }); - expect(videoTracks[45]).toMatchObject({ codec: 'hevc', displayWidth: 416, displayHeight: 234, bitrate: 340713, hasOnlyKeyPackets: false }); - - expect(await videoTracks[37]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[38]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[39]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[40]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[41]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[42]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[43]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[44]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[45]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - - // HEVC + audio group a2 (9 tracks) - - expect(videoTracks[46]).toMatchObject({ codec: 'hevc', displayWidth: 960, displayHeight: 540, bitrate: 2386827, hasOnlyKeyPackets: false }); - expect(videoTracks[47]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 6886727, hasOnlyKeyPackets: false }); - expect(videoTracks[48]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 5650398, hasOnlyKeyPackets: false }); - expect(videoTracks[49]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 4302269, hasOnlyKeyPackets: false }); - expect(videoTracks[50]).toMatchObject({ codec: 'hevc', displayWidth: 1280, displayHeight: 720, bitrate: 2987200, hasOnlyKeyPackets: false }); - expect(videoTracks[51]).toMatchObject({ codec: 'hevc', displayWidth: 768, displayHeight: 432, bitrate: 1448754, hasOnlyKeyPackets: false }); - expect(videoTracks[52]).toMatchObject({ codec: 'hevc', displayWidth: 640, displayHeight: 360, bitrate: 1124269, hasOnlyKeyPackets: false }); - expect(videoTracks[53]).toMatchObject({ codec: 'hevc', displayWidth: 480, displayHeight: 270, bitrate: 771426, hasOnlyKeyPackets: false }); - expect(videoTracks[54]).toMatchObject({ codec: 'hevc', displayWidth: 416, displayHeight: 234, bitrate: 563212, hasOnlyKeyPackets: false }); - - expect(await videoTracks[46]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[47]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[48]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[49]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[50]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[51]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[52]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[53]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[54]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - - // HEVC + audio group a3 (9 tracks) - - expect(videoTracks[55]).toMatchObject({ codec: 'hevc', displayWidth: 960, displayHeight: 540, bitrate: 2194827, hasOnlyKeyPackets: false }); - expect(videoTracks[56]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 6694727, hasOnlyKeyPackets: false }); - expect(videoTracks[57]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 5458398, hasOnlyKeyPackets: false }); - expect(videoTracks[58]).toMatchObject({ codec: 'hevc', displayWidth: 1920, displayHeight: 1080, bitrate: 4110269, hasOnlyKeyPackets: false }); - expect(videoTracks[59]).toMatchObject({ codec: 'hevc', displayWidth: 1280, displayHeight: 720, bitrate: 2795200, hasOnlyKeyPackets: false }); - expect(videoTracks[60]).toMatchObject({ codec: 'hevc', displayWidth: 768, displayHeight: 432, bitrate: 1256754, hasOnlyKeyPackets: false }); - expect(videoTracks[61]).toMatchObject({ codec: 'hevc', displayWidth: 640, displayHeight: 360, bitrate: 932269, hasOnlyKeyPackets: false }); - expect(videoTracks[62]).toMatchObject({ codec: 'hevc', displayWidth: 480, displayHeight: 270, bitrate: 579426, hasOnlyKeyPackets: false }); - expect(videoTracks[63]).toMatchObject({ codec: 'hevc', displayWidth: 416, displayHeight: 234, bitrate: 371212, hasOnlyKeyPackets: false }); - - expect(await videoTracks[55]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[56]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[57]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[58]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[59]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[60]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[61]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[62]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); - expect(await videoTracks[63]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0'); + for (let i = 19; i <= 27; i++) { + const videoTrack = videoTracks[i]!; + expect(audioTracks.every(x => x.canBePairedWith(videoTrack))).toBe(true); + } }); test.concurrent('Live HLS', { timeout: 30_000 }, async () => {