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: request Content Steering manifest #1419

Merged
merged 36 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ebaf833
feat: request Content Steering manifest
adrums86 Aug 16, 2023
9fb4255
fix abort request
adrums86 Aug 16, 2023
7c5c0dd
Merge branch 'main' into feat-ContentSteering
adrums86 Aug 16, 2023
b468f30
support query params and data uri
adrums86 Aug 18, 2023
f54cc95
fix dispose call
adrums86 Aug 18, 2023
88c4fc4
add some hls specific tests
adrums86 Aug 22, 2023
ca424b2
add dash and common tests
adrums86 Aug 23, 2023
f35e804
add event, queryBeforeStart, and fix tests
adrums86 Aug 23, 2023
cdebac1
move event
adrums86 Aug 23, 2023
11201ed
refactor everything
adrums86 Aug 24, 2023
eb5cac0
use set
adrums86 Aug 24, 2023
723e10a
round throughput param
adrums86 Aug 24, 2023
f197139
remove steering tag reference, fix comments
adrums86 Aug 24, 2023
22119da
fixes, refactors, and generic exclusion and switch
adrums86 Aug 25, 2023
562d6e2
refactor
adrums86 Aug 25, 2023
ecdc684
refactor, basic priority logic, queryBeforeStart
adrums86 Aug 26, 2023
2f4a78b
fix first couple tests rename files
adrums86 Aug 28, 2023
fa5f7dc
fix space
adrums86 Aug 28, 2023
bebb5f1
fix queryBeforeStart in pbc
adrums86 Aug 28, 2023
163ed88
remove early steering request
adrums86 Aug 28, 2023
4bf1679
rename to getPathway
adrums86 Aug 28, 2023
b13dd0a
allow undefined nextPathway
adrums86 Aug 28, 2023
fb1e128
demo page additions
adrums86 Aug 28, 2023
24c097c
account for serviceLocation
adrums86 Aug 28, 2023
e177d11
base HLS steering tests
adrums86 Aug 29, 2023
23d6fc6
dash and common tests, rename
adrums86 Aug 30, 2023
8f757d8
fix comments and tests
adrums86 Aug 30, 2023
21762d4
more tests
adrums86 Aug 30, 2023
d0c38b9
remove only fix function call
adrums86 Aug 30, 2023
eae53bd
private available pathways
adrums86 Aug 30, 2023
18e8544
remove pbc work
adrums86 Aug 30, 2023
def4132
one more remove
adrums86 Aug 30, 2023
b3fdddd
remove demo page components
adrums86 Aug 30, 2023
3f8660e
one more removal
adrums86 Aug 30, 2023
a42adfa
comments
adrums86 Aug 30, 2023
1326a15
more comments
adrums86 Aug 30, 2023
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
317 changes: 317 additions & 0 deletions src/content-steering-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
import resolveUrl from './resolve-url';
import window from 'global/window';
import logger from './util/logger';
import videojs from 'video.js';

/**
* A utility class for setting properties and maintaining the state of the content steering manifest.
*
* Content Steering manifest format:
* VERSION: number (required) currently only version 1 is supported.
* 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.
*/
class SteeringManifest {
constructor() {
this.priority_ = [];
}

set version(number) {
// Only version 1 is currently supported for both DASH and HLS.
if (number === 1) {
this.version_ = number;
}
}

set ttl(seconds) {
// TTL = time-to-live, default = 300 seconds.
this.ttl_ = seconds || 300;
}

set reloadUri(uri) {
if (uri) {
// reload URI can be relative to the previous reloadUri.
this.reloadUri_ = resolveUrl(this.reloadUri_, uri);
}
}

set priority(array) {
// priority must be non-empty and unique values.
if (array && array.length) {
this.priority_ = array;
}
}

get version() {
return this.version_;
}

get ttl() {
return this.ttl_;
}

get reloadUri() {
return this.reloadUri_;
}

get priority() {
return this.priority_;
}
}

/**
* This class represents a content steering manifest and associated state. See both HLS and DASH specifications.
* HLS: https://developer.apple.com/streaming/HLSContentSteeringSpecification.pdf and
* https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ section 4.4.6.6.
* DASH: https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf
*
* @param {Object} segmentLoader a reference to the mainSegmentLoader
*/
export default class ContentSteeringController extends videojs.EventTarget {
// pass a segment loader reference for throughput rate and xhr
constructor(segmentLoader) {
super();

this.currentPathway = null;
this.defaultPathway = null;
this.queryBeforeStart = null;
this.availablePathways_ = new Set();
// TODO: Implement exclusion.
this.excludedPathways_ = new Set();
this.steeringManifest = new SteeringManifest();
this.proxyServerUrl_ = null;
this.manifestType_ = null;
this.ttlTimeout_ = null;
this.request_ = null;
this.mainSegmentLoader_ = segmentLoader;
this.logger_ = logger('Content Steering');
}

/**
* Assigns the content steering tag properties to the steering controller
*
* @param {string} baseUrl the baseURL from the manifest for resolving the steering manifest url
* @param {Object} steeringTag the content steering tag from the main manifest
*/
assignTagProperties(baseUrl, steeringTag) {
this.manifestType_ = steeringTag.serverUri ? 'HLS' : 'DASH';
// serverUri is HLS serverURL is DASH
const steeringUri = steeringTag.serverUri || steeringTag.serverURL;

if (!steeringUri) {
this.logger_(`steering manifest URL is ${steeringUri}, cannot request steering manifest.`);
this.trigger('error');
return;
}
// Content steering manifests can be encoded as a data URI. We can decode, parse and return early if that's the case.
if (steeringUri.startsWith('data:')) {
this.decodeDataUriManifest_(steeringUri.substring(steeringUri.indexOf(',') + 1));
return;
}
this.steeringManifest.reloadUri = resolveUrl(baseUrl, steeringUri);
// pathwayId is HLS defaultServiceLocation is DASH
this.defaultPathway = steeringTag.pathwayId || steeringTag.defaultServiceLocation;
// currently only DASH supports the following properties on <ContentSteering> tags.
if (this.manifestType_ === 'DASH') {
this.queryBeforeStart = steeringTag.queryBeforeStart || false;
this.proxyServerUrl_ = steeringTag.proxyServerURL;
}

// trigger a steering event if we have a pathway from the content steering tag.
// this tells VHS which segment pathway to start with.
if (this.defaultPathway) {
this.trigger('content-steering');
}
}

/**
* Requests the content steering manifest and parse the response.
*/
requestSteeringManifest() {
// add parameters to the steering uri
const reloadUri = this.steeringManifest.reloadUri;
const uri = this.proxyServerUrl_ ? this.setProxyServerUrl_(reloadUri) : this.setSteeringParams_(reloadUri);
wseymour15 marked this conversation as resolved.
Show resolved Hide resolved

this.request_ = this.mainSegmentLoader_.vhs_.xhr({
uri
}, (error) => {
// TODO: HLS CASES THAT NEED ADDRESSED:
// If the client receives HTTP 410 Gone in response to a manifest request,
// it MUST NOT issue another request for that URI for the remainder of the
// playback session. It MAY continue to use the most-recently obtained set
// of Pathways.
// If the client receives HTTP 429 Too Many Requests with a Retry-After
// header in response to a manifest request, it SHOULD wait until the time
// specified by the Retry-After header to reissue the request.
if (error) {
// TODO: HLS RETRY CASE:
// If the Steering Manifest cannot be loaded and parsed correctly, the
// client SHOULD continue to use the previous values and attempt to reload
// it after waiting for the previously-specified TTL (or 5 minutes if
// none).
this.logger_(`manifest failed to load ${error}.`);
// TODO: we may want to expose the error object here.
this.trigger('error');
return;
}
const steeringManifestJson = JSON.parse(this.request_.responseText);

this.assignSteeringProperties_(steeringManifestJson);
});
}

/**
* Set the proxy server URL and add the steering manifest url as a URI encoded parameter.
*
* @param {string} steeringUrl the steering manifest url
* @return the steering manifest url to a proxy server with all parameters set
*/
setProxyServerUrl_(steeringUrl) {
const steeringUrlObject = new window.URL(steeringUrl);
const proxyServerUrlObject = new window.URL(this.proxyServerUrl_);

proxyServerUrlObject.searchParams.set('url', encodeURI(steeringUrlObject.toString()));
return this.setSteeringParams_(proxyServerUrlObject.toString());
}

/**
* Decodes and parses the data uri encoded steering manifest
*
* @param {string} dataUri the data uri to be decoded and parsed.
*/
decodeDataUriManifest_(dataUri) {
const steeringManifestJson = JSON.parse(window.atob(dataUri));

this.assignSteeringProperties_(steeringManifestJson);
}

/**
* Set the HLS or DASH content steering manifest request query parameters. For example:
* _HLS_pathway="<CURRENT-PATHWAY-ID>" and _HLS_throughput=<THROUGHPUT>
* _DASH_pathway and _DASH_throughput
*
* @param {string} uri to add content steering server parameters to.
* @return a new uri as a string with the added steering query parameters.
*/
setSteeringParams_(url) {
const urlObject = new window.URL(url);
const path = this.getPathway();

if (path) {
const pathwayKey = `_${this.manifestType_}_pathway`;

urlObject.searchParams.set(pathwayKey, path);
}

if (this.mainSegmentLoader_.throughput.rate) {
const throughputKey = `_${this.manifestType_}_throughput`;
const rateInteger = Math.round(this.mainSegmentLoader_.throughput.rate);

urlObject.searchParams.set(throughputKey, rateInteger);
}
return urlObject.toString();
}

/**
* Assigns the current steering manifest properties and to the ContentSteering class.
*
* @param {Object} steeringJson the raw JSON steering manifest
*/
assignSteeringProperties_(steeringJson) {
this.steeringManifest.version = steeringJson.VERSION;
if (!this.steeringManifest.version) {
this.logger_(`manifest version is ${steeringJson.VERSION}, which is not supported.`);
this.trigger('error');
return;
}
this.steeringManifest.ttl = steeringJson.TTL;
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/

// TODO: fully implement priority logic.
// 1. apply first pathway from the array.
// 2. if first first pathway doesn't exist in manifest, try next pathway.
// a. if all pathways are exhausted, ignore the steering manifest priority.
// 3. if segments fail from an established pathway, try all variants/renditions, then exclude the failed pathway.
// a. exclude a pathway for a minimum of the last TTL duration. Meaning, from the next steering response,
// the excluded pathway will be ignored.
const chooseNextPathway = (pathways) => {
for (const path of pathways) {
if (this.availablePathways_.has(path)) {
return path;
}
}
};
const nextPathway = chooseNextPathway(this.steeringManifest.priority);

if (this.currentPathway !== nextPathway) {
this.currentPathway = nextPathway;
this.trigger('content-steering');
}
this.startTTLTimeout_();
}

getPathway() {
return this.currentPathway || this.defaultPathway;
}

/**
* Start the timeout for re-requesting the steering manifest at the TTL interval.
*/
startTTLTimeout_() {
// 300 (5 minutes) is the default value.
const ttlMS = this.steeringManifest.ttl * 1000;

this.ttlTimeout_ = window.setTimeout(() => {
this.requestSteeringManifest();

Check warning on line 269 in src/content-steering-controller.js

View check run for this annotation

Codecov / codecov/patch

src/content-steering-controller.js#L269

Added line #L269 was not covered by tests
}, ttlMS);
}

/**
* Clear the TTL timeout if necessary.
*/
clearTTLTimeout_() {
window.clearTimeout(this.ttlTimeout_);
this.ttlTimeout_ = null;
}

/**
* aborts any current steering xhr and sets the current request object to null
*/
abort() {
if (this.request_) {
this.request_.abort();
}
this.request_ = null;
}

/**
* aborts steering requests clears the ttl timeout and resets all properties.
*/
dispose() {
this.abort();
this.clearTTLTimeout_();
this.currentPathway = null;
this.defaultPathway = null;
this.queryBeforeStart = null;
this.proxyServerUrl_ = null;
this.manifestType_ = null;
this.ttlTimeout_ = null;
this.request_ = null;
this.availablePathways_ = new Set();
this.excludedPathways_ = new Set();
this.steeringManifest = new SteeringManifest();
}

/**
* adds a pathway to the available pathways set
*
* @param {string} pathway the pathway string to add
*/
addAvailablePathway(pathway) {
this.availablePathways_.add(pathway);
}
}
Loading