Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DateRange parsing and validation enhancements for Interstitials #6213

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
47 changes: 44 additions & 3 deletions api-extractor/report/hls.js.api.md
Expand Up @@ -85,7 +85,7 @@ export type ABRControllerConfig = {
//
// @public (undocumented)
export class AttrList {
constructor(attrs: string | Record<string, any>);
constructor(attrs: string | Record<string, any>, parsed?: Pick<ParsedMultivariantPlaylist | LevelDetails, 'variableList' | 'hasVariableRefs' | 'playlistParsingError'>);
// (undocumented)
[key: string]: any;
// (undocumented)
Expand All @@ -104,13 +104,19 @@ export class AttrList {
// (undocumented)
enumeratedString(attrName: string): string | undefined;
// (undocumented)
enumeratedStringList<T extends {
[key: string]: boolean;
}>(attrName: string, dict: T): {
[key in keyof T]: boolean;
};
// (undocumented)
hexadecimalInteger(attrName: string): Uint8Array | null;
// (undocumented)
hexadecimalIntegerAsNumber(attrName: string): number;
// (undocumented)
optionalFloat(attrName: string, defaultValue: number): number;
// (undocumented)
static parseAttrList(input: string): Record<string, any>;
static parseAttrList(input: string, parsed?: Pick<ParsedMultivariantPlaylist | LevelDetails, 'variableList' | 'hasVariableRefs' | 'playlistParsingError'>): Record<string, string>;
}

// Warning: (ae-missing-release-tag) "AudioPlaylistType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down Expand Up @@ -838,12 +844,14 @@ export interface CuesParsedData {
//
// @public (undocumented)
export class DateRange {
constructor(dateRangeAttr: AttrList, dateRangeWithSameId?: DateRange);
constructor(dateRangeAttr: AttrList, dateRangeWithSameId?: DateRange | undefined, tagCount?: number);
// (undocumented)
attr: AttrList;
// (undocumented)
get class(): string;
// (undocumented)
get cue(): DateRangeCue;
// (undocumented)
get duration(): number | null;
// (undocumented)
get endDate(): Date | null;
Expand All @@ -852,13 +860,30 @@ export class DateRange {
// (undocumented)
get id(): string;
// (undocumented)
get isInterstitial(): boolean;
// (undocumented)
get isValid(): boolean;
// (undocumented)
get plannedDuration(): number | null;
// (undocumented)
get startDate(): Date;
// (undocumented)
get startTime(): number;
// (undocumented)
tagAnchor: Fragment | null;
// (undocumented)
tagOrder: number;
}

// Warning: (ae-missing-release-tag) "DateRangeCue" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type DateRangeCue = {
pre: boolean;
post: boolean;
once: boolean;
};

// Warning: (ae-missing-release-tag) "DRMSystemOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down Expand Up @@ -2177,6 +2202,8 @@ export class LevelDetails {
// (undocumented)
dateRanges: Record<string, DateRange>;
// (undocumented)
dateRangeTagCount: number;
// (undocumented)
deltaUpdateFailed?: boolean;
// (undocumented)
get drift(): number;
Expand Down Expand Up @@ -2996,6 +3023,20 @@ export interface NonNativeTextTracksData {
tracks: Array<NonNativeTextTrack>;
}

// Warning: (ae-missing-release-tag) "ParsedMultivariantPlaylist" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type ParsedMultivariantPlaylist = {
contentSteering: ContentSteeringOptions | null;
levels: LevelParsed[];
playlistParsingError: Error | null;
sessionData: Record<string, AttrList> | null;
sessionKeys: LevelKey[] | null;
startTimeOffset: number | null;
variableList: VariableMap | null;
hasVariableRefs: boolean;
};

// Warning: (ae-missing-release-tag) "Part" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
Expand Down
4 changes: 3 additions & 1 deletion src/controller/audio-stream-controller.ts
Expand Up @@ -128,7 +128,9 @@ class AudioStreamController
if (id === 'main') {
const cc = frag.cc;
this.initPTS[frag.cc] = { baseTime: initPTS, timescale };
this.log(`InitPTS for cc: ${cc} found from main: ${initPTS}`);
this.log(
`InitPTS for cc: ${cc} found from main: ${initPTS}/${timescale}`,
);
this.videoTrackCC = cc;
// If we are waiting, tick immediately to unblock audio fragment transmuxing
if (this.state === State.WAITING_INIT_PTS) {
Expand Down
66 changes: 38 additions & 28 deletions src/controller/id3-track-controller.ts
Expand Up @@ -9,10 +9,12 @@ import {
isDateRangeCueAttribute,
isSCTE35Attribute,
} from '../loader/date-range';
import { LevelDetails } from '../loader/level-details';
import { MetadataSchema } from '../types/demuxer';
import type {
BufferFlushingData,
FragParsingMetadataData,
LevelPTSUpdatedData,
LevelUpdatedData,
MediaAttachedData,
} from '../types/events';
Expand Down Expand Up @@ -69,10 +71,6 @@ const MAX_CUE_ENDTIME = (() => {
return Number.POSITIVE_INFINITY;
})();

function dateRangeDateToTimelineSeconds(date: Date, offset: number): number {
return date.getTime() / 1000 - offset;
}

function hexToArrayBuffer(str): ArrayBuffer {
return Uint8Array.from(
str
Expand Down Expand Up @@ -100,7 +98,7 @@ class ID3TrackController implements ComponentAPI {
this._registerListeners();
}

destroy() {
public destroy() {
this._unregisterListeners();
this.id3Track = null;
this.media = null;
Expand All @@ -117,6 +115,7 @@ class ID3TrackController implements ComponentAPI {
hls.on(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this);
hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
hls.on(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this);
}

private _unregisterListeners() {
Expand All @@ -127,17 +126,18 @@ class ID3TrackController implements ComponentAPI {
hls.off(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this);
hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
hls.off(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this);
}

// Add ID3 metatadata text track.
protected onMediaAttached(
private onMediaAttached(
event: Events.MEDIA_ATTACHED,
data: MediaAttachedData,
): void {
this.media = data.media;
}

protected onMediaDetaching(): void {
private onMediaDetaching(): void {
if (this.id3Track) {
clearCurrentCues(this.id3Track);
this.id3Track = null;
Expand All @@ -150,13 +150,13 @@ class ID3TrackController implements ComponentAPI {
this.dateRangeCuesAppended = {};
}

createTrack(media: HTMLMediaElement): TextTrack {
private createTrack(media: HTMLMediaElement): TextTrack {
const track = this.getID3Track(media.textTracks) as TextTrack;
track.mode = 'hidden';
return track;
}

getID3Track(textTracks: TextTrackList): TextTrack | void {
private getID3Track(textTracks: TextTrackList): TextTrack | void {
if (!this.media) {
return;
}
Expand All @@ -173,7 +173,7 @@ class ID3TrackController implements ComponentAPI {
return this.media.addTextTrack('metadata', 'id3');
}

onFragParsingMetadata(
private onFragParsingMetadata(
event: Events.FRAG_PARSING_METADATA,
data: FragParsingMetadataData,
) {
Expand Down Expand Up @@ -247,7 +247,7 @@ class ID3TrackController implements ComponentAPI {
}
}

updateId3CueEnds(startTime: number, type: MetadataSchema) {
private updateId3CueEnds(startTime: number, type: MetadataSchema) {
const cues = this.id3Track?.cues;
if (cues) {
for (let i = cues.length; i--; ) {
Expand All @@ -263,7 +263,7 @@ class ID3TrackController implements ComponentAPI {
}
}

onBufferFlushing(
private onBufferFlushing(
event: Events.BUFFER_FLUSHING,
{ startOffset, endOffset, type }: BufferFlushingData,
) {
Expand Down Expand Up @@ -295,7 +295,23 @@ class ID3TrackController implements ComponentAPI {
}
}

onLevelUpdated(event: Events.LEVEL_UPDATED, { details }: LevelUpdatedData) {
private onLevelUpdated(
event: Events.LEVEL_UPDATED,
{ details }: LevelUpdatedData,
) {
this.updateDateRangeCues(details, true);
}

private onLevelPtsUpdated(
event: Events.LEVEL_PTS_UPDATED,
data: LevelPTSUpdatedData,
) {
if (Math.abs(data.drift) > 0.01) {
this.updateDateRangeCues(data.details);
}
}

private updateDateRangeCues(details: LevelDetails, removeOldCues?: true) {
if (
!this.media ||
!details.hasProgramDateTime ||
Expand All @@ -307,7 +323,7 @@ class ID3TrackController implements ComponentAPI {
const { dateRanges } = details;
const ids = Object.keys(dateRanges);
// Remove cues from track not found in details.dateRanges
if (id3Track) {
if (id3Track && removeOldCues) {
const idsToRemove = Object.keys(dateRangeCuesAppended).filter(
(id) => !ids.includes(id),
);
Expand All @@ -329,26 +345,20 @@ class ID3TrackController implements ComponentAPI {
this.id3Track = this.createTrack(this.media);
}

const dateTimeOffset =
(lastFragment.programDateTime as number) / 1000 - lastFragment.start;
const Cue = getCueClass();

for (let i = 0; i < ids.length; i++) {
const id = ids[i];
const dateRange = dateRanges[id];
const startTime = dateRangeDateToTimelineSeconds(
dateRange.startDate,
dateTimeOffset,
);
const startTime = dateRange.startTime;

// Process DateRanges to determine end-time (known DURATION, END-DATE, or END-ON-NEXT)
const appendedDateRangeCues = dateRangeCuesAppended[id];
const cues = appendedDateRangeCues?.cues || {};
let durationKnown = appendedDateRangeCues?.durationKnown || false;
let endTime = MAX_CUE_ENDTIME;
const endDate = dateRange.endDate;
if (endDate) {
endTime = dateRangeDateToTimelineSeconds(endDate, dateTimeOffset);
const { duration, endDate } = dateRange;
if (endDate && duration !== null) {
endTime = startTime + duration;
durationKnown = true;
} else if (dateRange.endOnNext && !durationKnown) {
const nextDateRangeWithSameClass = ids.reduce(
Expand All @@ -369,10 +379,7 @@ class ID3TrackController implements ComponentAPI {
null,
);
if (nextDateRangeWithSameClass) {
endTime = dateRangeDateToTimelineSeconds(
nextDateRangeWithSameClass.startDate,
dateTimeOffset,
);
endTime = nextDateRangeWithSameClass.startTime;
durationKnown = true;
}
}
Expand All @@ -389,6 +396,9 @@ class ID3TrackController implements ComponentAPI {
if (cue) {
if (durationKnown && !appendedDateRangeCues.durationKnown) {
cue.endTime = endTime;
} else if (Math.abs(cue.startTime - startTime) > 0.01) {
cue.startTime = startTime;
cue.endTime = endTime;
}
} else if (Cue) {
let data = dateRange.attr[key];
Expand Down
3 changes: 2 additions & 1 deletion src/hls.ts
Expand Up @@ -1068,7 +1068,7 @@ export type {
KeySystems,
KeySystemFormats,
} from './utils/mediakeys-helper';
export type { DateRange } from './loader/date-range';
export type { DateRange, DateRangeCue } from './loader/date-range';
export type { LoadStats } from './loader/load-stats';
export type { LevelKey } from './loader/level-key';
export type { LevelDetails } from './loader/level-details';
Expand Down Expand Up @@ -1184,3 +1184,4 @@ export type {
IErrorAction,
} from './controller/error-controller';
export type { AttrList } from './utils/attr-list';
export type { ParsedMultivariantPlaylist } from './loader/m3u8-parser';