Fix HLS writing an incomplete init segment for sparse segments, improve decoder config validation, fix getFirstTimestamp() for Inputs with zero-packet tracks, allow zero-track muxing for some formats that allow it, add new track metadata that allows you to specify the decoder config early (enables zero-packet tracks), fix trafIndices in sparse fragmented MP4s, fix assertion errors in MPEG-TS demuxer

This commit is contained in:
Vanilagy
2026-08-08 17:00:36 +02:00
parent 7ec538e175
commit 5b22c8c17d
23 changed files with 1433 additions and 204 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ test('MPEG-TS output format', async () => {
video: { min: 0, max: 16 },
audio: { min: 0, max: 32 },
subtitle: { min: 0, max: 0 },
total: { min: 1, max: 48 },
total: { min: 0, max: 48 },
});
});
+711
View File
@@ -0,0 +1,711 @@
import { expect, test } from 'vitest';
import { Input } from '../../src/input.js';
import { ALL_FORMATS } from '../../src/input-format.js';
import { EncodedAudioPacketSource, EncodedVideoPacketSource } from '../../src/media-source.js';
import { AudioTrackMetadata, Output } from '../../src/output.js';
import {
AdtsOutputFormat,
CmafOutputFormat,
FlacOutputFormat,
HlsOutputFormat,
MkvOutputFormat,
Mp3OutputFormat,
Mp4OutputFormat,
MpegTsOutputFormat,
OggOutputFormat,
WavOutputFormat,
} from '../../src/output-format.js';
import { EncodedPacket } from '../../src/packet.js';
import { Bitstream } from '../../shared/bitstream.js';
import { BufferSource } from '../../src/source.js';
import { BufferTarget, PathedTarget } from '../../src/target.js';
import { InputAudioTrack, InputVideoTrack } from '../../src/input-track.js';
type EmptyMediaVariant =
| { type: 'mp4'; fastStart: false | 'in-memory' | 'reserve' | 'fragmented' }
| { type: 'cmaf' }
| { type: 'matroska' };
test('No tracks, MP4, fastStart: false', async () => {
await testNoTracks({ type: 'mp4', fastStart: false });
});
test('No tracks, MP4, fastStart: in-memory', async () => {
await testNoTracks({ type: 'mp4', fastStart: 'in-memory' });
});
test('No tracks, MP4, fastStart: reserve', async () => {
await testNoTracks({ type: 'mp4', fastStart: 'reserve' });
});
test('No tracks, MP4, fastStart: fragmented', async () => {
await testNoTracks({ type: 'mp4', fastStart: 'fragmented' });
});
test('No tracks, CMAF', async () => {
await testNoTracks({ type: 'cmaf' });
});
test('No tracks, Matroska', async () => {
await testNoTracks({ type: 'matroska' });
});
// These formats are inherently multi-track, so holding zero tracks is perfectly fine
const testNoTracks = async (variant: EmptyMediaVariant) => {
const initTarget = new BufferTarget();
const output = new Output({
format: createFormat(variant),
target: new BufferTarget(),
initTarget: variant.type === 'cmaf' ? initTarget : undefined,
});
await output.start();
await output.finalize();
using initInput = variant.type === 'cmaf'
? new Input({ source: new BufferSource(initTarget.buffer!), formats: ALL_FORMATS })
: undefined;
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
initInput,
});
expect(await input.getTracks()).toHaveLength(0);
};
test('No tracks, MPEG-TS', async () => {
const output = new Output({
format: new MpegTsOutputFormat(),
target: new BufferTarget(),
});
await output.start();
await output.finalize();
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
expect(await input.getTracks()).toHaveLength(0);
});
test('No tracks, Ogg', async () => {
const output = new Output({
format: new OggOutputFormat(),
target: new BufferTarget(),
});
await output.start();
await output.finalize();
// An Ogg file is nothing but its logical bitstreams, so zero tracks means zero bytes
expect(output.target.buffer!.byteLength).toBe(0);
});
test('No tracks, HLS', async () => {
const files = new Map<string, Uint8Array>();
const output = new Output({
format: new HlsOutputFormat({ segmentFormat: new MpegTsOutputFormat() }),
target: new PathedTarget('', (request) => {
const target = new BufferTarget();
target.on('finalized', () => files.set(request.path, new Uint8Array(target.buffer!)));
return target;
}),
});
await output.start();
await output.finalize();
// Without tracks there are no playlists either, so all we get is the master playlist (the root target)
expect([...files.keys()]).toEqual(['']);
});
test('No tracks, MP3', async () => {
const output = new Output({
format: new Mp3OutputFormat(),
target: new BufferTarget(),
});
await expect(output.start()).rejects.toThrow('MP3 requires exactly 1 audio track');
});
test('No tracks, WAVE', async () => {
const output = new Output({
format: new WavOutputFormat(),
target: new BufferTarget(),
});
await expect(output.start()).rejects.toThrow('WAVE requires exactly 1 audio track');
});
test('No tracks, ADTS', async () => {
const output = new Output({
format: new AdtsOutputFormat(),
target: new BufferTarget(),
});
await expect(output.start()).rejects.toThrow('ADTS requires exactly 1 audio track');
});
test('No tracks, FLAC', async () => {
const output = new Output({
format: new FlacOutputFormat(),
target: new BufferTarget(),
});
await expect(output.start()).rejects.toThrow('FLAC requires exactly 1 audio track');
});
test('Empty MP4, fastStart: false', async () => {
await testEmptyMedia({ type: 'mp4', fastStart: false });
});
test('Empty MP4, fastStart: in-memory', async () => {
await testEmptyMedia({ type: 'mp4', fastStart: 'in-memory' });
});
test('Empty MP4, fastStart: reserve', async () => {
await testEmptyMedia({ type: 'mp4', fastStart: 'reserve' });
});
test('Empty MP4, fastStart: fragmented', async () => {
await testEmptyMedia({ type: 'mp4', fastStart: 'fragmented' });
});
test('Empty CMAF', async () => {
await testEmptyMedia({ type: 'cmaf' });
});
test('Empty Matroska', async () => {
await testEmptyMedia({ type: 'matroska' });
});
const testEmptyMedia = async (variant: EmptyMediaVariant) => {
const initTarget = new BufferTarget();
const output = new Output({
format: createFormat(variant),
target: new BufferTarget(),
initTarget: variant.type === 'cmaf' ? initTarget : undefined,
});
output.addVideoTrack(new EncodedVideoPacketSource('avc'), { maximumPacketCount: 100 });
output.addAudioTrack(new EncodedAudioPacketSource('opus'), { maximumPacketCount: 100 });
await output.start();
await output.finalize();
using initInput = variant.type === 'cmaf'
? new Input({ source: new BufferSource(initTarget.buffer!), formats: ALL_FORMATS })
: undefined;
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
initInput,
});
expect(await input.getTracks()).toHaveLength(0);
};
test('Empty MP4 with declared decoder config, fastStart: false', async () => {
await testEmptyMediaWithDecoderConfig({ type: 'mp4', fastStart: false });
});
test('Empty MP4 with declared decoder config, fastStart: in-memory', async () => {
await testEmptyMediaWithDecoderConfig({ type: 'mp4', fastStart: 'in-memory' });
});
test('Empty MP4 with declared decoder config, fastStart: reserve', async () => {
await testEmptyMediaWithDecoderConfig({ type: 'mp4', fastStart: 'reserve' });
});
test('Empty MP4 with declared decoder config, fastStart: fragmented', async () => {
await testEmptyMediaWithDecoderConfig({ type: 'mp4', fastStart: 'fragmented' });
});
test('Empty CMAF with declared decoder config', async () => {
await testEmptyMediaWithDecoderConfig({ type: 'cmaf' });
});
test('Empty Matroska with declared decoder config', async () => {
await testEmptyMediaWithDecoderConfig({ type: 'matroska' });
});
// VP9 and Opus need no description, so the decoder config alone is enough to fully define the tracks
const testEmptyMediaWithDecoderConfig = async (variant: EmptyMediaVariant) => {
const initTarget = new BufferTarget();
const output = new Output({
format: createFormat(variant),
target: new BufferTarget(),
initTarget: variant.type === 'cmaf' ? initTarget : undefined,
});
output.addVideoTrack(new EncodedVideoPacketSource('vp9'), {
maximumPacketCount: 100,
decoderConfig: {
codec: 'vp09.00.10.08',
codedWidth: 1280,
codedHeight: 720,
},
});
output.addAudioTrack(new EncodedAudioPacketSource('opus'), {
maximumPacketCount: 100,
decoderConfig: {
codec: 'opus',
sampleRate: 48000,
numberOfChannels: 2,
},
});
await output.start();
await output.finalize();
using initInput = variant.type === 'cmaf'
? new Input({ source: new BufferSource(initTarget.buffer!), formats: ALL_FORMATS })
: undefined;
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
initInput,
});
const tracks = await input.getTracks();
expect(tracks).toHaveLength(2);
const videoTrack = tracks[0] as InputVideoTrack;
expect(videoTrack.isVideoTrack()).toBe(true);
expect(await videoTrack.getCodec()).toBe('vp9');
expect(await videoTrack.getCodedWidth()).toBe(1280);
expect(await videoTrack.getCodedHeight()).toBe(720);
expect((await videoTrack.computePacketStats()).packetCount).toBe(0);
const audioTrack = tracks[1] as InputAudioTrack;
expect(audioTrack.isAudioTrack()).toBe(true);
expect(await audioTrack.getCodec()).toBe('opus');
expect(await audioTrack.getSampleRate()).toBe(48000);
expect(await audioTrack.getNumberOfChannels()).toBe(2);
expect((await audioTrack.computePacketStats()).packetCount).toBe(0);
};
test('Empty Ogg', async () => {
const output = new Output({
format: new OggOutputFormat(),
target: new BufferTarget(),
});
// Ogg is audio-only, so let's go for two audio tracks here
output.addAudioTrack(new EncodedAudioPacketSource('opus'), { maximumPacketCount: 100 });
output.addAudioTrack(new EncodedAudioPacketSource('opus'), { maximumPacketCount: 100 });
await output.start();
await output.finalize();
// Ogg has no container-level header, so without any packets, nothing at all gets written
expect(output.target.buffer!.byteLength).toBe(0);
});
test('Empty Ogg with declared decoder config', async () => {
const output = new Output({
format: new OggOutputFormat(),
target: new BufferTarget(),
});
// An OpusHead packet as specified in RFC 7845
const description = new Uint8Array(19);
const view = new DataView(description.buffer);
description.set([0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]); // 'OpusHead'
view.setUint8(8, 1); // Version
view.setUint8(9, 2); // Channel count
view.setUint16(10, 312, true); // Pre-skip
view.setUint32(12, 48000, true); // Sample rate
view.setInt16(16, 0, true); // Output gain
view.setUint8(18, 0); // Channel mapping family
const metadata: AudioTrackMetadata = {
maximumPacketCount: 100,
decoderConfig: {
codec: 'opus',
sampleRate: 48000,
numberOfChannels: 2,
description,
},
};
output.addAudioTrack(new EncodedAudioPacketSource('opus'), metadata);
output.addAudioTrack(new EncodedAudioPacketSource('opus'), metadata);
await output.start();
await output.finalize();
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
const tracks = await input.getTracks();
expect(tracks).toHaveLength(2);
for (const track of tracks) {
const audioTrack = track as InputAudioTrack;
expect(audioTrack.isAudioTrack()).toBe(true);
expect(await audioTrack.getCodec()).toBe('opus');
expect(await audioTrack.getSampleRate()).toBe(48000);
expect(await audioTrack.getNumberOfChannels()).toBe(2);
expect((await audioTrack.computePacketStats()).packetCount).toBe(0);
}
});
test('Empty WAVE', async () => {
const output = new Output({
format: new WavOutputFormat(),
target: new BufferTarget(),
});
// WAVE holds a single audio track
output.addAudioTrack(new EncodedAudioPacketSource('pcm-s16'), { maximumPacketCount: 100 });
await output.start();
// There's no information to go on, so the muxer can't make anything up
await expect(output.finalize()).rejects.toThrow('Cannot finalize an empty WAVE file');
});
test('Empty WAVE with declared decoder config', async () => {
const output = new Output({
format: new WavOutputFormat(),
target: new BufferTarget(),
});
// Deliberately not the fallback values, so we can tell the declared config was actually used
output.addAudioTrack(new EncodedAudioPacketSource('pcm-s16'), {
maximumPacketCount: 100,
decoderConfig: {
codec: 'pcm-s16',
sampleRate: 44100,
numberOfChannels: 1,
},
});
await output.start();
await output.finalize();
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
const tracks = await input.getTracks();
expect(tracks).toHaveLength(1);
const audioTrack = tracks[0] as InputAudioTrack;
expect(audioTrack.isAudioTrack()).toBe(true);
expect(await audioTrack.getCodec()).toBe('pcm-s16');
expect(await audioTrack.getSampleRate()).toBe(44100);
expect(await audioTrack.getNumberOfChannels()).toBe(1);
expect((await audioTrack.computePacketStats()).packetCount).toBe(0);
});
test('Empty MP3', async () => {
const output = new Output({
format: new Mp3OutputFormat(),
target: new BufferTarget(),
});
output.addAudioTrack(new EncodedAudioPacketSource('mp3'), { maximumPacketCount: 100 });
await output.start();
// There's no information to go on, so the muxer can't make anything up
await expect(output.finalize()).rejects.toThrow('Cannot finalize an empty MP3 file');
});
test('Empty MP3 with declared decoder config', async () => {
const output = new Output({
format: new Mp3OutputFormat(),
target: new BufferTarget(),
});
// Deliberately not the fallback values, so we can tell the declared config was actually used
output.addAudioTrack(new EncodedAudioPacketSource('mp3'), {
maximumPacketCount: 100,
decoderConfig: {
codec: 'mp3',
sampleRate: 44100,
numberOfChannels: 1,
},
});
await output.start();
await output.finalize();
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
const tracks = await input.getTracks();
expect(tracks).toHaveLength(1);
const audioTrack = tracks[0] as InputAudioTrack;
expect(audioTrack.isAudioTrack()).toBe(true);
expect(await audioTrack.getCodec()).toBe('mp3');
expect(await audioTrack.getSampleRate()).toBe(44100);
expect(await audioTrack.getNumberOfChannels()).toBe(1);
expect((await audioTrack.computePacketStats()).packetCount).toBe(0);
});
test('Empty MP3 with priming packet', async () => {
const output = new Output({
format: new Mp3OutputFormat(),
target: new BufferTarget(),
});
// An MPEG Version 1 Layer III frame header, 128 kbps, 32 kHz, single channel
const frameHeader = new Uint8Array([0xff, 0xfb, 0x98, 0xc0]);
// The priming packet takes precedence over the declared config, so we can tell which one was used
output.addAudioTrack(new EncodedAudioPacketSource('mp3'), {
maximumPacketCount: 100,
decoderConfig: {
codec: 'mp3',
sampleRate: 44100,
numberOfChannels: 2,
},
primingPacket: new EncodedPacket(frameHeader, 'key', 0, 0),
});
await output.start();
await output.finalize();
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
const tracks = await input.getTracks();
expect(tracks).toHaveLength(1);
const audioTrack = tracks[0] as InputAudioTrack;
expect(audioTrack.isAudioTrack()).toBe(true);
expect(await audioTrack.getCodec()).toBe('mp3');
expect(await audioTrack.getSampleRate()).toBe(32000);
expect(await audioTrack.getNumberOfChannels()).toBe(1);
expect((await audioTrack.computePacketStats()).packetCount).toBe(0);
});
test('Empty MP3 without a Xing header', async () => {
const output = new Output({
format: new Mp3OutputFormat({ xingHeader: false }),
target: new BufferTarget(),
});
output.addAudioTrack(new EncodedAudioPacketSource('mp3'), {
maximumPacketCount: 100,
decoderConfig: {
codec: 'mp3',
sampleRate: 44100,
numberOfChannels: 1,
},
});
await output.start();
// The Xing frame is the only frame we could have synthesized, so there'd be nothing to write at all
await expect(output.finalize()).rejects.toThrow('Cannot finalize an empty MP3 file');
});
test('Empty ADTS', async () => {
const output = new Output({
format: new AdtsOutputFormat(),
target: new BufferTarget(),
});
output.addAudioTrack(new EncodedAudioPacketSource('aac'), { maximumPacketCount: 100 });
await output.start();
// ADTS is a bare sequence of frames, each carrying its own header, so there'd be nothing to write
await expect(output.finalize()).rejects.toThrow('Cannot finalize an empty ADTS file');
});
test('Empty ADTS with declared decoder config', async () => {
const output = new Output({
format: new AdtsOutputFormat(),
target: new BufferTarget(),
});
output.addAudioTrack(new EncodedAudioPacketSource('aac'), {
maximumPacketCount: 100,
decoderConfig: {
codec: 'mp4a.40.2',
sampleRate: 44100,
numberOfChannels: 2,
description: new Uint8Array([0x12, 0x10]), // AudioSpecificConfig: AAC-LC, 44100 Hz, stereo
},
});
await output.start();
// The declared config doesn't help; ADTS has no place to put it
await expect(output.finalize()).rejects.toThrow('Cannot finalize an empty ADTS file');
});
test('Empty FLAC', async () => {
const output = new Output({
format: new FlacOutputFormat(),
target: new BufferTarget(),
});
output.addAudioTrack(new EncodedAudioPacketSource('flac'), { maximumPacketCount: 100 });
await output.start();
// There's no information to go on, so the muxer can't make anything up
await expect(output.finalize()).rejects.toThrow('Cannot finalize an empty FLAC file');
});
test('Empty FLAC with declared decoder config', async () => {
const output = new Output({
format: new FlacOutputFormat(),
target: new BufferTarget(),
});
// A STREAMINFO metadata block for a 44100 Hz mono, 16-bit stream
const description = new Uint8Array(4 + 4 + 34);
description.set([0x66, 0x4c, 0x61, 0x43]); // 'fLaC'
description[4] = 0x80; // Last metadata block, type STREAMINFO
description[7] = 34; // Block size
const streamInfo = new Bitstream(description.subarray(8));
streamInfo.writeBits(16, 4096); // Minimum block size
streamInfo.writeBits(16, 4096); // Maximum block size
streamInfo.writeBits(24, 0); // Minimum frame size
streamInfo.writeBits(24, 0); // Maximum frame size
streamInfo.writeBits(20, 44100); // Sample rate
streamInfo.writeBits(3, 0); // Channels - 1
streamInfo.writeBits(5, 15); // Bits per sample - 1
output.addAudioTrack(new EncodedAudioPacketSource('flac'), {
maximumPacketCount: 100,
decoderConfig: {
codec: 'flac',
sampleRate: 44100,
numberOfChannels: 1,
description,
},
});
await output.start();
await output.finalize();
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
const tracks = await input.getTracks();
expect(tracks).toHaveLength(1);
const audioTrack = tracks[0] as InputAudioTrack;
expect(audioTrack.isAudioTrack()).toBe(true);
expect(await audioTrack.getCodec()).toBe('flac');
expect(await audioTrack.getSampleRate()).toBe(44100);
expect(await audioTrack.getNumberOfChannels()).toBe(1);
expect((await audioTrack.computePacketStats()).packetCount).toBe(0);
});
test('Empty MPEG-TS', async () => {
const output = new Output({
format: new MpegTsOutputFormat(),
target: new BufferTarget(),
});
output.addVideoTrack(new EncodedVideoPacketSource('avc'), { maximumPacketCount: 100 });
output.addAudioTrack(new EncodedAudioPacketSource('aac'), { maximumPacketCount: 100 });
await output.start();
await output.finalize();
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
expect(await input.getTracks()).toHaveLength(0);
});
test('Empty MPEG-TS with declared decoder config', async () => {
const output = new Output({
format: new MpegTsOutputFormat(),
target: new BufferTarget(),
});
// An MPEG-TS stream cannot be described without a packet - it's the first packet that defines the stream - so
// declaring the config up front changes nothing
output.addVideoTrack(new EncodedVideoPacketSource('avc'), {
maximumPacketCount: 100,
decoderConfig: {
codec: 'avc1.42001f',
codedWidth: 1280,
codedHeight: 720,
description: new Uint8Array([0x01, 0x42, 0x00, 0x1f, 0xff, 0xe1, 0x00, 0x00, 0x01, 0x00, 0x00]),
},
});
await output.start();
await output.finalize();
using input = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
expect(await input.getTracks()).toHaveLength(0);
});
test('Empty HLS', async () => {
const files = new Map<string, Uint8Array>();
const output = new Output({
format: new HlsOutputFormat({ segmentFormat: new MpegTsOutputFormat() }),
target: new PathedTarget('', (request) => {
const target = new BufferTarget();
target.on('finalized', () => files.set(request.path, new Uint8Array(target.buffer!)));
return target;
}),
});
output.addVideoTrack(new EncodedVideoPacketSource('avc'), {
maximumPacketCount: 100,
decoderConfig: {
codec: 'avc1.42001f',
codedWidth: 1280,
codedHeight: 720,
description: new Uint8Array([0x01, 0x42, 0x00, 0x1f, 0xff, 0xe1, 0x00, 0x00, 0x01, 0x00, 0x00]),
},
});
await output.start();
await output.finalize();
// HLS only ever writes segments for packets it has actually seen, so all we get is the master playlist (which is
// the root target) and one empty media playlist
expect([...files.keys()].sort()).toEqual(['', 'playlist-1.m3u8']);
});
const createFormat = (variant: EmptyMediaVariant) => {
if (variant.type === 'mp4') {
return new Mp4OutputFormat({ fastStart: variant.fastStart });
} else if (variant.type === 'cmaf') {
return new CmafOutputFormat();
} else {
return new MkvOutputFormat();
}
};
+106 -1
View File
@@ -3,6 +3,7 @@ import { Output, OutputTrackGroup } from '../../src/output.js';
import {
CmafOutputFormat,
HlsOutputFormat,
HlsOutputFormatOptions,
HlsOutputSegmentInfo,
Mp4OutputFormat,
MpegTsOutputFormat,
@@ -21,7 +22,7 @@ import { AudioCodec, VideoCodec } from '../../src/codec.js';
import { EncodedPacket, PacketType } from '../../src/packet.js';
import { assert, promiseWithResolvers } from '../../src/misc.js';
import { Input } from '../../src/input.js';
import { BufferSource } from '../../src/source.js';
import { BufferSource, CustomPathedSource } from '../../src/source.js';
import { ALL_FORMATS } from '../../src/input-format.js';
import { InputAudioTrack, InputVideoTrack } from '../../src/input-track.js';
import { EncodedPacketSink } from '../../src/media-sink.js';
@@ -2331,6 +2332,110 @@ test('CMAF segmentation, single file per playlist', async () => {
expect(playlistText).toContain('#EXT-X-MAP:URI=');
});
test('Sparse tracks in segments, MPEG-TS', async () => {
await runSparseTracksInSegments({
segmentFormat: new MpegTsOutputFormat(),
});
});
test('Sparse tracks in segments, CMAF', async () => {
await runSparseTracksInSegments({
segmentFormat: new CmafOutputFormat(),
});
});
test('Sparse tracks in segments, standard MP4', async () => {
await runSparseTracksInSegments({
segmentFormat: new Mp4OutputFormat(),
});
});
test('Sparse tracks in segments, fragmented MP4', async () => {
await runSparseTracksInSegments({
segmentFormat: new Mp4OutputFormat({ fastStart: 'fragmented' }),
});
});
test('Sparse tracks in segments, fragmented MP4 + single file', async () => {
await runSparseTracksInSegments({
segmentFormat: new Mp4OutputFormat({ fastStart: 'fragmented' }),
singleFilePerPlaylist: true,
});
});
const runSparseTracksInSegments = async (hlsOptions: HlsOutputFormatOptions) => {
const targets = new Map<string, BufferTarget>();
const output = new Output({
format: new HlsOutputFormat(hlsOptions),
target: new PathedTarget('master.m3u8', (request) => {
const target = new BufferTarget();
targets.set(request.path, target);
return target;
}),
});
const video = videoSource();
const audio = audioSource();
output.addVideoTrack(video);
output.addAudioTrack(audio);
await output.start();
// We expect three segments: First one with just video, second with video and audio, last one with just audio.
await video.add(new EncodedPacket(avcPacketData, 'key', 0, 0), avcMetadata);
await video.add(new EncodedPacket(avcPacketData, 'delta', 0.5, 0), avcMetadata);
await video.add(new EncodedPacket(avcPacketData, 'delta', 1, 0), avcMetadata);
await video.add(new EncodedPacket(avcPacketData, 'delta', 1.5, 0), avcMetadata);
await video.add(new EncodedPacket(avcPacketData, 'key', 2, 0), avcMetadata);
await video.add(new EncodedPacket(avcPacketData, 'delta', 2.5, 0), avcMetadata);
await video.add(new EncodedPacket(avcPacketData, 'delta', 3, 0), avcMetadata);
await video.add(new EncodedPacket(avcPacketData, 'delta', 3.5, 0), avcMetadata);
await audio.add(new EncodedPacket(aacPacketData, 'key', 2, 0), aacMetadata);
await audio.add(new EncodedPacket(aacPacketData, 'key', 2.5, 0), aacMetadata);
await audio.add(new EncodedPacket(aacPacketData, 'key', 3, 0), aacMetadata);
await audio.add(new EncodedPacket(aacPacketData, 'key', 3.5, 0), aacMetadata);
await audio.add(new EncodedPacket(aacPacketData, 'key', 4, 0), aacMetadata);
await audio.add(new EncodedPacket(aacPacketData, 'key', 4.5, 0), aacMetadata);
await audio.add(new EncodedPacket(aacPacketData, 'key', 5, 0), aacMetadata);
await audio.add(new EncodedPacket(aacPacketData, 'key', 5.5, 0), aacMetadata);
await output.finalize();
// Read the entire output back using the HLS input
using input = new Input({
source: new CustomPathedSource('master.m3u8', ({ path }) => {
const target = targets.get(path);
assert(target);
return new BufferSource(target.buffer!);
}),
formats: ALL_FORMATS,
});
const videoTrack = await input.getPrimaryVideoTrack() as InputVideoTrack;
expect(videoTrack).toBeTruthy();
const audioTrack = await input.getPrimaryAudioTrack() as InputAudioTrack;
expect(audioTrack).toBeTruthy();
const videoSink = new EncodedPacketSink(videoTrack);
const videoTimestamps: number[] = [];
for await (const packet of videoSink.packets()) {
videoTimestamps.push(packet.timestamp);
}
expect(videoTimestamps).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5]);
const audioSink = new EncodedPacketSink(audioTrack);
const audioTimestamps: number[] = [];
for await (const packet of audioSink.packets()) {
audioTimestamps.push(packet.timestamp);
}
expect(audioTimestamps).toEqual([2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5]);
};
test('Live mode', async () => {
const writtenTexts = new Map<string, string>();
const writeCounts = new Map<string, number>();