mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
Add onSegmentPopped, add more tests, improve validation, improve M3U8 grammar
This commit is contained in:
@@ -56,6 +56,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": "^(#)(EXT-X-[A-Z0-9-]+|EXT-[A-Z0-9-]+)$",
|
||||
"captures": {
|
||||
"1": { "name": "punctuation.definition.comment.m3u8" },
|
||||
"2": { "name": "keyword.control.tag.m3u8" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "^[^#\\s][^\\n]*$",
|
||||
"name": "source.m3u8.uri"
|
||||
|
||||
@@ -54,6 +54,7 @@ type PlaylistSegment = {
|
||||
timestamp: number;
|
||||
byteSize: number;
|
||||
byteOffset: number | null;
|
||||
info?: HlsOutputSegmentInfo;
|
||||
};
|
||||
|
||||
type Playlist = {
|
||||
@@ -1034,6 +1035,7 @@ export class HlsMuxer extends Muxer {
|
||||
byteOffset: playlist.singleFile
|
||||
? playlist.singleFile.nextOffset
|
||||
: null,
|
||||
info: segmentInfo ?? undefined,
|
||||
});
|
||||
|
||||
this.globalTargetDuration = Math.max(this.globalTargetDuration, segmentDuration);
|
||||
@@ -1047,8 +1049,13 @@ export class HlsMuxer extends Muxer {
|
||||
|
||||
if (this.isLive) {
|
||||
while (playlist.writtenSegments.length > this.maxLiveSegmentCount) {
|
||||
playlist.writtenSegments.shift();
|
||||
const popped = playlist.writtenSegments.shift()!;
|
||||
playlist.mediaSequence++;
|
||||
|
||||
if (!this.singleFilePerPlaylist) {
|
||||
assert(popped.info);
|
||||
this.format._options.onSegmentPopped?.(popped.path, popped.info);
|
||||
}
|
||||
}
|
||||
|
||||
await this.writePlaylist(playlist);
|
||||
|
||||
@@ -1271,6 +1271,12 @@ export type HlsOutputFormatOptions = {
|
||||
* function is never called.
|
||||
*/
|
||||
onInit?: (target: Target, info: HlsOutputPlaylistInfo) => unknown;
|
||||
/**
|
||||
* Called when a media segment is removed from the start of a media playlist due to
|
||||
* {@link HlsOutputFormatOptions.maxLiveSegmentCount}. Will not be called when
|
||||
* {@link HlsOutputFormatOptions.singleFilePerPlaylist} is `true`.
|
||||
*/
|
||||
onSegmentPopped?: (path: string, info: HlsOutputSegmentInfo) => unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1348,6 +1354,9 @@ export class HlsOutputFormat extends OutputFormat {
|
||||
if (options.onInit !== undefined && typeof options.onInit !== 'function') {
|
||||
throw new TypeError('options.onInit, when provided, must be a function.');
|
||||
}
|
||||
if (options.onSegmentPopped !== undefined && typeof options.onSegmentPopped !== 'function') {
|
||||
throw new TypeError('options.onSegmentPopped, when provided, must be a function.');
|
||||
}
|
||||
|
||||
super();
|
||||
|
||||
|
||||
@@ -179,11 +179,16 @@ export class OutputTrackGroup {
|
||||
/**
|
||||
* Marks this group as being pairable with another group, symmetrically. Output tracks where each track is assigned
|
||||
* to one half of a group pairing are then considered pairable.
|
||||
*
|
||||
* You cannot pair a group with itself.
|
||||
*/
|
||||
pairWith(other: OutputTrackGroup) {
|
||||
if (!(other instanceof OutputTrackGroup)) {
|
||||
throw new TypeError('other must be an OutputTrackGroup.');
|
||||
}
|
||||
if (this === other) {
|
||||
throw new TypeError('Cannot pair a group with itself.');
|
||||
}
|
||||
|
||||
this._pairedGroups.add(other);
|
||||
other._pairedGroups.add(this);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { expect, test, vi } from 'vitest';
|
||||
import { Output, OutputTrackGroup } from '../../src/output.js';
|
||||
import { CmafOutputFormat, HlsOutputFormat, MpegTsOutputFormat } from '../../src/output-format.js';
|
||||
import {
|
||||
CmafOutputFormat,
|
||||
HlsOutputFormat,
|
||||
HlsOutputSegmentInfo,
|
||||
MpegTsOutputFormat,
|
||||
} from '../../src/output-format.js';
|
||||
import { BufferTarget, NullTarget, PathedTarget, StreamTarget, StreamTargetChunk } from '../../src/target.js';
|
||||
import { EncodedAudioPacketSource, EncodedVideoPacketSource } from '../../src/media-source.js';
|
||||
import { HlsMuxer } from '../../src/hls/hls-muxer.js';
|
||||
@@ -2551,12 +2556,16 @@ test('Throws if some tracks are relativeToUnixEpoch and some are not', async ()
|
||||
|
||||
test('Live mode, maxLiveSegmentCount', async () => {
|
||||
const writtenTexts = new Map<string, string>();
|
||||
const poppedSegments: { path: string; info: HlsOutputSegmentInfo }[] = [];
|
||||
|
||||
const output = new Output({
|
||||
format: new HlsOutputFormat({
|
||||
segmentFormat: new MpegTsOutputFormat(),
|
||||
live: true,
|
||||
maxLiveSegmentCount: 2,
|
||||
onSegmentPopped: (path, info) => {
|
||||
poppedSegments.push({ path, info });
|
||||
},
|
||||
}),
|
||||
target: new PathedTarget('master.m3u8', (request) => {
|
||||
const target = new BufferTarget();
|
||||
@@ -2627,6 +2636,11 @@ segment-1-2.ts
|
||||
segment-1-3.ts
|
||||
`);
|
||||
|
||||
expect(poppedSegments).toHaveLength(1);
|
||||
expect(poppedSegments[0]!.path).toBe('segment-1-1.ts');
|
||||
expect(poppedSegments[0]!.info.n).toBe(1);
|
||||
expect(poppedSegments[0]!.info.isSingleFile).toBe(false);
|
||||
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 6.5, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 7, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 7.5, 0.5), avcMetadata);
|
||||
@@ -2646,4 +2660,62 @@ segment-1-4.ts
|
||||
|
||||
#EXT-X-ENDLIST
|
||||
`);
|
||||
|
||||
expect(poppedSegments).toHaveLength(2);
|
||||
expect(poppedSegments[1]!.path).toBe('segment-1-2.ts');
|
||||
expect(poppedSegments[1]!.info.n).toBe(2);
|
||||
});
|
||||
|
||||
test('Live mode, maxLiveSegmentCount with singleFilePerPlaylist', async () => {
|
||||
const onSegmentPopped = vi.fn();
|
||||
let lastPlaylistText = '';
|
||||
|
||||
const output = new Output({
|
||||
format: new HlsOutputFormat({
|
||||
segmentFormat: new MpegTsOutputFormat(),
|
||||
live: true,
|
||||
maxLiveSegmentCount: 2,
|
||||
singleFilePerPlaylist: true,
|
||||
onSegmentPopped,
|
||||
onPlaylist: (content) => {
|
||||
lastPlaylistText = content;
|
||||
},
|
||||
}),
|
||||
target: new PathedTarget('master.m3u8', () => {
|
||||
return new BufferTarget();
|
||||
}),
|
||||
});
|
||||
|
||||
const source = videoSource();
|
||||
output.addVideoTrack(source);
|
||||
|
||||
await output.start();
|
||||
|
||||
await source.add(new EncodedPacket(avcPacketData, 'key', 0, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 0.5, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 1, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 1.5, 0.5), avcMetadata);
|
||||
|
||||
await source.add(new EncodedPacket(avcPacketData, 'key', 2, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 2.5, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 3, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 3.5, 0.5), avcMetadata);
|
||||
|
||||
await source.add(new EncodedPacket(avcPacketData, 'key', 4, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 4.5, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 5, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 5.5, 0.5), avcMetadata);
|
||||
|
||||
await source.add(new EncodedPacket(avcPacketData, 'key', 6, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 6.5, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 7, 0.5), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 7.5, 0.5), avcMetadata);
|
||||
|
||||
await output.finalize();
|
||||
|
||||
expect(onSegmentPopped).not.toHaveBeenCalled();
|
||||
|
||||
// Popping still happened
|
||||
const extinfCount = (lastPlaylistText.match(/#EXTINF:/g) ?? []).length;
|
||||
expect(extinfCount).toBe(2);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user