Skip to content

Commit

Permalink
feat: Content Steering HLS Pathway Cloning (#1432)
Browse files Browse the repository at this point in the history
  • Loading branch information
wseymour15 authored Nov 7, 2023
1 parent 532aa4d commit 731058b
Show file tree
Hide file tree
Showing 6 changed files with 1,017 additions and 5 deletions.
23 changes: 20 additions & 3 deletions src/content-steering-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import videojs from 'video.js';
* TTL: number in seconds (optional) until the next content steering manifest reload.
* RELOAD-URI: string (optional) uri to fetch the next content steering manifest.
* SERVICE-LOCATION-PRIORITY or PATHWAY-PRIORITY a non empty array of unique string values.
* PATHWAY-CLONES: array (optional) (HLS only) pathway clone objects to copy from other playlists.
*/
class SteeringManifest {
constructor() {
this.priority_ = [];
this.pathwayClones_ = new Map();
}

set version(number) {
Expand Down Expand Up @@ -43,6 +45,13 @@ class SteeringManifest {
}
}

set pathwayClones(array) {
// pathwayClones must be non-empty.
if (array && array.length) {
this.pathwayClones_ = new Map(array.map((clone) => [clone.ID, clone]));
}
}

get version() {
return this.version_;
}
Expand All @@ -58,6 +67,10 @@ class SteeringManifest {
get priority() {
return this.priority_;
}

get pathwayClones() {
return this.pathwayClones_;
}
}

/**
Expand All @@ -77,12 +90,13 @@ export default class ContentSteeringController extends videojs.EventTarget {
this.defaultPathway = null;
this.queryBeforeStart = false;
this.availablePathways_ = new Set();
this.excludedPathways_ = new Set();
this.steeringManifest = new SteeringManifest();
this.proxyServerUrl_ = null;
this.manifestType_ = null;
this.ttlTimeout_ = null;
this.request_ = null;
this.currentPathwayClones = new Map();
this.nextPathwayClones = new Map();
this.excludedSteeringManifestURLs = new Set();
this.logger_ = logger('Content Steering');
this.xhr_ = xhr;
Expand Down Expand Up @@ -265,7 +279,11 @@ export default class ContentSteeringController extends videojs.EventTarget {
this.steeringManifest.reloadUri = steeringJson['RELOAD-URI'];
// HLS = PATHWAY-PRIORITY required. DASH = SERVICE-LOCATION-PRIORITY optional
this.steeringManifest.priority = steeringJson['PATHWAY-PRIORITY'] || steeringJson['SERVICE-LOCATION-PRIORITY'];
// TODO: HLS handle PATHWAY-CLONES. See section 7.2 https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/

// Pathway clones to be created/updated in HLS.
// See section 7.2 https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/
this.steeringManifest.pathwayClones = steeringJson['PATHWAY-CLONES'];
this.nextPathwayClones = this.steeringManifest.pathwayClones;

// 1. apply first pathway from the array.
// 2. if first pathway doesn't exist in manifest, try next pathway.
Expand Down Expand Up @@ -393,7 +411,6 @@ export default class ContentSteeringController extends videojs.EventTarget {
this.request_ = null;
this.excludedSteeringManifestURLs = new Set();
this.availablePathways_ = new Set();
this.excludedPathways_ = new Set();
this.steeringManifest = new SteeringManifest();
}

Expand Down
2 changes: 1 addition & 1 deletion src/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const createPlaylistID = (index, uri) => {
};

// default function for creating a group id
const groupID = (type, group, label) => {
export const groupID = (type, group, label) => {
return `placeholder-uri-${type}-${group}-${label}`;
};

Expand Down
103 changes: 103 additions & 0 deletions src/playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2197,6 +2197,9 @@ export class PlaylistController extends videojs.EventTarget {
if (!currentPathway) {
return;
}

this.handlePathwayClones_();

const main = this.main();
const playlists = main.playlists;
const ids = new Set();
Expand Down Expand Up @@ -2246,6 +2249,106 @@ export class PlaylistController extends videojs.EventTarget {
}
}

/**
* Add, update, or delete playlists and media groups for
* the pathway clones for HLS Content Steering.
*
* See https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/
*
* NOTE: Pathway cloning does not currently support the `PER_VARIANT_URIS` and
* `PER_RENDITION_URIS` as we do not handle `STABLE-VARIANT-ID` or
* `STABLE-RENDITION-ID` values.
*/
handlePathwayClones_() {
const main = this.main();
const playlists = main.playlists;
const currentPathwayClones = this.contentSteeringController_.currentPathwayClones;
const nextPathwayClones = this.contentSteeringController_.nextPathwayClones;

const hasClones = (currentPathwayClones && currentPathwayClones.size) || (nextPathwayClones && nextPathwayClones.size);

if (!hasClones) {
return;
}

for (const [id, clone] of currentPathwayClones.entries()) {
const newClone = nextPathwayClones.get(id);

// Delete the old pathway clone.
if (!newClone) {
this.mainPlaylistLoader_.updateOrDeleteClone(clone);
this.contentSteeringController_.excludePathway(id);
}
}

for (const [id, clone] of nextPathwayClones.entries()) {
const oldClone = currentPathwayClones.get(id);

// Create a new pathway if it is a new pathway clone object.
if (!oldClone) {
const playlistsToClone = playlists.filter(p => {
return p.attributes['PATHWAY-ID'] === clone['BASE-ID'];
});

playlistsToClone.forEach((p) => {
this.mainPlaylistLoader_.addClonePathway(clone, p);
});

this.contentSteeringController_.addAvailablePathway(id);
continue;
}

// There have not been changes to the pathway clone object, so skip.
if (this.equalPathwayClones_(oldClone, clone)) {
continue;
}

// Update a preexisting cloned pathway.
// True is set for the update flag.
this.mainPlaylistLoader_.updateOrDeleteClone(clone, true);
this.contentSteeringController_.addAvailablePathway(id);
}

// Deep copy contents of next to current pathways.
this.contentSteeringController_.currentPathwayClones = new Map(JSON.parse(JSON.stringify([...nextPathwayClones])));
}

/**
* Determines whether two pathway clone objects are equivalent.
*
* @param {Object} a The first pathway clone object.
* @param {Object} b The second pathway clone object.
* @return {boolean} True if the pathway clone objects are equal, false otherwise.
*/
equalPathwayClones_(a, b) {
if (
a['BASE-ID'] !== b['BASE-ID'] ||
a.ID !== b.ID ||
a['URI-REPLACEMENT'].HOST !== b['URI-REPLACEMENT'].HOST
) {
return false;
}

const aParams = a['URI-REPLACEMENT'].PARAMS;
const bParams = b['URI-REPLACEMENT'].PARAMS;

// We need to iterate through both lists of params because one could be
// missing a parameter that the other has.
for (const p in aParams) {
if (aParams[p] !== bParams[p]) {
return false;
}
}

for (const p in bParams) {
if (aParams[p] !== bParams[p]) {
return false;
}
}

return true;
}

/**
* Changes the current playlists for audio, video and subtitles after a new pathway
* is chosen from content steering.
Expand Down
Loading

0 comments on commit 731058b

Please sign in to comment.