Skip to content

Commit

Permalink
DateRange parsing and validation enhancements for Interstitials (#6213)
Browse files Browse the repository at this point in the history
* DateRange parsing and validation enhancements for Interstitials
Warn on invalid quoted-attribute attributes (missing quotes)
Related to #6203

* Shift DateRange metadata Cue times when media timeline is shifted
Fixes #6203

* Map DATERANGE tags to Segments with starting program date time

* DATERANGE tags after the last PDT tag that start at or after that tag should be anchored to that tag

* Handle DateRange mapping with Delta Playlist updates
Error (but parse and merge) multiple EXT-X-SKIP tags
  • Loading branch information
robwalch authored Jun 6, 2024
1 parent 1948f96 commit 514cc10
Show file tree
Hide file tree
Showing 26 changed files with 1,142 additions and 430 deletions.
71 changes: 60 additions & 11 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
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 @@ -352,18 +358,18 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP
protected getAppendedFrag(position: number, playlistType?: PlaylistLevelType): Fragment | null;
// (undocumented)
protected getCurrentContext(chunkMeta: ChunkMetadata): {
frag: Fragment;
frag: MediaFragment;
part: Part | null;
level: Level;
} | null;
// (undocumented)
protected getFragmentAtPosition(bufferEnd: number, end: number, levelDetails: LevelDetails): Fragment | null;
protected getFragmentAtPosition(bufferEnd: number, end: number, levelDetails: LevelDetails): MediaFragment | null;
// (undocumented)
protected getFwdBufferInfo(bufferable: Bufferable | null, type: PlaylistLevelType): BufferInfo | null;
// (undocumented)
protected getFwdBufferInfoAtPos(bufferable: Bufferable | null, pos: number, type: PlaylistLevelType): BufferInfo | null;
// (undocumented)
protected getInitialLiveFragment(levelDetails: LevelDetails, fragments: Array<Fragment>): Fragment | null;
protected getInitialLiveFragment(levelDetails: LevelDetails, fragments: MediaFragment[]): MediaFragment | null;
// (undocumented)
protected getLevelDetails(): LevelDetails | undefined;
// (undocumented)
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 @@ -1949,7 +1974,7 @@ export interface ILogger {
// @public (undocumented)
export interface InitPTSFoundData {
// (undocumented)
frag: Fragment;
frag: MediaFragment;
// (undocumented)
id: string;
// (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 All @@ -2199,9 +2226,9 @@ export class LevelDetails {
// (undocumented)
get fragmentEnd(): number;
// (undocumented)
fragmentHint?: Fragment;
fragmentHint?: MediaFragment;
// (undocumented)
fragments: Fragment[];
fragments: MediaFragment[];
// (undocumented)
get hasProgramDateTime(): boolean;
// (undocumented)
Expand Down Expand Up @@ -2809,6 +2836,14 @@ export interface MediaEndedData {
stalled: boolean;
}

// Warning: (ae-missing-release-tag) "MediaFragment" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface MediaFragment extends Fragment {
// (undocumented)
sn: number;
}

// Warning: (ae-missing-release-tag) "MediaKeyFunc" 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 @@ -2996,17 +3031,31 @@ 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
export class Part extends BaseSegment {
constructor(partAttrs: AttrList, frag: Fragment, baseurl: string, index: number, previous?: Part);
constructor(partAttrs: AttrList, frag: MediaFragment, baseurl: string, index: number, previous?: Part);
// (undocumented)
readonly duration: number;
// (undocumented)
get end(): number;
// (undocumented)
readonly fragment: Fragment;
readonly fragment: MediaFragment;
// (undocumented)
readonly fragOffset: number;
// (undocumented)
Expand Down
18 changes: 13 additions & 5 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { Bufferable, BufferHelper } from '../utils/buffer-helper';
import { FragmentState } from './fragment-tracker';
import { Level } from '../types/level';
import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
import { Fragment, ElementaryStreamTypes, Part } from '../loader/fragment';
import {
Fragment,
ElementaryStreamTypes,
Part,
MediaFragment,
} from '../loader/fragment';
import ChunkCache from '../demux/chunk-cache';
import TransmuxerInterface from '../demux/transmuxer-interface';
import { ChunkMetadata } from '../types/transmuxer';
Expand Down Expand Up @@ -40,7 +45,7 @@ import type { MediaPlaylist } from '../types/media-playlist';
const TICK_INTERVAL = 100; // how often to tick in ms

type WaitingForPTSData = {
frag: Fragment;
frag: MediaFragment;
part: Part | null;
cache: ChunkCache;
complete: boolean;
Expand Down Expand Up @@ -128,7 +133,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 Expand Up @@ -561,7 +568,8 @@ class AudioStreamController
}

_handleFragmentLoadProgress(data: FragLoadedData) {
const { frag, part, payload } = data;
const frag = data.frag as MediaFragment;
const { part, payload } = data;
const { config, trackId, levels } = this;
if (!levels) {
this.warn(
Expand Down Expand Up @@ -606,7 +614,7 @@ class AudioStreamController
const partial = partIndex !== -1;
const chunkMeta = new ChunkMetadata(
frag.level,
frag.sn as number,
frag.sn,
frag.stats.chunkCount,
payload.byteLength,
partIndex,
Expand Down
54 changes: 28 additions & 26 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
updateFragPTSDTS,
} from '../utils/level-helper';
import TransmuxerInterface from '../demux/transmuxer-interface';
import { Fragment, Part } from '../loader/fragment';
import { Fragment, MediaFragment, Part } from '../loader/fragment';
import FragmentLoader, {
FragmentLoadProgressCallback,
LoadError,
Expand Down Expand Up @@ -954,22 +954,24 @@ export default class BaseStreamController
part.stats.parsing.end = now;
}
// See if part loading should be disabled/enabled based on buffer and playback position.
if (frag.sn !== 'initSegment') {
const levelDetails = this.getLevelDetails();
const loadingPartsAtEdge = levelDetails && frag.sn > levelDetails.endSN;
const shouldLoadParts =
loadingPartsAtEdge || this.shouldLoadParts(levelDetails, frag.end);
if (shouldLoadParts !== this.loadingParts) {
this.log(
`LL-Part loading ${
shouldLoadParts ? 'ON' : 'OFF'
} after parsing segment ending @${frag.end.toFixed(2)}`,
);
this.loadingParts = shouldLoadParts;
}
const levelDetails = this.getLevelDetails();
const loadingPartsAtEdge = levelDetails && frag.sn > levelDetails.endSN;
const shouldLoadParts =
loadingPartsAtEdge || this.shouldLoadParts(levelDetails, frag.end);
if (shouldLoadParts !== this.loadingParts) {
this.log(
`LL-Part loading ${
shouldLoadParts ? 'ON' : 'OFF'
} after parsing segment ending @${frag.end.toFixed(2)}`,
);
this.loadingParts = shouldLoadParts;
}

this.updateLevelTiming(frag, part, level, chunkMeta.partial);
this.updateLevelTiming(
frag as MediaFragment,
part,
level,
chunkMeta.partial,
);
}

private shouldLoadParts(
Expand Down Expand Up @@ -999,7 +1001,7 @@ export default class BaseStreamController

protected getCurrentContext(
chunkMeta: ChunkMetadata,
): { frag: Fragment; part: Part | null; level: Level } | null {
): { frag: MediaFragment; part: Part | null; level: Level } | null {
const { levels, fragCurrent } = this;
const { level: levelIndex, sn, part: partIndex } = chunkMeta;
if (!levels?.[levelIndex]) {
Expand Down Expand Up @@ -1183,7 +1185,7 @@ export default class BaseStreamController
const { config } = this;
const start = fragments[0].start;
const canLoadParts = config.lowLatencyMode && !!levelDetails.partList;
let frag: Fragment | null = null;
let frag: MediaFragment | null = null;

if (levelDetails.live) {
const initialLiveManifestSize = config.initialLiveManifestSize;
Expand Down Expand Up @@ -1326,10 +1328,10 @@ export default class BaseStreamController
*/
protected getInitialLiveFragment(
levelDetails: LevelDetails,
fragments: Array<Fragment>,
): Fragment | null {
fragments: MediaFragment[],
): MediaFragment | null {
const fragPrevious = this.fragPrevious;
let frag: Fragment | null = null;
let frag: MediaFragment | null = null;
if (fragPrevious) {
if (levelDetails.hasProgramDateTime) {
// Prefer using PDT, because it can be accurate enough to choose the correct fragment without knowing the level sliding
Expand Down Expand Up @@ -1393,7 +1395,7 @@ export default class BaseStreamController
bufferEnd: number,
end: number,
levelDetails: LevelDetails,
): Fragment | null {
): MediaFragment | null {
const { config } = this;
let { fragPrevious } = this;
let { fragments, endSN } = levelDetails;
Expand All @@ -1409,10 +1411,10 @@ export default class BaseStreamController
if (loadingParts && fragmentHint && !this.bitrateTest) {
// Include incomplete fragment with parts at end
fragments = fragments.concat(fragmentHint);
endSN = fragmentHint.sn as number;
endSN = fragmentHint.sn;
}

let frag;
let frag: MediaFragment | null;
if (bufferEnd < end) {
const lookupTolerance =
bufferEnd > end - maxFragLookUpTolerance ? 0 : maxFragLookUpTolerance;
Expand Down Expand Up @@ -1649,7 +1651,7 @@ export default class BaseStreamController
}
const gapTagEncountered = data.details === ErrorDetails.FRAG_GAP;
if (gapTagEncountered) {
this.fragmentTracker.fragBuffered(frag, true);
this.fragmentTracker.fragBuffered(frag as MediaFragment, true);
}
// keep retrying until the limit will be reached
const errorAction = data.errorAction;
Expand Down Expand Up @@ -1813,7 +1815,7 @@ export default class BaseStreamController
}

private updateLevelTiming(
frag: Fragment,
frag: MediaFragment,
part: Part | null,
level: Level,
partial: boolean,
Expand Down
Loading

0 comments on commit 514cc10

Please sign in to comment.