mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-01 04:43:51 +02:00
Add HlsInputFormatOptions.offsetTimestampsByDateTime option, add InputTrack.getUnixTimeForTimestamp()
This commit is contained in:
@@ -215,6 +215,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
@@ -595,6 +595,10 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
@@ -759,6 +759,10 @@ abstract class HlsInputTrackBacking implements InputTrackBacking {
|
||||
return this.delegate(() => this.internalTrack.backingTrack!.isRelativeToUnixEpoch());
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp(timestamp: number): MaybePromise<number | null> {
|
||||
return this.delegate(() => this.internalTrack.backingTrack!.getUnixTimeForTimestamp(timestamp));
|
||||
}
|
||||
|
||||
getBitrate(): number | null {
|
||||
return this.internalTrack.peakBitrate;
|
||||
}
|
||||
|
||||
@@ -141,8 +141,11 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
}
|
||||
}
|
||||
|
||||
const offsetTimestampsByDateTime = this.input._formatOptions.hls?.offsetTimestampsByDateTime !== false;
|
||||
|
||||
let headerRead = false;
|
||||
let accumulatedTime = 0;
|
||||
let accumulatedUnixTime: number | null = null;
|
||||
let nextSegmentDuration: number | null = null;
|
||||
let currentKey: HlsEncryptionInfo | null = null;
|
||||
let nextSequenceNumber = 0;
|
||||
@@ -189,6 +192,9 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
currentFirstSegment = prevLastSegment.firstSegment;
|
||||
currentInitSegment = prevLastSegment.initSegment;
|
||||
lastProgramDateTimeSeconds = prevLastSegment.lastProgramDateTimeSeconds;
|
||||
accumulatedUnixTime = prevLastSegment.unixEpochTimestamp !== null
|
||||
? prevLastSegment.unixEpochTimestamp + prevLastSegment.duration
|
||||
: null;
|
||||
prevLastSegment = null;
|
||||
}
|
||||
}
|
||||
@@ -235,7 +241,7 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
|
||||
const segment: HlsSegment = {
|
||||
timestamp: accumulatedTime,
|
||||
relativeToUnixEpoch: lastProgramDateTimeSeconds !== null,
|
||||
unixEpochTimestamp: accumulatedUnixTime,
|
||||
firstSegment: currentFirstSegment,
|
||||
sequenceNumber: nextSequenceNumber,
|
||||
location,
|
||||
@@ -247,6 +253,9 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
|
||||
currentFirstSegment ??= segment;
|
||||
accumulatedTime += nextSegmentDuration;
|
||||
if (accumulatedUnixTime !== null) {
|
||||
accumulatedUnixTime += nextSegmentDuration;
|
||||
}
|
||||
|
||||
this.segments.push(segment);
|
||||
} else {
|
||||
@@ -320,7 +329,7 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
|
||||
const segment: HlsSegment = {
|
||||
timestamp: accumulatedTime,
|
||||
relativeToUnixEpoch: lastProgramDateTimeSeconds !== null,
|
||||
unixEpochTimestamp: accumulatedUnixTime,
|
||||
firstSegment: null,
|
||||
sequenceNumber: null,
|
||||
location,
|
||||
@@ -478,15 +487,19 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
const offset = dateTimeSeconds - lastSegmentEnd;
|
||||
|
||||
for (const segment of this.segments) {
|
||||
segment.timestamp += offset;
|
||||
segment.relativeToUnixEpoch = true;
|
||||
segment.unixEpochTimestamp = segment.timestamp + offset;
|
||||
if (offsetTimestampsByDateTime) {
|
||||
segment.timestamp = segment.unixEpochTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
accumulatedTime += offset;
|
||||
}
|
||||
|
||||
lastProgramDateTimeSeconds = dateTimeSeconds;
|
||||
accumulatedTime = dateTimeSeconds; // Snap the accumulated time to the datetime
|
||||
accumulatedUnixTime = dateTimeSeconds;
|
||||
|
||||
if (offsetTimestampsByDateTime) {
|
||||
accumulatedTime = dateTimeSeconds; // Snap the accumulated time into Unix space
|
||||
}
|
||||
} else if (line === TAG_DISCONTINUITY) {
|
||||
currentFirstSegment = null;
|
||||
// Note: the init segment is not reset; the #EXT-X-MAP statement simply lasts until the next
|
||||
|
||||
@@ -197,6 +197,7 @@ export {
|
||||
IsobmffInputFormat,
|
||||
type IsobmffInputFormatOptions,
|
||||
HlsInputFormat,
|
||||
type HlsInputFormatOptions,
|
||||
MatroskaInputFormat,
|
||||
Mp3InputFormat,
|
||||
Mp4InputFormat,
|
||||
|
||||
@@ -727,6 +727,8 @@ export const HLS_FORMATS: InputFormat[] = [HLS, MP4, QTFF, MP3, ADTS, MPEG_TS];
|
||||
export type InputFormatOptions = {
|
||||
/** ISOBMFF-specific configuration. */
|
||||
isobmff?: IsobmffInputFormatOptions;
|
||||
/** HLS-specific configuration. */
|
||||
hls?: HlsInputFormatOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -755,6 +757,26 @@ export type IsobmffInputFormatOptions = {
|
||||
_suppressPsshParsing?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Additional HLS input configuration.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export type HlsInputFormatOptions = {
|
||||
/**
|
||||
* Whether, in the presence of `#EXT-X-PROGRAM-DATE-TIME` tags, to offset track and packet timestamps to be relative
|
||||
* to the Unix epoch.
|
||||
*
|
||||
* Defaults to `true`, meaning packet timestamps map directly to wall-clock time. This guarantees AV sync across
|
||||
* multiple tracks, even with gaps present.
|
||||
*
|
||||
* When you don't want this mapping, you can set this value to `false`. In addition to timestamps not being Unix
|
||||
* timestamps anymore, any gaps in the playlist are also naturally removed. When `false`, you can still access the
|
||||
* wall-clock Unix timestamps via {@link InputTrack.getUnixTimeForTimestamp}.
|
||||
*/
|
||||
offsetTimestampsByDateTime?: boolean;
|
||||
};
|
||||
|
||||
export const validateInputFormatOptions = (options: InputFormatOptions, prefix: string) => {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new TypeError(`${prefix}, when provided, must be an object.`);
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface InputTrackBacking {
|
||||
getLanguageCode(): MaybePromise<string>;
|
||||
getTimeResolution(): MaybePromise<number>;
|
||||
isRelativeToUnixEpoch(): MaybePromise<boolean>;
|
||||
getUnixTimeForTimestamp(timestamp: number): MaybePromise<number | null>;
|
||||
getDisposition(): MaybePromise<TrackDisposition>;
|
||||
getPairingMask(): bigint;
|
||||
getBitrate(): MaybePromise<number | null>;
|
||||
@@ -203,6 +204,28 @@ export abstract class InputTrack {
|
||||
return this._backing.isRelativeToUnixEpoch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Unix time (in seconds since January 1, 1970 00:00:00 UTC) that the given track timestamp (in seconds)
|
||||
* maps to, or `null` if there is no such mapping. This provides a piecewise-continuous mapping from this track's
|
||||
* timestamp space into wall-clock time. Such mapping exists, for example, for HLS playlists with
|
||||
* `#EXT-X-PROGRAM-DATE-TIME` tags present.
|
||||
*
|
||||
* This mapping can be available even when {@link InputTrack.isRelativeToUnixEpoch} is `false`, for example for HLS
|
||||
* streams with program date time information but with {@link HlsInputFormatOptions.offsetTimestampsByDateTime}
|
||||
* set to `false`.
|
||||
*/
|
||||
async getUnixTimeForTimestamp(timestamp: number): Promise<number | null> {
|
||||
return this._backing.getUnixTimeForTimestamp(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the track's timestamps can be mapped to Unix wall clock time via
|
||||
* {@link InputTrack.getUnixTimeForTimestamp}.
|
||||
*/
|
||||
async hasUnixTimeMapping(): Promise<boolean> {
|
||||
return (await this._backing.getUnixTimeForTimestamp(await this.getFirstTimestamp())) !== null;
|
||||
}
|
||||
|
||||
/** Returns the track's disposition, i.e. information about its intended usage. */
|
||||
async getDisposition() {
|
||||
return this._backing.getDisposition();
|
||||
|
||||
@@ -2811,6 +2811,10 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDisposition() {
|
||||
return this.internalTrack.disposition;
|
||||
}
|
||||
|
||||
@@ -1953,6 +1953,10 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDisposition() {
|
||||
return this.internalTrack.disposition;
|
||||
}
|
||||
|
||||
@@ -264,6 +264,10 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
@@ -1103,6 +1103,10 @@ abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
@@ -454,6 +454,10 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
+23
-2
@@ -41,7 +41,12 @@ export type AssociatedGroup = {
|
||||
export type Segment = {
|
||||
timestamp: number;
|
||||
duration: number;
|
||||
relativeToUnixEpoch: boolean;
|
||||
/**
|
||||
* The Unix time (in seconds) corresponding to this segment's start timestamp, or null if unknown. This is computed
|
||||
* whenever the source provides wall-clock information (e.g. HLS program date time), even if the segment timestamps
|
||||
* themselves are not shifted into Unix time space.
|
||||
*/
|
||||
unixEpochTimestamp: number | null;
|
||||
firstSegment: Segment | null;
|
||||
};
|
||||
|
||||
@@ -97,6 +102,18 @@ export abstract class SegmentedInput {
|
||||
return lastSegment.timestamp + lastSegment.duration;
|
||||
}
|
||||
|
||||
async getUnixTimeForTimestamp(timestamp: number): Promise<number | null> {
|
||||
let segment = await this.getSegmentAt(timestamp, {});
|
||||
segment ??= await this.getFirstSegment({});
|
||||
|
||||
if (!segment || segment.unixEpochTimestamp === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elapsed = timestamp - segment.timestamp;
|
||||
return segment.unixEpochTimestamp + elapsed;
|
||||
}
|
||||
|
||||
async getTrackBackings(): Promise<InputTrackBacking[]> {
|
||||
return this.trackBackingsPromise ??= (async () => {
|
||||
const backings: InputTrackBacking[] = [];
|
||||
@@ -310,7 +327,11 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking {
|
||||
await this.hydrate();
|
||||
|
||||
assert(this.segmentedInput.firstSegment);
|
||||
return this.segmentedInput.firstSegment.relativeToUnixEpoch;
|
||||
return this.segmentedInput.firstSegment.unixEpochTimestamp === this.segmentedInput.firstSegment.timestamp;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp(timestamp: number) {
|
||||
return this.segmentedInput.getUnixTimeForTimestamp(timestamp);
|
||||
}
|
||||
|
||||
getBitrate() {
|
||||
|
||||
@@ -402,6 +402,10 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user