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

Use Content Steering Pathways to manage Redundant Streams #5970

Merged
merged 3 commits into from
Nov 15, 2023
Merged
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
97 changes: 84 additions & 13 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1625,7 +1625,7 @@ class Hls implements HlsEventEmitter {
// (undocumented)
removeAllListeners<E extends keyof HlsListeners>(event?: E | undefined): void;
// (undocumented)
removeLevel(levelIndex: any, urlId?: number): void;
removeLevel(levelIndex: number): void;
resumeBuffering(): void;
get startLevel(): number;
// Warning: (ae-setter-with-docs) The doc comment for the property "startLevel" must appear on the getter, not the setter.
Expand Down Expand Up @@ -2003,11 +2003,11 @@ export type LatencyControllerConfig = {
//
// @public (undocumented)
export class Level {
constructor(data: LevelParsed);
constructor(data: LevelParsed | MediaPlaylist);
// (undocumented)
addFallback(data: LevelParsed): void;
addFallback(): void;
// (undocumented)
addGroupId(type: string, groupId: string | undefined, fallbackIndex: number): void;
addGroupId(type: string, groupId: string | undefined): void;
// (undocumented)
get attrs(): LevelAttributes;
// (undocumented)
Expand All @@ -2017,7 +2017,7 @@ export class Level {
// (undocumented)
get audioGroupId(): string | undefined;
// (undocumented)
audioGroupIds?: (string | undefined)[];
get audioGroupIds(): (string | undefined)[] | undefined;
// (undocumented)
get audioGroups(): (string | undefined)[] | undefined;
// (undocumented)
Expand All @@ -2035,6 +2035,10 @@ export class Level {
// (undocumented)
readonly frameRate: number;
// (undocumented)
hasAudioGroup(groupId: string | undefined): boolean;
// (undocumented)
hasSubtitleGroup(groupId: string | undefined): boolean;
// (undocumented)
readonly height: number;
// (undocumented)
readonly id: number;
Expand Down Expand Up @@ -2064,13 +2068,11 @@ export class Level {
// (undocumented)
get textGroupId(): string | undefined;
// (undocumented)
textGroupIds?: (string | undefined)[];
// (undocumented)
readonly unknownCodecs: string[] | undefined;
get textGroupIds(): (string | undefined)[] | undefined;
// (undocumented)
get uri(): string;
// (undocumented)
url: string[];
readonly url: string[];
// (undocumented)
get urlId(): number;
set urlId(value: number);
Expand Down Expand Up @@ -2309,6 +2311,8 @@ export interface LevelLoadingData {
// (undocumented)
level: number;
// (undocumented)
pathwayId: string | undefined;
// (undocumented)
url: string;
}

Expand All @@ -2329,8 +2333,6 @@ export interface LevelParsed {
// (undocumented)
id?: number;
// (undocumented)
level?: number;
// (undocumented)
name: string;
// (undocumented)
textCodec?: string;
Expand Down Expand Up @@ -2383,9 +2385,58 @@ export interface LevelSwitchedData {
// Warning: (ae-missing-release-tag) "LevelSwitchingData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface LevelSwitchingData extends Omit<Level, '_urlId'> {
export interface LevelSwitchingData {
// (undocumented)
attrs: LevelAttributes;
// (undocumented)
audioCodec: string | undefined;
// (undocumented)
audioGroupIds: (string | undefined)[] | undefined;
// (undocumented)
audioGroups: (string | undefined)[] | undefined;
// (undocumented)
averageBitrate: number;
// (undocumented)
bitrate: number;
// (undocumented)
codecSet: string;
// (undocumented)
details: LevelDetails | undefined;
// (undocumented)
fragmentError: number;
// (undocumented)
height: number;
// (undocumented)
id: number;
// (undocumented)
level: number;
// (undocumented)
loaded: {
bytes: number;
duration: number;
} | undefined;
// (undocumented)
loadError: number;
// (undocumented)
maxBitrate: number;
// (undocumented)
name: string | undefined;
// (undocumented)
realBitrate: number;
// (undocumented)
subtitleGroups: (string | undefined)[] | undefined;
// (undocumented)
textGroupIds: (string | undefined)[] | undefined;
// (undocumented)
uri: string;
// (undocumented)
url: string[];
// (undocumented)
urlId: 0;
// (undocumented)
videoCodec: string | undefined;
// (undocumented)
width: number;
}

// Warning: (ae-missing-release-tag) "LevelUpdatedData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down Expand Up @@ -2739,22 +2790,30 @@ export interface MediaKeySessionContext {
// Warning: (ae-missing-release-tag) "MediaPlaylist" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface MediaPlaylist extends Omit<LevelParsed, 'attrs'> {
export interface MediaPlaylist {
// (undocumented)
attrs: MediaAttributes;
// (undocumented)
audioCodec?: string;
// (undocumented)
autoselect: boolean;
// (undocumented)
bitrate: number;
// (undocumented)
channels?: string;
// (undocumented)
characteristics?: string;
// (undocumented)
default: boolean;
// (undocumented)
details?: LevelDetails;
// (undocumented)
forced: boolean;
// (undocumented)
groupId: string;
// (undocumented)
height?: number;
// (undocumented)
id: number;
// (undocumented)
instreamId?: string;
Expand All @@ -2763,7 +2822,17 @@ export interface MediaPlaylist extends Omit<LevelParsed, 'attrs'> {
// (undocumented)
name: string;
// (undocumented)
textCodec?: string;
// (undocumented)
type: MediaPlaylistType | 'main';
// (undocumented)
unknownCodecs?: string[];
// (undocumented)
url: string;
// (undocumented)
videoCodec?: string;
// (undocumented)
width?: number;
}

// Warning: (ae-missing-release-tag) "MediaPlaylistType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down Expand Up @@ -2973,6 +3042,8 @@ export interface PlaylistLoaderContext extends LoaderContext {
// (undocumented)
levelDetails?: LevelDetails;
// (undocumented)
pathwayId?: string;
// (undocumented)
type: PlaylistContextType;
}

Expand Down
2 changes: 1 addition & 1 deletion build-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const buildConstants = (type, additional = {}) => ({
__USE_EME_DRM__: JSON.stringify(type === BUILD_TYPE.full || addEMESupport),
__USE_CMCD__: JSON.stringify(type === BUILD_TYPE.full || addCMCDSupport),
__USE_CONTENT_STEERING__: JSON.stringify(
type === BUILD_TYPE.full || addContentSteeringSupport,
type === BUILD_TYPE.full || BUILD_TYPE.light || addContentSteeringSupport,
),
__USE_VARIABLE_SUBSTITUTION__: JSON.stringify(
type === BUILD_TYPE.full || addVariableSubstitutionSupport,
Expand Down
4 changes: 2 additions & 2 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ export default class BaseStreamController
}

protected onHandlerDestroying() {
this.hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
this.stopLoad();
super.onHandlerDestroying();
}
Expand Down Expand Up @@ -548,8 +549,7 @@ export default class BaseStreamController
!frag ||
!fragCurrent ||
frag.level !== fragCurrent.level ||
frag.sn !== fragCurrent.sn ||
frag.urlId !== fragCurrent.urlId
frag.sn !== fragCurrent.sn
);
}

Expand Down
108 changes: 84 additions & 24 deletions src/controller/content-steering-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import type {
ManifestParsedData,
} from '../types/events';
import type { RetryConfig } from '../config';
import type {
Loader,
LoaderCallbacks,
LoaderConfiguration,
LoaderContext,
LoaderResponse,
LoaderStats,
import {
PlaylistContextType,
type Loader,
type LoaderCallbacks,
type LoaderConfiguration,
type LoaderContext,
type LoaderResponse,
type LoaderStats,
} from '../types/loader';
import type { LevelParsed } from '../types/level';
import type { MediaAttributes, MediaPlaylist } from '../types/media-playlist';
Expand Down Expand Up @@ -94,14 +95,13 @@ export default class ContentSteeringController implements NetworkComponentAPI {
this.clearTimeout();
if (this.enabled && this.uri) {
if (this.updated) {
const ttl = Math.max(
this.timeToLoad * 1000 - (performance.now() - this.updated),
0,
);
this.scheduleRefresh(this.uri, ttl);
} else {
this.loadSteeringManifest(this.uri);
const ttl = this.timeToLoad * 1000 - (performance.now() - this.updated);
if (ttl > 0) {
this.scheduleRefresh(this.uri, ttl);
return;
}
}
this.loadSteeringManifest(this.uri);
}
}

Expand Down Expand Up @@ -175,14 +175,23 @@ export default class ContentSteeringController implements NetworkComponentAPI {
errorAction?.action === NetworkErrorAction.SendAlternateToPenaltyBox &&
errorAction.flags === ErrorActionFlags.MoveAllAlternatesMatchingHost
) {
const levels = this.levels;
let pathwayPriority = this.pathwayPriority;
const pathwayId = this.pathwayId;
if (!this.penalizedPathways[pathwayId]) {
this.penalizedPathways[pathwayId] = performance.now();
let errorPathway = this.pathwayId;
if (data.context) {
const { groupId, pathwayId, type } = data.context;
if (groupId && levels) {
errorPathway = this.getPathwayForGroupId(groupId, type, errorPathway);
} else if (pathwayId) {
errorPathway = pathwayId;
}
}
if (!pathwayPriority && this.levels) {
if (!(errorPathway in this.penalizedPathways)) {
this.penalizedPathways[errorPathway] = performance.now();
}
if (!pathwayPriority && levels) {
// If PATHWAY-PRIORITY was not provided, list pathways for error handling
pathwayPriority = this.levels.reduce((pathways, level) => {
pathwayPriority = levels.reduce((pathways, level) => {
if (pathways.indexOf(level.pathwayId) === -1) {
pathways.push(level.pathwayId);
}
Expand All @@ -191,7 +200,18 @@ export default class ContentSteeringController implements NetworkComponentAPI {
}
if (pathwayPriority && pathwayPriority.length > 1) {
this.updatePathwayPriority(pathwayPriority);
errorAction.resolved = this.pathwayId !== pathwayId;
errorAction.resolved = this.pathwayId !== errorPathway;
}
if (!errorAction.resolved) {
logger.warn(
`Could not resolve ${data.details} ("${
data.error.message
}") with content-steering for Pathway: ${errorPathway} levels: ${
levels ? levels.length : levels
} priorities: ${JSON.stringify(
pathwayPriority,
)} penalized: ${JSON.stringify(this.penalizedPathways)}`,
);
}
}
}
Expand Down Expand Up @@ -238,7 +258,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
});
for (let i = 0; i < pathwayPriority.length; i++) {
const pathwayId = pathwayPriority[i];
if (penalizedPathways[pathwayId]) {
if (pathwayId in penalizedPathways) {
continue;
}
if (pathwayId === this.pathwayId) {
Expand Down Expand Up @@ -271,6 +291,27 @@ export default class ContentSteeringController implements NetworkComponentAPI {
}
}

private getPathwayForGroupId(
groupId: string,
type: PlaylistContextType,
defaultPathway: string,
): string {
const levels = this.getLevelsForPathway(defaultPathway).concat(
this.levels || [],
);
for (let i = 0; i < levels.length; i++) {
if (
(type === PlaylistContextType.AUDIO_TRACK &&
levels[i].hasAudioGroup(groupId)) ||
(type === PlaylistContextType.SUBTITLE_TRACK &&
levels[i].hasSubtitleGroup(groupId))
) {
return levels[i].pathwayId;
}
}
return defaultPathway;
}

private clonePathways(pathwayClones: PathwayClone[]) {
const levels = this.levels;
if (!levels) {
Expand Down Expand Up @@ -313,8 +354,22 @@ export default class ContentSteeringController implements NetworkComponentAPI {
}
levelParsed.attrs = attributes;
const clonedLevel = new Level(levelParsed);
clonedLevel.addGroupId('audio', clonedAudioGroupId, -1);
clonedLevel.addGroupId('text', clonedSubtitleGroupId, -1);
if (baseLevel.audioGroups) {
for (let i = 1; i < baseLevel.audioGroups.length; i++) {
clonedLevel.addGroupId(
'audio',
`${baseLevel.audioGroups[i]}_clone_${cloneId}`,
);
}
}
if (baseLevel.subtitleGroups) {
for (let i = 1; i < baseLevel.subtitleGroups.length; i++) {
clonedLevel.addGroupId(
'text',
`${baseLevel.subtitleGroups[i]}_clone_${cloneId}`,
);
}
}
return clonedLevel;
},
);
Expand Down Expand Up @@ -466,7 +521,12 @@ export default class ContentSteeringController implements NetworkComponentAPI {
private scheduleRefresh(uri: string, ttlMs: number = this.timeToLoad * 1000) {
this.clearTimeout();
this.reloadTimer = self.setTimeout(() => {
this.loadSteeringManifest(uri);
const media = this.hls?.media;
if (media && !media.ended) {
this.loadSteeringManifest(uri);
return;
}
this.scheduleRefresh(uri, this.timeToLoad * 1000);
}, ttlMs);
}
}
Expand Down