mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +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]*$",
|
"match": "^[^#\\s][^\\n]*$",
|
||||||
"name": "source.m3u8.uri"
|
"name": "source.m3u8.uri"
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type PlaylistSegment = {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
byteSize: number;
|
byteSize: number;
|
||||||
byteOffset: number | null;
|
byteOffset: number | null;
|
||||||
|
info?: HlsOutputSegmentInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Playlist = {
|
type Playlist = {
|
||||||
@@ -1034,6 +1035,7 @@ export class HlsMuxer extends Muxer {
|
|||||||
byteOffset: playlist.singleFile
|
byteOffset: playlist.singleFile
|
||||||
? playlist.singleFile.nextOffset
|
? playlist.singleFile.nextOffset
|
||||||
: null,
|
: null,
|
||||||
|
info: segmentInfo ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalTargetDuration = Math.max(this.globalTargetDuration, segmentDuration);
|
this.globalTargetDuration = Math.max(this.globalTargetDuration, segmentDuration);
|
||||||
@@ -1047,8 +1049,13 @@ export class HlsMuxer extends Muxer {
|
|||||||
|
|
||||||
if (this.isLive) {
|
if (this.isLive) {
|
||||||
while (playlist.writtenSegments.length > this.maxLiveSegmentCount) {
|
while (playlist.writtenSegments.length > this.maxLiveSegmentCount) {
|
||||||
playlist.writtenSegments.shift();
|
const popped = playlist.writtenSegments.shift()!;
|
||||||
playlist.mediaSequence++;
|
playlist.mediaSequence++;
|
||||||
|
|
||||||
|
if (!this.singleFilePerPlaylist) {
|
||||||
|
assert(popped.info);
|
||||||
|
this.format._options.onSegmentPopped?.(popped.path, popped.info);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.writePlaylist(playlist);
|
await this.writePlaylist(playlist);
|
||||||
|
|||||||
@@ -1271,6 +1271,12 @@ export type HlsOutputFormatOptions = {
|
|||||||
* function is never called.
|
* function is never called.
|
||||||
*/
|
*/
|
||||||
onInit?: (target: Target, info: HlsOutputPlaylistInfo) => unknown;
|
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') {
|
if (options.onInit !== undefined && typeof options.onInit !== 'function') {
|
||||||
throw new TypeError('options.onInit, when provided, must be a 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();
|
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
|
* 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.
|
* to one half of a group pairing are then considered pairable.
|
||||||
|
*
|
||||||
|
* You cannot pair a group with itself.
|
||||||
*/
|
*/
|
||||||
pairWith(other: OutputTrackGroup) {
|
pairWith(other: OutputTrackGroup) {
|
||||||
if (!(other instanceof OutputTrackGroup)) {
|
if (!(other instanceof OutputTrackGroup)) {
|
||||||
throw new TypeError('other must be an 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);
|
this._pairedGroups.add(other);
|
||||||
other._pairedGroups.add(this);
|
other._pairedGroups.add(this);
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { expect, test, vi } from 'vitest';
|
import { expect, test, vi } from 'vitest';
|
||||||
import { Output, OutputTrackGroup } from '../../src/output.js';
|
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 { BufferTarget, NullTarget, PathedTarget, StreamTarget, StreamTargetChunk } from '../../src/target.js';
|
||||||
import { EncodedAudioPacketSource, EncodedVideoPacketSource } from '../../src/media-source.js';
|
import { EncodedAudioPacketSource, EncodedVideoPacketSource } from '../../src/media-source.js';
|
||||||
import { HlsMuxer } from '../../src/hls/hls-muxer.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 () => {
|
test('Live mode, maxLiveSegmentCount', async () => {
|
||||||
const writtenTexts = new Map<string, string>();
|
const writtenTexts = new Map<string, string>();
|
||||||
|
const poppedSegments: { path: string; info: HlsOutputSegmentInfo }[] = [];
|
||||||
|
|
||||||
const output = new Output({
|
const output = new Output({
|
||||||
format: new HlsOutputFormat({
|
format: new HlsOutputFormat({
|
||||||
segmentFormat: new MpegTsOutputFormat(),
|
segmentFormat: new MpegTsOutputFormat(),
|
||||||
live: true,
|
live: true,
|
||||||
maxLiveSegmentCount: 2,
|
maxLiveSegmentCount: 2,
|
||||||
|
onSegmentPopped: (path, info) => {
|
||||||
|
poppedSegments.push({ path, info });
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
target: new PathedTarget('master.m3u8', (request) => {
|
target: new PathedTarget('master.m3u8', (request) => {
|
||||||
const target = new BufferTarget();
|
const target = new BufferTarget();
|
||||||
@@ -2627,6 +2636,11 @@ segment-1-2.ts
|
|||||||
segment-1-3.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', 6.5, 0.5), avcMetadata);
|
||||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 7, 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 source.add(new EncodedPacket(avcPacketData, 'delta', 7.5, 0.5), avcMetadata);
|
||||||
@@ -2646,4 +2660,62 @@ segment-1-4.ts
|
|||||||
|
|
||||||
#EXT-X-ENDLIST
|
#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