Skip to content

Commit

Permalink
DateRange parsing and validation enhancements for Interstitials
Browse files Browse the repository at this point in the history
Warn on invalid quoted-attribute attributes (missing quotes)
Related to #6203
  • Loading branch information
robwalch committed Apr 11, 2024
1 parent 0a49731 commit bc777d0
Show file tree
Hide file tree
Showing 11 changed files with 433 additions and 216 deletions.
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, frag?: Fragment | null, 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
23 changes: 5 additions & 18 deletions src/controller/id3-track-controller.ts
Expand Up @@ -69,10 +69,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 @@ -329,26 +325,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 +359,7 @@ class ID3TrackController implements ComponentAPI {
null,
);
if (nextDateRangeWithSameClass) {
endTime = dateRangeDateToTimelineSeconds(
nextDateRangeWithSameClass.startDate,
dateTimeOffset,
);
endTime = nextDateRangeWithSameClass.startTime;
durationKnown = true;
}
}
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';
69 changes: 66 additions & 3 deletions src/loader/date-range.ts
@@ -1,23 +1,35 @@
import { AttrList } from '../utils/attr-list';
import { logger } from '../utils/logger';
import type { Fragment } from './fragment';

// Avoid exporting const enum so that these values can be inlined
const enum DateRangeAttribute {
ID = 'ID',
CLASS = 'CLASS',
CUE = 'CUE',
START_DATE = 'START-DATE',
DURATION = 'DURATION',
END_DATE = 'END-DATE',
END_ON_NEXT = 'END-ON-NEXT',
PLANNED_DURATION = 'PLANNED-DURATION',
SCTE35_OUT = 'SCTE35-OUT',
SCTE35_IN = 'SCTE35-IN',
SCTE35_CMD = 'SCTE35-CMD',
}

export type DateRangeCue = {
pre: boolean;
post: boolean;
once: boolean;
};

const CLASS_INTERSTITIAL = 'com.apple.hls.interstitial';

export function isDateRangeCueAttribute(attrName: string): boolean {
return (
attrName !== DateRangeAttribute.ID &&
attrName !== DateRangeAttribute.CLASS &&
attrName !== DateRangeAttribute.CUE &&
attrName !== DateRangeAttribute.START_DATE &&
attrName !== DateRangeAttribute.DURATION &&
attrName !== DateRangeAttribute.END_DATE &&
Expand All @@ -28,17 +40,28 @@ export function isDateRangeCueAttribute(attrName: string): boolean {
export function isSCTE35Attribute(attrName: string): boolean {
return (
attrName === DateRangeAttribute.SCTE35_OUT ||
attrName === DateRangeAttribute.SCTE35_IN
attrName === DateRangeAttribute.SCTE35_IN ||
attrName === DateRangeAttribute.SCTE35_CMD
);
}

export class DateRange {
public attr: AttrList;
public tagAnchor: Fragment | null;
public tagOrder: number;
private _startDate: Date;
private _endDate?: Date;
private _cue?: DateRangeCue;
private _badValueForSameId?: string;

constructor(dateRangeAttr: AttrList, dateRangeWithSameId?: DateRange) {
constructor(
dateRangeAttr: AttrList,
dateRangeWithSameId?: DateRange | undefined,
frag: Fragment | null = null,
tagCount: number = 0,
) {
this.tagAnchor = dateRangeWithSameId?.tagAnchor ?? frag;
this.tagOrder = dateRangeWithSameId?.tagOrder ?? tagCount;
if (dateRangeWithSameId) {
const previousAttr = dateRangeWithSameId.attr;
for (const key in previousAttr) {
Expand Down Expand Up @@ -78,6 +101,36 @@ export class DateRange {
return this.attr.CLASS;
}

get cue(): DateRangeCue {
const _cue = this._cue;
if (_cue === undefined) {
return (this._cue = this.attr.enumeratedStringList(
this.attr.CUE ? 'CUE' : 'X-CUE',
{
pre: false,
post: false,
once: false,
},
));
}
return _cue;
}

get startTime(): number {
const { tagAnchor } = this;
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (tagAnchor === null || tagAnchor.programDateTime === null) {
logger.warn(
`Expected tagAnchor Fragment with PDT set for DateRange "${this.id}": ${tagAnchor}`,
);
return NaN;
}
return (
tagAnchor.start +
(this.startDate.getTime() - tagAnchor.programDateTime) / 1000
);
}

get startDate(): Date {
return this._startDate;
}
Expand Down Expand Up @@ -120,13 +173,23 @@ export class DateRange {
return this.attr.bool(DateRangeAttribute.END_ON_NEXT);
}

get isInterstitial(): boolean {
return this.class === CLASS_INTERSTITIAL;
}

get isValid(): boolean {
return (
!!this.id &&
!this._badValueForSameId &&
Number.isFinite(this.startDate.getTime()) &&
(this.duration === null || this.duration >= 0) &&
(!this.endOnNext || !!this.class)
(!this.endOnNext || !!this.class) &&
(!this.attr.CUE ||
(!this.cue.pre && !this.cue.post) ||
this.cue.pre !== this.cue.post) &&
(!this.isInterstitial ||
'X-ASSET-URI' in this.attr ||
'X-ASSET-LIST' in this.attr)
);
}
}
1 change: 1 addition & 0 deletions src/loader/level-details.ts
Expand Up @@ -19,6 +19,7 @@ export class LevelDetails {
public fragmentHint?: Fragment;
public partList: Part[] | null = null;
public dateRanges: Record<string, DateRange>;
public dateRangeTagCount: number = 0;
public live: boolean = true;
public ageHeader: number = 0;
public advancedDateTime?: number;
Expand Down

0 comments on commit bc777d0

Please sign in to comment.