Add support for live HLS streams, add playlist refresh logic, add live input tracks

This commit is contained in:
Vanilagy
2026-03-12 15:54:19 +01:00
parent ff523c01c8
commit e8df451bf5
23 changed files with 22399 additions and 417 deletions
+66 -2
View File
@@ -2,7 +2,7 @@
import { ALL_FORMATS, EncodedPacketSink, Input, InputAudioTrack, InputVideoTrack, UrlSource } from 'mediabunny';
import { expect, test } from 'vitest';
import { HLS, HlsInputFormat } from '../../src/input-format.js';
import { assert } from '../../src/misc.js';
import { assert, rejectAfter } from '../../src/misc.js';
// A lot of test cases taken from:
// https://github.com/video-dev/hls.js/blob/master/tests/test-streams.js
@@ -150,6 +150,10 @@ test.concurrent('Big Buck Bunny', { timeout: 15_000 }, async () => {
expect(tracks.every(x => x.isHydrated)).toBe(true);
expect(tracks.every(x => !x.timestampsAreRelativeToUnixEpoch)).toBe(true);
for (const track of tracks) {
expect(await track.isLive()).toBe(false);
}
expect(videoTracks[0]!.codedWidth).toBe(1280);
expect(videoTracks[0]!.codedHeight).toBe(720);
expect(videoTracks[1]!.codedWidth).toBe(320);
@@ -478,7 +482,7 @@ test.concurrent('Single-value PDT', { timeout: 15_000 }, async () => {
expect(lastPacket.sequenceNumber).toBeLessThan(1e8 * 60);
});
test.concurrent('Duplicate PDT', { timeout: 15_000 }, async () => {
test.concurrent('Duplicate PDT', { timeout: 30_000 }, async () => {
using input = new Input({
entryPath: 'https://playertest.longtailvideo.com/adaptive/artbeats/manifest.m3u8',
source: ({ path }) => new UrlSource(path),
@@ -734,3 +738,63 @@ test.concurrent('Advanced Apple HLS', { timeout: 30_000 }, async () => {
expect(await videoTracks[62]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0');
expect(await videoTracks[63]!.getCodecParameterString()).toBe('hvc1.2.4.L123.B0');
});
test.concurrent('Live HLS', { timeout: 30_000 }, async () => {
using input = new Input({
entryPath: 'https://stream.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM.m3u8',
source: ({ path }) => new UrlSource(path),
formats: ALL_FORMATS,
});
const videoTrack = await input.getPrimaryVideoTrack();
assert(videoTrack);
const audioTrack = await input.getPrimaryAudioTrack();
assert(audioTrack);
await expect(videoTrack.isLive()).rejects.toThrow();
await expect(audioTrack.isLive()).rejects.toThrow();
await videoTrack.hydrate();
await audioTrack.hydrate();
expect(await videoTrack.isLive()).toBe(true);
expect(await audioTrack.isLive()).toBe(true);
const videoRefreshInterval = await videoTrack.getLiveRefreshInterval();
const audioRefreshInterval = await audioTrack.getLiveRefreshInterval();
expect(videoRefreshInterval).toBe(2);
expect(audioRefreshInterval).toBe(2);
const durationFailed = Promise.race([
videoTrack.computeDuration(),
rejectAfter(2000, 'Baby you make me smile'),
]);
await expect(durationFailed).rejects.toThrow('Baby you make me smile');
const duration = await Promise.race([
videoTrack.computeDuration({ skipLiveWait: true }),
rejectAfter(2000, 'Baby you make me smile'),
]);
expect(duration).toBeGreaterThan((Date.now() / 1000) - 3600);
const sink = new EncodedPacketSink(videoTrack);
let currentLastPacket = await sink.getPacket(Infinity, { skipLiveWait: true });
assert(currentLastPacket);
// Actually find the last packet in decode order
while (true) {
const nextKnownPacket = await sink.getNextPacket(currentLastPacket, { skipLiveWait: true });
if (!nextKnownPacket) {
break;
}
currentLastPacket = nextKnownPacket;
}
// This tests waiting for the live stream to advance
const nextPacket = await sink.getNextPacket(currentLastPacket);
assert(nextPacket);
expect(nextPacket.sequenceNumber).toBeGreaterThan(currentLastPacket.sequenceNumber);
});