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

Merged
merged 8 commits into from
Jun 6, 2024
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
Loading