mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-02 21:33:52 +02:00
Add maxLiveSegmentCount HLS option
This commit is contained in:
@@ -61,6 +61,7 @@ type Playlist = {
|
|||||||
writtenSegments: PlaylistSegment[];
|
writtenSegments: PlaylistSegment[];
|
||||||
peakBitrate: number | null;
|
peakBitrate: number | null;
|
||||||
averageBitrate: number | null;
|
averageBitrate: number | null;
|
||||||
|
mediaSequence: number;
|
||||||
done: boolean;
|
done: boolean;
|
||||||
|
|
||||||
singleFile: {
|
singleFile: {
|
||||||
@@ -88,6 +89,7 @@ export class HlsMuxer extends Muxer {
|
|||||||
trackDatas: HlsTrackData[] = [];
|
trackDatas: HlsTrackData[] = [];
|
||||||
singleFilePerPlaylist: boolean;
|
singleFilePerPlaylist: boolean;
|
||||||
isLive: boolean;
|
isLive: boolean;
|
||||||
|
maxLiveSegmentCount: number;
|
||||||
isRelativeToUnixEpoch = false;
|
isRelativeToUnixEpoch = false;
|
||||||
globalTargetDuration: number;
|
globalTargetDuration: number;
|
||||||
|
|
||||||
@@ -105,6 +107,7 @@ export class HlsMuxer extends Muxer {
|
|||||||
this.targetSegmentDuration = format._options.targetDuration ?? 2;
|
this.targetSegmentDuration = format._options.targetDuration ?? 2;
|
||||||
this.singleFilePerPlaylist = format._options.singleFilePerPlaylist ?? false;
|
this.singleFilePerPlaylist = format._options.singleFilePerPlaylist ?? false;
|
||||||
this.isLive = format._options.live ?? false;
|
this.isLive = format._options.live ?? false;
|
||||||
|
this.maxLiveSegmentCount = format._options.maxLiveSegmentCount ?? Infinity;
|
||||||
this.globalTargetDuration = this.targetSegmentDuration;
|
this.globalTargetDuration = this.targetSegmentDuration;
|
||||||
|
|
||||||
this.getPlaylistPath = format._options.getPlaylistPath
|
this.getPlaylistPath = format._options.getPlaylistPath
|
||||||
@@ -440,6 +443,7 @@ export class HlsMuxer extends Muxer {
|
|||||||
writtenSegments: [],
|
writtenSegments: [],
|
||||||
peakBitrate: null,
|
peakBitrate: null,
|
||||||
averageBitrate: null,
|
averageBitrate: null,
|
||||||
|
mediaSequence: 0,
|
||||||
done: false,
|
done: false,
|
||||||
singleFile: null,
|
singleFile: null,
|
||||||
};
|
};
|
||||||
@@ -1024,6 +1028,11 @@ export class HlsMuxer extends Muxer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.isLive) {
|
if (this.isLive) {
|
||||||
|
while (playlist.writtenSegments.length > this.maxLiveSegmentCount) {
|
||||||
|
playlist.writtenSegments.shift();
|
||||||
|
playlist.mediaSequence++;
|
||||||
|
}
|
||||||
|
|
||||||
await this.writePlaylist(playlist);
|
await this.writePlaylist(playlist);
|
||||||
await this.tryWriteMasterPlaylist();
|
await this.tryWriteMasterPlaylist();
|
||||||
}
|
}
|
||||||
@@ -1128,6 +1137,7 @@ export class HlsMuxer extends Muxer {
|
|||||||
+ `#EXT-X-VERSION:${version}\n`
|
+ `#EXT-X-VERSION:${version}\n`
|
||||||
+ (!this.isLive ? '#EXT-X-PLAYLIST-TYPE:VOD\n' : '')
|
+ (!this.isLive ? '#EXT-X-PLAYLIST-TYPE:VOD\n' : '')
|
||||||
+ `#EXT-X-TARGETDURATION:${Math.ceil(targetDuration)}\n` // Must be a "decimal-integer"
|
+ `#EXT-X-TARGETDURATION:${Math.ceil(targetDuration)}\n` // Must be a "decimal-integer"
|
||||||
|
+ (Number.isFinite(this.maxLiveSegmentCount) ? `#EXT-X-MEDIA-SEQUENCE:${playlist.mediaSequence}\n` : '')
|
||||||
+ '#EXT-X-INDEPENDENT-SEGMENTS\n' // Todo not for live?
|
+ '#EXT-X-INDEPENDENT-SEGMENTS\n' // Todo not for live?
|
||||||
+ (isKeyPacketsOnly ? '#EXT-X-I-FRAMES-ONLY\n' : '')
|
+ (isKeyPacketsOnly ? '#EXT-X-I-FRAMES-ONLY\n' : '')
|
||||||
+ (playlist.initSegment
|
+ (playlist.initSegment
|
||||||
|
|||||||
@@ -1173,6 +1173,7 @@ export type HlsOutputFormatOptions = {
|
|||||||
targetDuration?: number;
|
targetDuration?: number;
|
||||||
singleFilePerPlaylist?: boolean;
|
singleFilePerPlaylist?: boolean;
|
||||||
live?: boolean;
|
live?: boolean;
|
||||||
|
maxLiveSegmentCount?: number;
|
||||||
getPlaylistPath?: (info: HlsOutputPlaylistInfo) => MaybePromise<string>;
|
getPlaylistPath?: (info: HlsOutputPlaylistInfo) => MaybePromise<string>;
|
||||||
getSegmentPath?: (info: HlsOutputSegmentInfo) => MaybePromise<string>;
|
getSegmentPath?: (info: HlsOutputSegmentInfo) => MaybePromise<string>;
|
||||||
getInitPath?: (info: HlsOutputPlaylistInfo) => MaybePromise<string>;
|
getInitPath?: (info: HlsOutputPlaylistInfo) => MaybePromise<string>;
|
||||||
@@ -1217,6 +1218,13 @@ export class HlsOutputFormat extends OutputFormat {
|
|||||||
if (options.live !== undefined && typeof options.live !== 'boolean') {
|
if (options.live !== undefined && typeof options.live !== 'boolean') {
|
||||||
throw new TypeError('options.live, when provided, must be a boolean.');
|
throw new TypeError('options.live, when provided, must be a boolean.');
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
options.maxLiveSegmentCount !== undefined
|
||||||
|
&& (typeof options.maxLiveSegmentCount !== 'number' || options.maxLiveSegmentCount < 1
|
||||||
|
|| (Number.isFinite(options.maxLiveSegmentCount) && !Number.isInteger(options.maxLiveSegmentCount)))
|
||||||
|
) {
|
||||||
|
throw new TypeError('options.maxLiveSegmentCount, when provided, must be a positive integer or Infinity.');
|
||||||
|
}
|
||||||
if (options.getPlaylistPath !== undefined && typeof options.getPlaylistPath !== 'function') {
|
if (options.getPlaylistPath !== undefined && typeof options.getPlaylistPath !== 'function') {
|
||||||
throw new TypeError('options.getPlaylistPath, when provided, must be a function.');
|
throw new TypeError('options.getPlaylistPath, when provided, must be a function.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2523,3 +2523,103 @@ test('Throws if some tracks are relativeToUnixEpoch and some are not', async ()
|
|||||||
|
|
||||||
await expect(output.start()).rejects.toThrow('relativeToUnixEpoch');
|
await expect(output.start()).rejects.toThrow('relativeToUnixEpoch');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Live mode, maxLiveSegmentCount', async () => {
|
||||||
|
const writtenTexts = new Map<string, string>();
|
||||||
|
|
||||||
|
const output = new Output({
|
||||||
|
format: new HlsOutputFormat({
|
||||||
|
segmentFormat: new MpegTsOutputFormat(),
|
||||||
|
live: true,
|
||||||
|
maxLiveSegmentCount: 2,
|
||||||
|
}),
|
||||||
|
target: (request) => {
|
||||||
|
const target = new BufferTarget();
|
||||||
|
target.on('finalized', () => {
|
||||||
|
if (request.path.endsWith('.m3u8')) {
|
||||||
|
writtenTexts.set(request.path, new TextDecoder().decode(target.buffer!));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return target;
|
||||||
|
},
|
||||||
|
rootPath: 'master.m3u8',
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
expect(writtenTexts.get('playlist-1.m3u8')).toBe(`#EXTM3U
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-TARGETDURATION:2
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:0
|
||||||
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
|
#EXTINF:2,
|
||||||
|
segment-1-1.ts
|
||||||
|
`);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
expect(writtenTexts.get('playlist-1.m3u8')).toBe(`#EXTM3U
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-TARGETDURATION:2
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:0
|
||||||
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
|
#EXTINF:2,
|
||||||
|
segment-1-1.ts
|
||||||
|
#EXTINF:2,
|
||||||
|
segment-1-2.ts
|
||||||
|
`);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
expect(writtenTexts.get('playlist-1.m3u8')).toBe(`#EXTM3U
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-TARGETDURATION:2
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:1
|
||||||
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
|
#EXTINF:2,
|
||||||
|
segment-1-2.ts
|
||||||
|
#EXTINF:2,
|
||||||
|
segment-1-3.ts
|
||||||
|
`);
|
||||||
|
|
||||||
|
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(writtenTexts.get('playlist-1.m3u8')).toBe(`#EXTM3U
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-TARGETDURATION:2
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:2
|
||||||
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
|
#EXTINF:2,
|
||||||
|
segment-1-3.ts
|
||||||
|
#EXTINF:2,
|
||||||
|
segment-1-4.ts
|
||||||
|
|
||||||
|
#EXT-X-ENDLIST
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user