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

feat: Content Steering HLS Pathway Cloning #1432

Merged
merged 7 commits into from
Nov 7, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = null;
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 @@ -269,7 +283,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 @@ -397,7 +415,6 @@ export default class ContentSteeringController extends videojs.EventTarget {
this.request_ = null;
this.excludedSteeringManifestURLs = new Set();
this.availablePathways_ = new Set();
this.excludedPathways_ = new Set();
adrums86 marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -2162,6 +2162,9 @@
if (!currentPathway) {
return;
}

this.handlePathwayClones_();

const main = this.main();
const playlists = main.playlists;
const ids = new Set();
Expand Down Expand Up @@ -2211,6 +2214,106 @@
}
}

/**
* 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;

Check warning on line 2304 in src/playlist-controller.js

View check run for this annotation

Codecov / codecov/patch

src/playlist-controller.js#L2304

Added line #L2304 was not covered by tests
}
}

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

Check warning on line 2310 in src/playlist-controller.js

View check run for this annotation

Codecov / codecov/patch

src/playlist-controller.js#L2310

Added line #L2310 was not covered by tests
}
}

return true;
}

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