mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
Add some HLS events, add configurable target duration, add public-API OutputTrack
This commit is contained in:
+26
-9
@@ -11,9 +11,10 @@ import {
|
||||
OutputVideoTrack,
|
||||
TrackType,
|
||||
} from '../output';
|
||||
import { HlsOutputFormat, HlsOutputFormatOptions, OutputFormat } from '../output-format';
|
||||
import { HlsOutputFormat, HlsOutputFormatOptions, HlsOutputSegmentInfo, OutputFormat } from '../output-format';
|
||||
import { EncodedPacket } from '../packet';
|
||||
import { SubtitleCue, SubtitleMetadata } from '../subtitles';
|
||||
import { Target } from '../target';
|
||||
|
||||
type HlsTrackData = {
|
||||
track: OutputTrack;
|
||||
@@ -59,7 +60,7 @@ export class HlsMuxer extends Muxer {
|
||||
getPlaylistPath: NonNullable<HlsOutputFormatOptions['getPlaylistPath']>;
|
||||
getSegmentPath: NonNullable<HlsOutputFormatOptions['getSegmentPath']>;
|
||||
|
||||
targetSegmentDuration = 2;
|
||||
targetSegmentDuration: number;
|
||||
trackDatas: HlsTrackData[] = [];
|
||||
|
||||
playlists: Playlist[] = [];
|
||||
@@ -73,6 +74,7 @@ export class HlsMuxer extends Muxer {
|
||||
super(output);
|
||||
|
||||
this.format = format;
|
||||
this.targetSegmentDuration = format._options.targetDuration ?? 5;
|
||||
|
||||
this.getPlaylistPath = format._options.getPlaylistPath
|
||||
?? (({ n }) => `playlist-${n}.m3u8`);
|
||||
@@ -303,10 +305,10 @@ export class HlsMuxer extends Muxer {
|
||||
let candidateScore = -Infinity;
|
||||
|
||||
for (const track of tracks) {
|
||||
if (track.type === 'video') {
|
||||
if (track.isVideoTrack()) {
|
||||
videoCount++;
|
||||
requiresRotationMetadata ||= (track.metadata.rotation ?? 0) !== 0;
|
||||
} else if (track.type === 'audio') {
|
||||
} else if (track.isAudioTrack()) {
|
||||
audioCount++;
|
||||
}
|
||||
|
||||
@@ -715,14 +717,16 @@ export class HlsMuxer extends Muxer {
|
||||
}
|
||||
|
||||
// We can finalize a new segment! Let's first get the path
|
||||
const relativeSegmentPath = await this.getSegmentPath({
|
||||
const segmentInfo: HlsOutputSegmentInfo = {
|
||||
n: playlist.nextSegmentId,
|
||||
format: playlist.segmentFormat,
|
||||
playlist: {
|
||||
n: playlist.id,
|
||||
tracks: playlist.tracks,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const relativeSegmentPath = await this.getSegmentPath(segmentInfo);
|
||||
if (typeof relativeSegmentPath !== 'string') {
|
||||
throw new TypeError('options.getSegmentPath must return or resolve to a string');
|
||||
}
|
||||
@@ -737,6 +741,8 @@ export class HlsMuxer extends Muxer {
|
||||
playlist.nextSegmentId++;
|
||||
|
||||
let segmentSize = 0;
|
||||
let outputTarget: Target | null = null;
|
||||
|
||||
const output = new Output({
|
||||
format: playlist.segmentFormat,
|
||||
rootPath: fullSegmentPath,
|
||||
@@ -744,6 +750,7 @@ export class HlsMuxer extends Muxer {
|
||||
const target = await this.output._getTarget(request);
|
||||
|
||||
if (request.path === fullSegmentPath) {
|
||||
outputTarget = target;
|
||||
target.onwrite = (_, end) => segmentSize = Math.max(segmentSize, end);
|
||||
}
|
||||
|
||||
@@ -803,6 +810,9 @@ export class HlsMuxer extends Muxer {
|
||||
throw e;
|
||||
}
|
||||
|
||||
assert(outputTarget);
|
||||
this.format._options.onSegment?.(outputTarget, segmentInfo);
|
||||
|
||||
if (videoEndIndex > 0) {
|
||||
assert(videoTrack);
|
||||
videoTrack.packets.splice(0, videoEndIndex);
|
||||
@@ -883,7 +893,12 @@ export class HlsMuxer extends Muxer {
|
||||
+ '\n'
|
||||
+ '#EXT-X-ENDLIST\n';
|
||||
|
||||
const target = await this.output._getTarget({ path: playlistPath });
|
||||
this.format._options.onPlaylist?.(playlistText, {
|
||||
n: playlist.id,
|
||||
tracks: playlist.tracks,
|
||||
});
|
||||
|
||||
const target = await this.output._getTarget({ path: playlistPath, isRoot: false });
|
||||
const writer = target._createWriter();
|
||||
writer.write(textEncoder.encode(playlistText));
|
||||
await writer.finalize();
|
||||
@@ -942,8 +957,8 @@ export class HlsMuxer extends Muxer {
|
||||
|
||||
masterPlaylistText += `,CODECS="${codecs.join(',')}"`;
|
||||
|
||||
const videoTrack = decl.playlist.tracks.find(x => x.type === 'video');
|
||||
if (videoTrack) {
|
||||
const videoTrack = decl.playlist.tracks.find(x => x.isVideoTrack());
|
||||
if (videoTrack?.isVideoTrack()) {
|
||||
const trackData = this.trackDatas.find(x => x.track === videoTrack) as
|
||||
HlsVideoTrackData | undefined;
|
||||
const decoderConfig = trackData?.info.decoderConfig;
|
||||
@@ -1059,6 +1074,8 @@ export class HlsMuxer extends Muxer {
|
||||
}
|
||||
}
|
||||
|
||||
this.format._options.onMaster?.(masterPlaylistText);
|
||||
|
||||
const rootWriter = await this.output._getRootWriter();
|
||||
rootWriter.write(textEncoder.encode(masterPlaylistText));
|
||||
})();
|
||||
|
||||
+5
-1
@@ -23,6 +23,11 @@ if ((globalThis as Record<symbol, unknown>)[MEDIABUNNY_LOADED_SYMBOL]) {
|
||||
export {
|
||||
Output,
|
||||
OutputOptions,
|
||||
OutputTrack,
|
||||
OutputVideoTrack,
|
||||
OutputAudioTrack,
|
||||
OutputSubtitleTrack,
|
||||
OutputTrackGroup,
|
||||
BaseTrackMetadata,
|
||||
VideoTrackMetadata,
|
||||
AudioTrackMetadata,
|
||||
@@ -136,7 +141,6 @@ export {
|
||||
prefer,
|
||||
} from './misc';
|
||||
export {
|
||||
OutputTrackGroup,
|
||||
TrackType,
|
||||
ALL_TRACK_TYPES,
|
||||
} from './output';
|
||||
|
||||
@@ -199,7 +199,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
this.writer.ensureMonotonicity = true;
|
||||
}
|
||||
|
||||
const holdsAvc = this.output._tracks.some(x => x.type === 'video' && x.source._codec === 'avc');
|
||||
const holdsAvc = this.output._tracks.some(x => x.isVideoTrack() && x.source._codec === 'avc');
|
||||
|
||||
// Write the header
|
||||
{
|
||||
|
||||
+6
-3
@@ -10,6 +10,7 @@ import { buildAacAudioSpecificConfig, parseAacAudioSpecificConfig } from '../sha
|
||||
import {
|
||||
AUDIO_CODECS,
|
||||
AudioCodec,
|
||||
MediaCodec,
|
||||
parsePcmCodec,
|
||||
PCM_AUDIO_CODECS,
|
||||
PcmAudioCodec,
|
||||
@@ -62,6 +63,8 @@ import {
|
||||
* @public
|
||||
*/
|
||||
export abstract class MediaSource {
|
||||
/** @internal */
|
||||
abstract readonly _codec: MediaCodec;
|
||||
/** @internal */
|
||||
_connectedTrack: OutputTrack | null = null;
|
||||
/** @internal */
|
||||
@@ -154,7 +157,7 @@ export abstract class VideoSource extends MediaSource {
|
||||
/** @internal */
|
||||
override _connectedTrack: OutputVideoTrack | null = null;
|
||||
/** @internal */
|
||||
_codec: VideoCodec;
|
||||
override readonly _codec: VideoCodec;
|
||||
|
||||
/** Internal constructor. */
|
||||
constructor(codec: VideoCodec) {
|
||||
@@ -1475,7 +1478,7 @@ export abstract class AudioSource extends MediaSource {
|
||||
/** @internal */
|
||||
override _connectedTrack: OutputAudioTrack | null = null;
|
||||
/** @internal */
|
||||
_codec: AudioCodec;
|
||||
override readonly _codec: AudioCodec;
|
||||
|
||||
/** Internal constructor. */
|
||||
constructor(codec: AudioCodec) {
|
||||
@@ -2459,7 +2462,7 @@ export abstract class SubtitleSource extends MediaSource {
|
||||
/** @internal */
|
||||
override _connectedTrack: OutputSubtitleTrack | null = null;
|
||||
/** @internal */
|
||||
_codec: SubtitleCodec;
|
||||
override readonly _codec: SubtitleCodec;
|
||||
|
||||
/** Internal constructor. */
|
||||
constructor(codec: SubtitleCodec) {
|
||||
|
||||
+22
-1
@@ -30,6 +30,7 @@ import { MpegTsMuxer } from './mpeg-ts/mpeg-ts-muxer';
|
||||
import { WaveMuxer } from './wave/wave-muxer';
|
||||
import { HlsMuxer } from './hls/hls-muxer';
|
||||
import { MaybePromise } from './misc';
|
||||
import { Target } from './target';
|
||||
|
||||
/**
|
||||
* Specifies an inclusive range of integers.
|
||||
@@ -1099,7 +1100,7 @@ export type HlsOutputPlaylistInfo = {
|
||||
tracks: OutputTrack[];
|
||||
};
|
||||
|
||||
type HlsOutputSegmentInfo = {
|
||||
export type HlsOutputSegmentInfo = {
|
||||
n: number;
|
||||
format: OutputFormat;
|
||||
playlist: HlsOutputPlaylistInfo;
|
||||
@@ -1107,8 +1108,13 @@ type HlsOutputSegmentInfo = {
|
||||
|
||||
export type HlsOutputFormatOptions = {
|
||||
segmentFormats: OutputFormat[];
|
||||
targetDuration?: number;
|
||||
getPlaylistPath?: (info: HlsOutputPlaylistInfo) => MaybePromise<string>;
|
||||
getSegmentPath?: (info: HlsOutputSegmentInfo) => MaybePromise<string>;
|
||||
|
||||
onMaster?: (content: string) => unknown;
|
||||
onPlaylist?: (content: string, info: HlsOutputPlaylistInfo) => unknown;
|
||||
onSegment?: (target: Target, info: HlsOutputSegmentInfo) => unknown;
|
||||
};
|
||||
|
||||
export class HlsOutputFormat extends OutputFormat {
|
||||
@@ -1127,12 +1133,27 @@ export class HlsOutputFormat extends OutputFormat {
|
||||
) {
|
||||
throw new TypeError('options.segmentFormats must be a non-empty array of OutputFormat instances.');
|
||||
}
|
||||
if (
|
||||
options.targetDuration !== undefined
|
||||
&& (typeof options.targetDuration !== 'number' || options.targetDuration <= 0)
|
||||
) {
|
||||
throw new TypeError('options.targetDuration, when provided, must be a positive number.');
|
||||
}
|
||||
if (options.getPlaylistPath !== undefined && typeof options.getPlaylistPath !== 'function') {
|
||||
throw new TypeError('options.getPlaylistPath, when provided, must be a function.');
|
||||
}
|
||||
if (options.getSegmentPath !== undefined && typeof options.getSegmentPath !== 'function') {
|
||||
throw new TypeError('options.getSegmentPath, when provided, must be a function.');
|
||||
}
|
||||
if (options.onMaster !== undefined && typeof options.onMaster !== 'function') {
|
||||
throw new TypeError('options.onMaster, when provided, must be a function.');
|
||||
}
|
||||
if (options.onPlaylist !== undefined && typeof options.onPlaylist !== 'function') {
|
||||
throw new TypeError('options.onPlaylist, when provided, must be a function.');
|
||||
}
|
||||
if (options.onSegment !== undefined && typeof options.onSegment !== 'function') {
|
||||
throw new TypeError('options.onSegment, when provided, must be a function.');
|
||||
}
|
||||
|
||||
super();
|
||||
|
||||
|
||||
+118
-43
@@ -16,6 +16,7 @@ import { Writer } from './writer';
|
||||
|
||||
export type TargetRequest = {
|
||||
path: string;
|
||||
isRoot: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -47,27 +48,101 @@ export const ALL_TRACK_TYPES = ['video', 'audio', 'subtitle'] as const;
|
||||
*/
|
||||
export type TrackType = typeof ALL_TRACK_TYPES[number];
|
||||
|
||||
export type OutputTrack = {
|
||||
id: number;
|
||||
output: Output;
|
||||
type: TrackType;
|
||||
} & ({
|
||||
type: 'video';
|
||||
source: VideoSource;
|
||||
metadata: VideoTrackMetadata;
|
||||
} | {
|
||||
type: 'audio';
|
||||
source: AudioSource;
|
||||
metadata: AudioTrackMetadata;
|
||||
} | {
|
||||
type: 'subtitle';
|
||||
source: SubtitleSource;
|
||||
metadata: SubtitleTrackMetadata;
|
||||
});
|
||||
/**
|
||||
* Represents a track added to an {@link Output}.
|
||||
* @group Output files
|
||||
* @public
|
||||
*/
|
||||
export abstract class OutputTrack {
|
||||
/** @internal */
|
||||
readonly id: number;
|
||||
/** The {@link Output} this track belongs to. */
|
||||
readonly output: Output;
|
||||
/** The type of this track. */
|
||||
readonly type: TrackType;
|
||||
/** The media source providing data for this track. */
|
||||
readonly source: MediaSource;
|
||||
/** The metadata associated with this track. */
|
||||
readonly metadata: BaseTrackMetadata;
|
||||
|
||||
export type OutputVideoTrack = OutputTrack & { type: 'video' };
|
||||
export type OutputAudioTrack = OutputTrack & { type: 'audio' };
|
||||
export type OutputSubtitleTrack = OutputTrack & { type: 'subtitle' };
|
||||
/** @internal */
|
||||
protected constructor(
|
||||
id: number,
|
||||
output: Output,
|
||||
type: TrackType,
|
||||
source: MediaSource,
|
||||
metadata: BaseTrackMetadata,
|
||||
) {
|
||||
this.id = id;
|
||||
this.output = output;
|
||||
this.type = type;
|
||||
this.source = source;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
/** Returns true if and only if this track is a video track. */
|
||||
isVideoTrack(): this is OutputVideoTrack {
|
||||
return this.type === 'video';
|
||||
}
|
||||
|
||||
/** Returns true if and only if this track is an audio track. */
|
||||
isAudioTrack(): this is OutputAudioTrack {
|
||||
return this.type === 'audio';
|
||||
}
|
||||
|
||||
/** Returns true if and only if this track is a subtitle track. */
|
||||
isSubtitleTrack(): this is OutputSubtitleTrack {
|
||||
return this.type === 'subtitle';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link OutputTrack} containing video data.
|
||||
* @group Output files
|
||||
* @public
|
||||
*/
|
||||
export class OutputVideoTrack extends OutputTrack {
|
||||
declare readonly type: 'video';
|
||||
declare readonly source: VideoSource;
|
||||
declare readonly metadata: VideoTrackMetadata;
|
||||
|
||||
/** @internal */
|
||||
constructor(id: number, output: Output, source: VideoSource, metadata: VideoTrackMetadata) {
|
||||
super(id, output, 'video', source, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link OutputTrack} containing audio data.
|
||||
* @group Output files
|
||||
* @public
|
||||
*/
|
||||
export class OutputAudioTrack extends OutputTrack {
|
||||
declare readonly type: 'audio';
|
||||
declare readonly source: AudioSource;
|
||||
declare readonly metadata: AudioTrackMetadata;
|
||||
|
||||
/** @internal */
|
||||
constructor(id: number, output: Output, source: AudioSource, metadata: AudioTrackMetadata) {
|
||||
super(id, output, 'audio', source, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link OutputTrack} containing subtitle data.
|
||||
* @group Output files
|
||||
* @public
|
||||
*/
|
||||
export class OutputSubtitleTrack extends OutputTrack {
|
||||
declare readonly type: 'subtitle';
|
||||
declare readonly source: SubtitleSource;
|
||||
declare readonly metadata: SubtitleTrackMetadata;
|
||||
|
||||
/** @internal */
|
||||
constructor(id: number, output: Output, source: SubtitleSource, metadata: SubtitleTrackMetadata) {
|
||||
super(id, output, 'subtitle', source, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
export class OutputTrackGroup {
|
||||
/** @internal */
|
||||
@@ -246,7 +321,7 @@ export class Output<
|
||||
}
|
||||
|
||||
assert(this._rootPath !== null);
|
||||
const returnValue = this._target({ path: this._rootPath });
|
||||
const returnValue = this._target({ path: this._rootPath, isRoot: true });
|
||||
if (returnValue instanceof Promise) {
|
||||
throw new TypeError(
|
||||
'Output.target cannot be used when the target function resolves asynchronously.',
|
||||
@@ -310,7 +385,7 @@ export class Output<
|
||||
|
||||
if (typeof this._target === 'function') {
|
||||
assert(this._rootPath !== null);
|
||||
const rootTarget = await this._getTarget({ path: this._rootPath });
|
||||
const rootTarget = await this._getTarget({ path: this._rootPath, isRoot: true });
|
||||
writer = rootTarget._createWriter();
|
||||
} else {
|
||||
writer = this._target._createWriter();
|
||||
@@ -346,7 +421,9 @@ export class Output<
|
||||
const metadataCopy = { ...metadata };
|
||||
metadataCopy.group ??= this._defaultTrackGroup;
|
||||
|
||||
this._addTrack('video', source, metadataCopy);
|
||||
return this._addTrack(new OutputVideoTrack(
|
||||
this._tracks.length + 1, this, source, metadataCopy,
|
||||
));
|
||||
}
|
||||
|
||||
/** Adds an audio track to the output with the given source. Can only be called before the output is started. */
|
||||
@@ -359,7 +436,9 @@ export class Output<
|
||||
const metadataCopy = { ...metadata };
|
||||
metadataCopy.group ??= this._defaultTrackGroup;
|
||||
|
||||
this._addTrack('audio', source, metadataCopy);
|
||||
return this._addTrack(new OutputAudioTrack(
|
||||
this._tracks.length + 1, this, source, metadataCopy,
|
||||
));
|
||||
}
|
||||
|
||||
/** Adds a subtitle track to the output with the given source. Can only be called before the output is started. */
|
||||
@@ -372,7 +451,9 @@ export class Output<
|
||||
const metadataCopy = { ...metadata };
|
||||
metadataCopy.group ??= this._defaultTrackGroup;
|
||||
|
||||
this._addTrack('subtitle', source, metadataCopy);
|
||||
return this._addTrack(new OutputSubtitleTrack(
|
||||
this._tracks.length + 1, this, source, metadataCopy,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -392,26 +473,26 @@ export class Output<
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _addTrack(type: OutputTrack['type'], source: MediaSource, metadata: BaseTrackMetadata) {
|
||||
private _addTrack<T extends OutputTrack>(track: T) {
|
||||
if (this.state !== 'pending') {
|
||||
throw new Error('Cannot add track after output has been started or canceled.');
|
||||
}
|
||||
if (source._connectedTrack) {
|
||||
if (track.source._connectedTrack) {
|
||||
throw new Error('Source is already used for a track.');
|
||||
}
|
||||
|
||||
// Verify maximum track count constraints
|
||||
const supportedTrackCounts = this.format.getSupportedTrackCounts();
|
||||
const presentTracksOfThisType = this._tracks.reduce(
|
||||
(count, track) => count + (track.type === type ? 1 : 0),
|
||||
(count, t) => count + (t.type === track.type ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
const maxCount = supportedTrackCounts[type].max;
|
||||
const maxCount = supportedTrackCounts[track.type].max;
|
||||
if (presentTracksOfThisType === maxCount) {
|
||||
throw new Error(
|
||||
maxCount === 0
|
||||
? `${this.format._name} does not support ${type} tracks.`
|
||||
: (`${this.format._name} does not support more than ${maxCount} ${type} track`
|
||||
? `${this.format._name} does not support ${track.type} tracks.`
|
||||
: (`${this.format._name} does not support more than ${maxCount} ${track.type} track`
|
||||
+ `${maxCount === 1 ? '' : 's'}.`),
|
||||
);
|
||||
}
|
||||
@@ -423,15 +504,7 @@ export class Output<
|
||||
);
|
||||
}
|
||||
|
||||
const track = {
|
||||
id: this._tracks.length + 1,
|
||||
output: this,
|
||||
type,
|
||||
source: source as unknown,
|
||||
metadata,
|
||||
} as OutputTrack;
|
||||
|
||||
if (track.type === 'video') {
|
||||
if (track.isVideoTrack()) {
|
||||
const supportedVideoCodecs = this.format.getSupportedVideoCodecs();
|
||||
|
||||
if (supportedVideoCodecs.length === 0) {
|
||||
@@ -446,7 +519,7 @@ export class Output<
|
||||
+ this.format._codecUnsupportedHint(track.source._codec),
|
||||
);
|
||||
}
|
||||
} else if (track.type === 'audio') {
|
||||
} else if (track.isAudioTrack()) {
|
||||
const supportedAudioCodecs = this.format.getSupportedAudioCodecs();
|
||||
|
||||
if (supportedAudioCodecs.length === 0) {
|
||||
@@ -461,7 +534,7 @@ export class Output<
|
||||
+ this.format._codecUnsupportedHint(track.source._codec),
|
||||
);
|
||||
}
|
||||
} else if (track.type === 'subtitle') {
|
||||
} else if (track.isSubtitleTrack()) {
|
||||
const supportedSubtitleCodecs = this.format.getSupportedSubtitleCodecs();
|
||||
|
||||
if (supportedSubtitleCodecs.length === 0) {
|
||||
@@ -479,7 +552,9 @@ export class Output<
|
||||
}
|
||||
|
||||
this._tracks.push(track);
|
||||
source._connectedTrack = track;
|
||||
track.source._connectedTrack = track;
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -831,6 +831,7 @@ const setUpSegmentationEnvironment = async (options: {
|
||||
const output = new Output({
|
||||
format: new HlsOutputFormat({
|
||||
segmentFormats: [new MpegTsOutputFormat()], // No ADTS for simplicity
|
||||
targetDuration: 2,
|
||||
}),
|
||||
target: (request) => {
|
||||
const target = new BufferTarget();
|
||||
@@ -1731,3 +1732,65 @@ segment-1-1.ts
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
test('onSegment, onPlaylist, onMaster events', async () => {
|
||||
const onSegment = vi.fn();
|
||||
const onPlaylist = vi.fn();
|
||||
const onMaster = vi.fn();
|
||||
|
||||
const output = new Output({
|
||||
format: new HlsOutputFormat({
|
||||
segmentFormats: [new MpegTsOutputFormat()],
|
||||
targetDuration: 2,
|
||||
onSegment,
|
||||
onPlaylist,
|
||||
onMaster,
|
||||
}),
|
||||
target: () => new BufferTarget(),
|
||||
rootPath: '',
|
||||
});
|
||||
|
||||
const source = videoSource();
|
||||
output.addVideoTrack(source);
|
||||
|
||||
await output.start();
|
||||
|
||||
// First segment
|
||||
await source.add(new EncodedPacket(avcPacketData, 'key', 0, 0), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 0.5, 0), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 1, 0), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 1.5, 0), avcMetadata);
|
||||
expect(onSegment).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Second segment starts, first one is finalized
|
||||
await source.add(new EncodedPacket(avcPacketData, 'key', 2, 0), avcMetadata);
|
||||
expect(onSegment).toHaveBeenCalledTimes(1);
|
||||
expect(onSegment.mock.calls[0]![1]).toEqual(expect.objectContaining({ n: 1 }));
|
||||
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 2.5, 0), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 3, 0), avcMetadata);
|
||||
await source.add(new EncodedPacket(avcPacketData, 'delta', 3.5, 0), avcMetadata);
|
||||
|
||||
expect(onPlaylist).not.toHaveBeenCalled();
|
||||
expect(onMaster).not.toHaveBeenCalled();
|
||||
|
||||
await output.finalize();
|
||||
|
||||
// Second segment finalized on close
|
||||
expect(onSegment).toHaveBeenCalledTimes(2);
|
||||
expect(onSegment.mock.calls[1]![1]).toEqual(expect.objectContaining({ n: 2 }));
|
||||
|
||||
// Both segment calls should have a Target as the first argument
|
||||
expect(onSegment.mock.calls[0]![0]).toBeDefined();
|
||||
expect(onSegment.mock.calls[1]![0]).toBeDefined();
|
||||
|
||||
// Playlist and master should have been called once each
|
||||
expect(onPlaylist).toHaveBeenCalledTimes(1);
|
||||
expect(typeof onPlaylist.mock.calls[0]![0]).toBe('string');
|
||||
expect(onPlaylist.mock.calls[0]![0]).toContain('#EXTM3U');
|
||||
expect(onPlaylist.mock.calls[0]![1]).toEqual(expect.objectContaining({ n: 1 }));
|
||||
|
||||
expect(onMaster).toHaveBeenCalledTimes(1);
|
||||
expect(typeof onMaster.mock.calls[0]![0]).toBe('string');
|
||||
expect(onMaster.mock.calls[0]![0]).toContain('#EXTM3U');
|
||||
});
|
||||
|
||||
@@ -6,10 +6,6 @@ Also, why not just have the packet metadata on the packet? I think that would ma
|
||||
|
||||
- Add an HLS "live mode". Challenge, where does bitrate come from?
|
||||
|
||||
events:
|
||||
- onPlaylist
|
||||
- onSegment
|
||||
- onMaster
|
||||
|
||||
- hls muxer starting offset
|
||||
- hls demuxer offset by segment number * targetduration
|
||||
- fmp4 muxing (CMAF??)
|
||||
Reference in New Issue
Block a user