Add HLS master playlist generation, add multi-playlistadd output track grouping, add multi-playlist HLS files, add configurable playlist and segment names, add TrackDisposition.primary

This commit is contained in:
Vanilagy
2026-03-24 15:53:21 +01:00
parent c91718cdba
commit 3e87d747e6
17 changed files with 1777 additions and 117 deletions
+40
View File
@@ -414,6 +414,46 @@ test.concurrent('fMP4', { timeout: 15_000 }, async () => {
expect(sourceCount).toBe(1 + 2 * (1 + 1 + 1 + 1));
});
test.concurrent('Track disposition & metadata', { timeout: 15_000 }, async () => {
using input = new Input({
entryPath: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8',
source: ({ path }) => new UrlSource(path),
formats: ALL_FORMATS,
});
const audioTracks = await input.getAudioTracks();
expect(audioTracks).toHaveLength(6);
expect(audioTracks[0]!.languageCode).toBe('en');
expect(audioTracks[1]!.languageCode).toBe('de');
expect(audioTracks[2]!.languageCode).toBe('it');
expect(audioTracks[3]!.languageCode).toBe('fr');
expect(audioTracks[4]!.languageCode).toBe('es');
expect(audioTracks[5]!.languageCode).toBe('en');
expect(audioTracks[0]!.disposition.primary).toBe(true);
expect(audioTracks[1]!.disposition.primary).toBe(false);
expect(audioTracks[2]!.disposition.primary).toBe(false);
expect(audioTracks[3]!.disposition.primary).toBe(false);
expect(audioTracks[4]!.disposition.primary).toBe(false);
expect(audioTracks[5]!.disposition.primary).toBe(false);
expect(audioTracks[0]!.disposition.default).toBe(true);
expect(audioTracks[1]!.disposition.default).toBe(true);
expect(audioTracks[2]!.disposition.default).toBe(true);
expect(audioTracks[3]!.disposition.default).toBe(true);
expect(audioTracks[4]!.disposition.default).toBe(true);
expect(audioTracks[5]!.disposition.default).toBe(false);
expect(audioTracks[0]!.name).toBe('stream_5');
expect(audioTracks[1]!.name).toBe('stream_4');
expect(audioTracks[2]!.name).toBe('stream_8');
expect(audioTracks[3]!.name).toBe('stream_7');
expect(audioTracks[4]!.name).toBe('stream_9');
expect(audioTracks[5]!.name).toBe('stream_6');
});
test.concurrent('fMP4 Bitmovin', { timeout: 15_000 }, async () => {
using input = new Input({
entryPath: 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s-fmp4/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8',
+793
View File
@@ -0,0 +1,793 @@
import { expect, test, vi } from 'vitest';
import { Output, OutputTrackGroup } from '../../src/output.js';
import { HLS_OUTPUT_FORMATS_DEFAULT, HlsOutputFormat } from '../../src/output-format.js';
import { NullTarget } from '../../src/target.js';
import { EncodedAudioPacketSource, EncodedVideoPacketSource } from '../../src/media-source.js';
import { HlsMuxer } from '../../src/hls/hls-muxer.js';
import { AudioCodec, VideoCodec } from '../../src/codec.js';
const videoSource = (codec: VideoCodec = 'avc') => new EncodedVideoPacketSource(codec);
const audioSource = (codec: AudioCodec = 'aac') => new EncodedAudioPacketSource(codec);
test('Playlist assignment, single video', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addVideoTrack(videoSource());
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(1);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.groupId).toBeNull();
expect(decl[0]!.references).toHaveLength(0);
});
test('Playlist assignment, single audio', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addAudioTrack(audioSource());
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(1);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.groupId).toBeNull();
expect(decl[0]!.references).toHaveLength(0);
});
test('Playlist assignment, multiple video', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addVideoTrack(videoSource());
output.addVideoTrack(videoSource());
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(2);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.groupId).toBeNull();
expect(decl[0]!.references).toHaveLength(0);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.groupId).toBeNull();
expect(decl[1]!.references).toHaveLength(0);
});
test('Playlist assignment, multiple audio', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addAudioTrack(audioSource());
output.addAudioTrack(audioSource());
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(2);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.groupId).toBeNull();
expect(decl[0]!.references).toHaveLength(0);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.groupId).toBeNull();
expect(decl[1]!.references).toHaveLength(0);
});
test('Playlist assignment, multiple video with different metadata #1', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addVideoTrack(videoSource(), { languageCode: 'eng' });
output.addVideoTrack(videoSource(), { languageCode: 'esp' });
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(3);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.groupId).toBe('video-1');
expect(decl[0]!.references).toHaveLength(0);
expect(decl[0]!.noUri).toBe(true);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.groupId).toBe('video-1');
expect(decl[1]!.references).toHaveLength(0);
expect(decl[1]!.noUri).toBe(false);
expect(decl[2]!.playlist.tracks).toHaveLength(1);
expect(decl[2]!.groupId).toBeNull();
expect(decl[2]!.references).toEqual(decl.slice(0, 2));
expect(decl[2]!.playlist).toBe(decl[0]!.playlist);
});
test('Playlist assignment, multiple video with different metadata #2', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addVideoTrack(videoSource(), { disposition: { primary: true } });
output.addVideoTrack(videoSource(), { disposition: { primary: false } });
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(3);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.groupId).toBe('video-1');
expect(decl[0]!.references).toHaveLength(0);
expect(decl[0]!.noUri).toBe(true);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.groupId).toBe('video-1');
expect(decl[1]!.references).toHaveLength(0);
expect(decl[1]!.noUri).toBe(false);
expect(decl[2]!.playlist.tracks).toHaveLength(1);
expect(decl[2]!.groupId).toBeNull();
expect(decl[2]!.references).toEqual(decl.slice(0, 2));
expect(decl[2]!.playlist).toBe(decl[0]!.playlist);
});
test('Playlist assignment, multiple audio with different metadata', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addAudioTrack(audioSource(), { languageCode: 'eng' });
output.addAudioTrack(audioSource(), { languageCode: 'esp' });
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(3);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.groupId).toBe('audio-1');
expect(decl[0]!.references).toHaveLength(0);
expect(decl[0]!.noUri).toBe(true);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.groupId).toBe('audio-1');
expect(decl[1]!.references).toHaveLength(0);
expect(decl[1]!.noUri).toBe(false);
expect(decl[2]!.playlist.tracks).toHaveLength(1);
expect(decl[2]!.groupId).toBeNull();
expect(decl[2]!.references).toEqual(decl.slice(0, 2));
expect(decl[2]!.playlist).toBe(decl[0]!.playlist);
});
test('Playlist assignment, video and audio', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addVideoTrack(videoSource());
output.addAudioTrack(audioSource());
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(1);
expect(decl[0]!.playlist.tracks).toHaveLength(2);
expect(decl[0]!.groupId).toBeNull();
expect(decl[0]!.references).toHaveLength(0);
});
test('Playlist assignment, one video and multiple audio', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addVideoTrack(videoSource());
output.addAudioTrack(audioSource());
output.addAudioTrack(audioSource());
output.addAudioTrack(audioSource());
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(4);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[0]!.groupId).toBe('audio-1');
expect(decl[0]!.noUri).toBe(false);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[0]!.groupId).toBe('audio-1');
expect(decl[1]!.noUri).toBe(false);
expect(decl[2]!.playlist.tracks).toHaveLength(1);
expect(decl[2]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[0]!.groupId).toBe('audio-1');
expect(decl[2]!.noUri).toBe(false);
expect(decl[3]!.playlist.tracks).toHaveLength(1);
expect(decl[3]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[3]!.groupId).toBeNull();
expect(decl[3]!.references).toEqual(decl.slice(0, 3));
});
test('Playlist assignment, multiple video and one audio', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addVideoTrack(videoSource());
output.addVideoTrack(videoSource());
output.addVideoTrack(videoSource());
output.addAudioTrack(audioSource());
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(4);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[0]!.groupId).toBe('audio-1');
expect(decl[0]!.noUri).toBe(false);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[1]!.groupId).toBeNull();
expect(decl[1]!.references).toEqual([decl[0]!]);
expect(decl[2]!.playlist.tracks).toHaveLength(1);
expect(decl[2]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[2]!.groupId).toBeNull();
expect(decl[2]!.references).toEqual([decl[0]!]);
expect(decl[3]!.playlist.tracks).toHaveLength(1);
expect(decl[3]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[3]!.groupId).toBeNull();
expect(decl[3]!.references).toEqual([decl[0]!]);
});
test('Playlist assignment, multiple video and audio', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addVideoTrack(videoSource());
output.addVideoTrack(videoSource());
output.addVideoTrack(videoSource());
output.addAudioTrack(audioSource());
output.addAudioTrack(audioSource());
output.addAudioTrack(audioSource());
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(6);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[0]!.groupId).toBe('audio-1');
expect(decl[0]!.noUri).toBe(false);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[1]!.groupId).toBe('audio-1');
expect(decl[1]!.noUri).toBe(false);
expect(decl[2]!.playlist.tracks).toHaveLength(1);
expect(decl[2]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[2]!.groupId).toBe('audio-1');
expect(decl[2]!.noUri).toBe(false);
expect(decl[3]!.playlist.tracks).toHaveLength(1);
expect(decl[3]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[3]!.groupId).toBeNull();
expect(decl[3]!.references).toEqual(decl.slice(0, 3));
expect(decl[4]!.playlist.tracks).toHaveLength(1);
expect(decl[4]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[4]!.groupId).toBeNull();
expect(decl[4]!.references).toEqual(decl.slice(0, 3));
expect(decl[5]!.playlist.tracks).toHaveLength(1);
expect(decl[5]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[5]!.groupId).toBeNull();
expect(decl[5]!.references).toEqual(decl.slice(0, 3));
});
test('Playlist assignment, video and audio in different groups', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
const a = new OutputTrackGroup();
const b = new OutputTrackGroup();
output.addVideoTrack(videoSource(), { group: a });
output.addAudioTrack(audioSource(), { group: b });
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(2);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[0]!.groupId).toBeNull();
expect(decl[0]!.references).toHaveLength(0);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[1]!.groupId).toBeNull();
expect(decl[1]!.references).toHaveLength(0);
});
test('Playlist assignment, multiple video and audio in pairs', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
const a = new OutputTrackGroup();
const b = new OutputTrackGroup();
const c = new OutputTrackGroup();
output.addVideoTrack(videoSource(), { group: a });
output.addVideoTrack(videoSource(), { group: b });
output.addVideoTrack(videoSource(), { group: c });
output.addAudioTrack(audioSource(), { group: a });
output.addAudioTrack(audioSource(), { group: b });
output.addAudioTrack(audioSource(), { group: c });
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(3);
expect(decl[0]!.playlist.tracks).toHaveLength(2);
expect(decl[0]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[0]!.playlist.tracks[1]!.type).toBe('audio');
expect(decl[0]!.groupId).toBeNull();
expect(decl[0]!.references).toHaveLength(0);
expect(decl[1]!.playlist.tracks).toHaveLength(2);
expect(decl[1]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[1]!.playlist.tracks[1]!.type).toBe('audio');
expect(decl[1]!.groupId).toBeNull();
expect(decl[1]!.references).toHaveLength(0);
expect(decl[2]!.playlist.tracks).toHaveLength(2);
expect(decl[2]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[2]!.playlist.tracks[1]!.type).toBe('audio');
expect(decl[2]!.groupId).toBeNull();
expect(decl[2]!.references).toHaveLength(0);
});
test('Playlist assignment, multiple video and audio with some unpaired', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
const a = new OutputTrackGroup();
const b = new OutputTrackGroup();
const c = new OutputTrackGroup();
output.addVideoTrack(videoSource(), { group: a });
output.addVideoTrack(videoSource(), { group: a });
output.addVideoTrack(videoSource(), { group: b });
output.addAudioTrack(audioSource(), { group: a });
output.addAudioTrack(audioSource(), { group: a });
output.addAudioTrack(audioSource(), { group: c });
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(6);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[0]!.groupId).toBe('audio-1');
expect(decl[0]!.noUri).toBe(false);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[1]!.groupId).toBe('audio-1');
expect(decl[1]!.noUri).toBe(false);
expect(decl[2]!.playlist.tracks).toHaveLength(1);
expect(decl[2]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[2]!.groupId).toBeNull();
expect(decl[2]!.references).toEqual(decl.slice(0, 2));
expect(decl[3]!.playlist.tracks).toHaveLength(1);
expect(decl[3]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[3]!.groupId).toBeNull();
expect(decl[3]!.references).toEqual(decl.slice(0, 2));
expect(decl[4]!.playlist.tracks).toHaveLength(1);
expect(decl[4]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[4]!.groupId).toBeNull();
expect(decl[4]!.references).toHaveLength(0);
expect(decl[5]!.playlist.tracks).toHaveLength(1);
expect(decl[5]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[5]!.groupId).toBeNull();
expect(decl[5]!.references).toHaveLength(0);
});
test('Playlist assignment, multiple video and audio with multiple groups', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
const a = new OutputTrackGroup();
const b = new OutputTrackGroup();
output.addVideoTrack(videoSource(), { group: a });
output.addVideoTrack(videoSource(), { group: a });
output.addVideoTrack(videoSource(), { group: b });
output.addVideoTrack(videoSource(), { group: b });
output.addAudioTrack(audioSource(), { group: a });
output.addAudioTrack(audioSource(), { group: a });
output.addAudioTrack(audioSource(), { group: b });
output.addAudioTrack(audioSource(), { group: b });
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(8);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[0]!.groupId).toBe('audio-1');
expect(decl[0]!.noUri).toBe(false);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[1]!.groupId).toBe('audio-1');
expect(decl[1]!.noUri).toBe(false);
expect(decl[2]!.playlist.tracks).toHaveLength(1);
expect(decl[2]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[2]!.groupId).toBe('audio-2');
expect(decl[2]!.noUri).toBe(false);
expect(decl[3]!.playlist.tracks).toHaveLength(1);
expect(decl[3]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[3]!.groupId).toBe('audio-2');
expect(decl[3]!.noUri).toBe(false);
expect(decl[4]!.playlist.tracks).toHaveLength(1);
expect(decl[4]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[4]!.groupId).toBeNull();
expect(decl[4]!.references).toEqual(decl.slice(0, 2));
expect(decl[5]!.playlist.tracks).toHaveLength(1);
expect(decl[5]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[5]!.groupId).toBeNull();
expect(decl[5]!.references).toEqual(decl.slice(0, 2));
expect(decl[6]!.playlist.tracks).toHaveLength(1);
expect(decl[6]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[6]!.groupId).toBeNull();
expect(decl[6]!.references).toEqual(decl.slice(2, 4));
expect(decl[7]!.playlist.tracks).toHaveLength(1);
expect(decl[7]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[7]!.groupId).toBeNull();
expect(decl[7]!.references).toEqual(decl.slice(2, 4));
});
test('Playlist assignment, video with multiple audio codecs', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addVideoTrack(videoSource());
output.addAudioTrack(audioSource('aac'));
output.addAudioTrack(audioSource('ac3'));
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(4);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[0]!.playlist.tracks[0]!.source._codec).toBe('aac');
expect(decl[0]!.groupId).toBe('audio-1');
expect(decl[0]!.noUri).toBe(false);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[1]!.playlist.tracks[0]!.source._codec).toBe('ac3');
expect(decl[1]!.groupId).toBe('audio-2');
expect(decl[1]!.noUri).toBe(false);
expect(decl[2]!.playlist.tracks).toHaveLength(1);
expect(decl[2]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[2]!.groupId).toBeNull();
expect(decl[2]!.references).toEqual([decl[0]!]);
expect(decl[3]!.playlist.tracks).toHaveLength(1);
expect(decl[3]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[3]!.groupId).toBeNull();
expect(decl[3]!.references).toEqual([decl[1]!]);
expect(decl[2]!.playlist).toBe(decl[3]!.playlist);
});
test('Playlist assignment, audio with multiple video codecs', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
output.addVideoTrack(videoSource('avc'));
output.addVideoTrack(videoSource('hevc'));
output.addAudioTrack(audioSource());
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(3);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[0]!.groupId).toBe('audio-1');
expect(decl[0]!.noUri).toBe(false);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[1]!.playlist.tracks[0]!.source._codec).toBe('avc');
expect(decl[1]!.groupId).toBeNull();
expect(decl[1]!.references).toEqual([decl[0]!]);
expect(decl[2]!.playlist.tracks).toHaveLength(1);
expect(decl[2]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[2]!.playlist.tracks[0]!.source._codec).toBe('hevc');
expect(decl[2]!.groupId).toBeNull();
expect(decl[2]!.references).toEqual([decl[0]!]);
});
test('Playlist assignment, multiple video with conflicting audio interests', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
const a = new OutputTrackGroup();
const b = new OutputTrackGroup();
output.addVideoTrack(videoSource(), { group: a });
output.addVideoTrack(videoSource(), { group: b });
output.addAudioTrack(audioSource(), { group: [a, b] });
output.addAudioTrack(audioSource(), { group: a });
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(5);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[0]!.groupId).toBe('audio-1');
expect(decl[0]!.noUri).toBe(false);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[1]!.groupId).toBe('audio-1');
expect(decl[1]!.noUri).toBe(false);
expect(decl[2]!.playlist.tracks).toHaveLength(1);
expect(decl[2]!.playlist.tracks[0]!.type).toBe('audio');
expect(decl[2]!.playlist).toBe(decl[0]!.playlist);
expect(decl[2]!.groupId).toBe('audio-2');
expect(decl[2]!.noUri).toBe(false);
expect(decl[3]!.playlist.tracks).toHaveLength(1);
expect(decl[3]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[3]!.groupId).toBeNull();
expect(decl[3]!.references).toEqual([decl[0]!, decl[1]!]);
expect(decl[4]!.playlist.tracks).toHaveLength(1);
expect(decl[4]!.playlist.tracks[0]!.type).toBe('video');
expect(decl[4]!.groupId).toBeNull();
expect(decl[4]!.references).toEqual([decl[2]!]);
});
test('Playlist assignment, video paired with video', async () => {
const consoleSpy = vi.spyOn(console, 'warn');
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
const a = new OutputTrackGroup();
const b = new OutputTrackGroup();
a.pairWith(b);
output.addVideoTrack(videoSource(), { group: a });
output.addVideoTrack(videoSource(), { group: b });
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(2);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.groupId).toBeNull();
expect(decl[0]!.references).toHaveLength(0);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.groupId).toBeNull();
expect(decl[1]!.references).toHaveLength(0);
expect(consoleSpy.mock.calls).toHaveLength(1);
expect(consoleSpy.mock.calls[0]![0]).toContain('Illegal pairing');
});
test('Playlist assignment, audio paired with audio', async () => {
const consoleSpy = vi.spyOn(console, 'warn');
const output = new Output({
format: new HlsOutputFormat({
segmentFormats: HLS_OUTPUT_FORMATS_DEFAULT,
}),
target: () => new NullTarget(),
rootPath: '',
});
const a = new OutputTrackGroup();
const b = new OutputTrackGroup();
a.pairWith(b);
output.addAudioTrack(audioSource(), { group: a });
output.addAudioTrack(audioSource(), { group: b });
await output.start();
const muxer = output._muxer as HlsMuxer;
const decl = muxer.playlistDeclarations;
expect(decl).toHaveLength(2);
expect(decl[0]!.playlist.tracks).toHaveLength(1);
expect(decl[0]!.groupId).toBeNull();
expect(decl[0]!.references).toHaveLength(0);
expect(decl[1]!.playlist.tracks).toHaveLength(1);
expect(decl[1]!.groupId).toBeNull();
expect(decl[1]!.references).toHaveLength(0);
expect(consoleSpy.mock.calls).toHaveLength(1);
expect(consoleSpy.mock.calls[0]![0]).toContain('Illegal pairing');
});