Skip to content

Commit

Permalink
Content Steering Pathway grouping and selection
Browse files Browse the repository at this point in the history
  • Loading branch information
robwalch committed Jan 27, 2023
1 parent cede43d commit 509d369
Show file tree
Hide file tree
Showing 8 changed files with 520 additions and 165 deletions.
97 changes: 82 additions & 15 deletions src/controller/content-steering-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Events } from '../events';
import { logger } from '../utils/logger';
import type Hls from '../hls';
import type { NetworkComponentAPI } from '../types/component-api';
import type { ManifestLoadedData } from '../types/events';
import type { ManifestLoadedData, ManifestParsedData } from '../types/events';
import type {
Loader,
LoaderCallbacks,
Expand All @@ -11,6 +11,7 @@ import type {
LoaderResponse,
LoaderStats,
} from '../types/loader';
import type { Level } from '../types/level';

type SteeringManifest = {
VERSION: 1;
Expand All @@ -37,10 +38,12 @@ export default class ContentSteeringController implements NetworkComponentAPI {
private loader: Loader<LoaderContext> | null = null;
private uri: string | null = null;
private pathwayId: string = '.';
private pathwayPriority: string[] | null = null;
private timeToLoad: number = 300;
private reloadTimer: number = -1;
private updated: number = 0;
private enabled: boolean = true;
private levels: Level[] | null = null;

constructor(hls: Hls) {
this.hls = hls;
Expand All @@ -56,6 +59,9 @@ export default class ContentSteeringController implements NetworkComponentAPI {

private unregisterListeners() {
const hls = this.hls;
if (!hls) {
return;
}
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
}
Expand All @@ -74,6 +80,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
}
}
}

stopLoad(): void {
if (this.loader) {
this.loader.destroy();
Expand All @@ -86,7 +93,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
this.unregisterListeners();
this.stopLoad();
// @ts-ignore
this.hls = this.config = null;
this.hls = this.config = this.levels = null;
}

private onManifestLoading() {
Expand All @@ -96,6 +103,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
this.updated = 0;
this.uri = null;
this.pathwayId = '.';
this.levels = null;
}

private onManifestLoaded(
Expand All @@ -111,6 +119,54 @@ export default class ContentSteeringController implements NetworkComponentAPI {
this.startLoad();
}

public filterParsedLevels(levels: Level[]): Level[] {
// Filter levels to only include those that are in the initial pathway
this.levels = levels;
let pathwayLevels = this.getLevelsForPathway(this.pathwayId);
if (pathwayLevels.length === 0) {
const pathwayId = levels[0].attrs['PATHWAY-ID'] || '.';
this.log(`Setting initial Pathway to "${pathwayId}"`);
pathwayLevels = this.getLevelsForPathway(pathwayId);
this.pathwayId = pathwayId;
}
if (pathwayLevels.length !== levels.length) {
this.log(
`Found ${pathwayLevels.length} levels in Pathway "${this.pathwayId}"`
);
return pathwayLevels;
}
return levels;
}

private getLevelsForPathway(pathwayId: string): Level[] {
if (this.levels === null) {
return [];
}
return this.levels.filter(
(level) => pathwayId === (level.attrs['PATHWAY-ID'] || '.')
);
}

private updatePathwayPriority(pathwayPriority: string[]) {
this.pathwayPriority = pathwayPriority;
let levels: Level[] | undefined;
for (let i = 0; i < pathwayPriority.length; i++) {
const pathwayId = pathwayPriority[i];
if (pathwayId === this.pathwayId) {
return;
}
levels = this.getLevelsForPathway(pathwayId);
if (levels.length > 0) {
this.log(`Setting Pathway to "${pathwayId}"`);
this.pathwayId = pathwayId;
break;
}
}
if (levels) {
this.hls.trigger(Events.LEVELS_UPDATED, { levels });
}
}

private loadSteeringManifest(uri: string) {
const config = this.hls.config;
const Loader = config.loader;
Expand All @@ -119,7 +175,14 @@ export default class ContentSteeringController implements NetworkComponentAPI {
}
this.loader = new Loader(config) as Loader<LoaderContext>;

const url: URL = new self.URL(uri);
let url: URL;
try {
url = new self.URL(uri);
} catch (error) {
this.enabled = false;
this.log(`Failed to parse Steering Manifest URI: ${uri}`);
return;
}
if (url.protocol !== 'data:') {
const throughput =
(this.hls.bandwidthEstimate || config.abrEwmaDefaultEstimate) | 0;
Expand All @@ -131,7 +194,6 @@ export default class ContentSteeringController implements NetworkComponentAPI {
url: url.href,
};

// TODO: Keys and Steering Manifests do not have their own Network Policy settings
const loaderConfig: LoaderConfiguration = {
timeout: config.levelLoadingTimeOut,
maxRetry: 0,
Expand All @@ -146,31 +208,36 @@ export default class ContentSteeringController implements NetworkComponentAPI {
context: LoaderContext,
networkDetails: any
) => {
this.log(
`Loaded steering manifest: ${url} ${JSON.stringify(
response.data,
null,
2
)}`
);
this.log(`Loaded steering manifest: "${url}"`);
const steeringData = response.data as SteeringManifest;
if (steeringData.VERSION !== 1) {
this.log(`Steering VERSION ${steeringData.VERSION} not supported!`);
return;
}
this.updated = Date.now();
this.timeToLoad = steeringData.TTL;
if (steeringData['RELOAD-URI']) {
this.uri = new URL(steeringData['RELOAD-URI'], url).href;
const reloadUri = steeringData['RELOAD-URI'];
if (reloadUri) {
try {
this.uri = new URL(reloadUri, url).href;
} catch (error) {
this.enabled = false;
this.log(
`Failed to parse Steering Manifest RELOAD-URI: ${reloadUri}`
);
return;
}
}

this.scheduleRefresh(this.uri || context.url);

// TODO: Handle PATHWAY-CLONES (if present)
// PATHWAY-CLONES

// TODO: Handle PATHWAY-PRIORITY
// PATHWAY-PRIORITY: ['My-cali-CDN', 'My-creek-CDN', 'My-dry-CDN']
const pathwayPriority = steeringData['PATHWAY-PRIORITY'];
if (pathwayPriority) {
this.updatePathwayPriority(pathwayPriority);
}
},

onError: (
Expand Down
84 changes: 52 additions & 32 deletions src/controller/level-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
FragLoadedData,
ErrorData,
LevelSwitchingData,
LevelsUpdatedData,
} from '../types/events';
import { HdcpLevel, HdcpLevels, Level } from '../types/level';
import { Events } from '../events';
Expand All @@ -20,6 +21,7 @@ import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
import type Hls from '../hls';
import type { HlsUrlParameters, LevelParsed } from '../types/level';
import type { MediaPlaylist } from '../types/media-playlist';
import ContentSteeringController from './content-steering-controller';

let chromeOrFirefox: boolean;

Expand All @@ -29,18 +31,24 @@ export default class LevelController extends BasePlaylistController {
private _startLevel?: number;
private currentLevelIndex: number = -1;
private manualLevelIndex: number = -1;
private steering: ContentSteeringController | null;

public onParsedComplete!: Function;

constructor(hls: Hls) {
constructor(
hls: Hls,
contentSteeringController: ContentSteeringController | null
) {
super(hls, '[level-controller]');
this.steering = contentSteeringController;
this._registerListeners();
}

private _registerListeners() {
const { hls } = this;
hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
hls.on(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this);
hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
hls.on(Events.ERROR, this.onError, this);
Expand All @@ -50,13 +58,15 @@ export default class LevelController extends BasePlaylistController {
const { hls } = this;
hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
hls.off(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this);
hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
hls.off(Events.ERROR, this.onError, this);
}

public destroy() {
this._unregisterListeners();
this.steering = null;
this.manualLevelIndex = -1;
this._levels.length = 0;
super.destroy();
Expand Down Expand Up @@ -174,7 +184,9 @@ export default class LevelController extends BasePlaylistController {
assignTrackIdsByGroup(subtitleTracks);
}
// start bitrate is the first bitrate of the manifest
const firstLevelInPlaylist = levels[0];
const firstLevelInPlaylist = (
this.steering ? this.steering.filterParsedLevels(levels) : levels
)[0];
// sort levels from lowest to highest
levels.sort((a, b) => {
if (a.attrs['HDCP-LEVEL'] !== b.attrs['HDCP-LEVEL']) {
Expand Down Expand Up @@ -203,6 +215,10 @@ export default class LevelController extends BasePlaylistController {
return 0;
});

if (this.steering) {
levels = this.steering.filterParsedLevels(levels);
}

this._levels = levels;

// find index of first level in sorted levels
Expand Down Expand Up @@ -649,42 +665,46 @@ export default class LevelController extends BasePlaylistController {

removeLevel(levelIndex, urlId) {
const filterLevelAndGroupByIdIndex = (url, id) => id !== urlId;
const levels = this._levels
.filter((level, index) => {
if (index !== levelIndex) {
return true;
}
const levels = this._levels.filter((level, index) => {
if (index !== levelIndex) {
return true;
}

if (level.url.length > 1 && urlId !== undefined) {
level.url = level.url.filter(filterLevelAndGroupByIdIndex);
if (level.audioGroupIds) {
level.audioGroupIds = level.audioGroupIds.filter(
filterLevelAndGroupByIdIndex
);
}
if (level.textGroupIds) {
level.textGroupIds = level.textGroupIds.filter(
filterLevelAndGroupByIdIndex
);
}
level.urlId = 0;
return true;
if (level.url.length > 1 && urlId !== undefined) {
level.url = level.url.filter(filterLevelAndGroupByIdIndex);
if (level.audioGroupIds) {
level.audioGroupIds = level.audioGroupIds.filter(
filterLevelAndGroupByIdIndex
);
}
return false;
})
.map((level, index) => {
const { details } = level;
if (details?.fragments) {
details.fragments.forEach((fragment) => {
fragment.level = index;
});
if (level.textGroupIds) {
level.textGroupIds = level.textGroupIds.filter(
filterLevelAndGroupByIdIndex
);
}
return level;
});
this._levels = levels;
level.urlId = 0;
return true;
}
return false;
});

this.hls.trigger(Events.LEVELS_UPDATED, { levels });
}

private onLevelsUpdated(
event: Events.LEVELS_UPDATED,
{ levels }: LevelsUpdatedData
) {
levels.forEach((level, index) => {
const { details } = level;
if (details?.fragments) {
details.fragments.forEach((fragment) => {
fragment.level = index;
});
}
});
this._levels = levels;
}
}

function addGroupId(level: Level, type: string, id: string | undefined): void {
Expand Down
19 changes: 12 additions & 7 deletions src/hls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,15 @@ export default class Hls implements HlsEventEmitter {
const playListLoader = new PlaylistLoader(this);
const id3TrackController = new ID3TrackController(this);

// network controllers
const levelController = (this.levelController = new LevelController(this));
const ConfigContentSteeringController = config.contentSteeringController;
// ConentSteeringController is defined before LevelController to receive Multivariant Playlist events first
const contentSteeing = ConfigContentSteeringController
? new ConfigContentSteeringController(this)
: null;
const levelController = (this.levelController = new LevelController(
this,
contentSteeing
));
// FragmentTracker must be defined before StreamController because the order of event handling is important
const fragmentTracker = new FragmentTracker(this);
const keyLoader = new KeyLoader(this.config);
Expand All @@ -159,6 +166,9 @@ export default class Hls implements HlsEventEmitter {
levelController,
streamController,
];
if (contentSteeing) {
networkControllers.splice(1, 0, contentSteeing);
}

this.networkControllers = networkControllers;
const coreComponents: ComponentAPI[] = [
Expand Down Expand Up @@ -191,11 +201,6 @@ export default class Hls implements HlsEventEmitter {
new SubtitleStreamControllerClass(this, fragmentTracker, keyLoader)
);
}
const ConfigContentSteeringController = config.contentSteeringController;
if (ConfigContentSteeringController) {
const contentSteeing = new ConfigContentSteeringController(this);
networkControllers.push(contentSteeing);
}
this.createController(config.timelineController, coreComponents);
keyLoader.emeController = this.emeController = this.createController(
config.emeController,
Expand Down
Loading

0 comments on commit 509d369

Please # to comment.