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 switching #1427

Merged
merged 24 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
09276dd
feat: add steering switching
adrums86 Aug 31, 2023
1e6583b
switch audio and text playlists on switch for DASH
wseymour15 Aug 31, 2023
f2278e0
steering request error handling
wseymour15 Sep 1, 2023
6f9af6a
queryBeforeStart support and switching logic improvement
wseymour15 Sep 5, 2023
8bab119
remove demo html
adrums86 Sep 5, 2023
1805722
fix DASH playlist switching
adrums86 Sep 6, 2023
7c17c32
live dash content steering support
wseymour15 Sep 7, 2023
980cfd8
slight improvement to queryBeforeStart
wseymour15 Sep 7, 2023
418c703
finish playlist exclusion logic
adrums86 Sep 7, 2023
e42bd9c
fix tests
adrums86 Sep 7, 2023
b1337e8
minor fixes and tests
wseymour15 Sep 7, 2023
33bbeb7
minor improvements to queryBeforeStart
wseymour15 Sep 7, 2023
813f8dd
add DASH specific logic back to exclude function
adrums86 Sep 7, 2023
e2927ce
tests for live content steering
wseymour15 Sep 11, 2023
2ba74c5
playlist controller tests
wseymour15 Sep 12, 2023
50f9a48
tests for content steering controller
wseymour15 Sep 13, 2023
d3d5f60
minor test updates
wseymour15 Sep 13, 2023
65f1016
remove stopLoader from subtitle onError
wseymour15 Sep 14, 2023
e882de5
improvements based on comments
wseymour15 Sep 18, 2023
9465337
update requestSteeringManifest to use uri
wseymour15 Sep 18, 2023
5604ea5
minor changes based on comments
wseymour15 Sep 19, 2023
c9d470d
remove unecessary error property
wseymour15 Sep 19, 2023
5ee0b01
use bandwidth for throughput param
adrums86 Sep 21, 2023
72af63a
use xhr and bandwidth parameters
adrums86 Sep 22, 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
161 changes: 130 additions & 31 deletions src/content-steering-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ export default class ContentSteeringController extends videojs.EventTarget {
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.excludedSteeringManifestURLs = new Set();
this.mainSegmentLoader_ = segmentLoader;
this.logger_ = logger('Content Steering');
}
Expand All @@ -109,57 +109,93 @@ export default class ContentSteeringController extends videojs.EventTarget {
this.decodeDataUriManifest_(steeringUri.substring(steeringUri.indexOf(',') + 1));
return;
}
this.steeringManifest.reloadUri = resolveUrl(baseUrl, steeringUri);
// With DASH queryBeforeStart, we want to use the steeringUri as soon as possible for the request.
this.steeringManifest.reloadUri = this.queryBeforeStart ? steeringUri : 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.
dzianis-dashkevich marked this conversation as resolved.
Show resolved Hide resolved
if (this.manifestType_ === 'DASH') {
this.queryBeforeStart = steeringTag.queryBeforeStart || false;
this.proxyServerUrl_ = steeringTag.proxyServerURL;
}
this.queryBeforeStart = steeringTag.queryBeforeStart || false;
this.proxyServerUrl_ = steeringTag.proxyServerURL || null;

// 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) {
// If queryBeforeStart is true we need to wait for the steering manifest response.
if (this.defaultPathway && !this.queryBeforeStart) {
this.trigger('content-steering');
}

if (this.queryBeforeStart) {
this.requestSteeringManifest(this.steeringManifest.reloadUri);
}
}

/**
* Requests the content steering manifest and parse the response. This should only be called after
* assignTagProperties was called with a content steering tag.
*
* @param {string} initialUri The optional uri to make the request with.
* If set, the request should be made with exactly what is passed in this variable.
* This scenario is specific to DASH when the queryBeforeStart parameter is true.
* This scenario should only happen once on initalization.
*/
requestSteeringManifest() {
// add parameters to the steering uri
requestSteeringManifest(initialUri) {
const reloadUri = this.steeringManifest.reloadUri;

if (!initialUri && !reloadUri) {
return;
}

// We currently don't support passing MPD query parameters directly to the content steering URL as this requires
// ExtUrlQueryInfo tag support. See the DASH content steering spec section 8.1.
const uri = this.proxyServerUrl_ ? this.setProxyServerUrl_(reloadUri) : this.setSteeringParams_(reloadUri);

// This request URI accounts for manifest URIs that have been excluded.
const uri = initialUri || this.getRequestURI(reloadUri);

// If there are no valid manifest URIs, we should stop content steering.
if (!uri) {
this.logger_('No valid content steering manifest URIs. Stopping content steering.');
this.trigger('error');
dzianis-dashkevich marked this conversation as resolved.
Show resolved Hide resolved
this.dispose();
return;
}

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.
}, (error, errorInfo) => {
if (error) {
// TODO: HLS RETRY CASE:
// 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 (errorInfo.status === 410) {
this.logger_(`manifest request 410 ${error}.`);
this.logger_(`There will be no more content steering requests to ${uri} this session.`);

this.excludedSteeringManifestURLs.add(uri);
return;
}
// 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 (errorInfo.status === 429) {
const retrySeconds = errorInfo.responseHeaders['retry-after'];

this.logger_(`manifest request 429 ${error}.`);
this.logger_(`content steering will retry in ${retrySeconds} seconds.`);
this.startTTLTimeout_(parseInt(retrySeconds, 10));
return;
}
// 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');
this.startTTLTimeout_();
return;
}
const steeringManifestJson = JSON.parse(this.request_.responseText);

this.startTTLTimeout_();
this.assignSteeringProperties_(steeringManifestJson);
});
}
Expand Down Expand Up @@ -234,44 +270,91 @@ export default class ContentSteeringController extends videojs.EventTarget {
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.
// 2. if 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) {
// See excludePathway usage in excludePlaylist().

// If there are no available pathways, we need to stop content steering.
if (!this.availablePathways_.size) {
this.logger_('There are no available pathways for content steering. Ending content steering.');
this.trigger('error');
this.dispose();
}

const chooseNextPathway = (pathwaysByPriority) => {
for (const path of pathwaysByPriority) {
if (this.availablePathways_.has(path)) {
return path;
}
}

// If no pathway matches, ignore the manifest and choose the first available.
return [...this.availablePathways_][0];
dzianis-dashkevich marked this conversation as resolved.
Show resolved Hide resolved
};

const nextPathway = chooseNextPathway(this.steeringManifest.priority);

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

/**
* Returns the pathway to use for steering decisions
*
* @return returns the current pathway or the default
* @return {string} returns the current pathway or the default
*/
getPathway() {
return this.currentPathway || this.defaultPathway;
}

/**
* Chooses the manifest request URI based on proxy URIs and server URLs.
* Also accounts for exclusion on certain manifest URIs.
*
* @param {string} reloadUri the base uri before parameters
*
* @return {string} the final URI for the request to the manifest server.
*/
getRequestURI(reloadUri) {
if (!reloadUri) {
return null;
}

const isExcluded = (uri) => this.excludedSteeringManifestURLs.has(uri);

if (this.proxyServerUrl_) {
const proxyURI = this.setProxyServerUrl_(reloadUri);

if (!isExcluded(proxyURI)) {
return proxyURI;
}
}

const steeringURI = this.setSteeringParams_(reloadUri);

if (!isExcluded(steeringURI)) {
return steeringURI;
}

// Return nothing if all valid manifest URIs are excluded.
return null;
}

/**
* Start the timeout for re-requesting the steering manifest at the TTL interval.
*
* @param {number} ttl time in seconds of the timeout. Defaults to the
* ttl interval in the steering manifest
*/
startTTLTimeout_() {
startTTLTimeout_(ttl = this.steeringManifest.ttl) {
// 300 (5 minutes) is the default value.
const ttlMS = this.steeringManifest.ttl * 1000;
const ttlMS = ttl * 1000;

this.ttlTimeout_ = window.setTimeout(() => {
this.requestSteeringManifest();
Expand Down Expand Up @@ -300,6 +383,8 @@ export default class ContentSteeringController extends videojs.EventTarget {
* aborts steering requests clears the ttl timeout and resets all properties.
*/
dispose() {
this.off('content-steering');
this.off('error');
this.abort();
this.clearTTLTimeout_();
this.currentPathway = null;
Expand All @@ -309,6 +394,7 @@ export default class ContentSteeringController extends videojs.EventTarget {
this.manifestType_ = null;
this.ttlTimeout_ = null;
this.request_ = null;
this.excludedSteeringManifestURLs = new Set();
this.availablePathways_ = new Set();
this.excludedPathways_ = new Set();
this.steeringManifest = new SteeringManifest();
Expand All @@ -320,6 +406,19 @@ export default class ContentSteeringController extends videojs.EventTarget {
* @param {string} pathway the pathway string to add
*/
addAvailablePathway(pathway) {
this.availablePathways_.add(pathway);
if (pathway) {
this.availablePathways_.add(pathway);
}
}

/**
* clears all pathways from the available pathways set
*/
clearAvailablePathways() {
this.availablePathways_.clear();
}

excludePathway(pathway) {
return this.availablePathways_.delete(pathway);
}
}
5 changes: 4 additions & 1 deletion src/dash-playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,10 @@ export default class DashPlaylistLoader extends EventTarget {

// live playlist staleness timeout
this.on('mediaupdatetimeout', () => {
this.refreshMedia_(this.media().id);
// We handle live content steering in the playlist controller
if (!this.media().attributes.serviceLocation) {
this.refreshMedia_(this.media().id);
dzianis-dashkevich marked this conversation as resolved.
Show resolved Hide resolved
}
});

this.state = 'HAVE_NOTHING';
Expand Down
6 changes: 0 additions & 6 deletions src/media-groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,10 @@ export const onError = {
*/
AUDIO: (type, settings) => () => {
const {
segmentLoaders: { [type]: segmentLoader},
mediaTypes: { [type]: mediaType },
excludePlaylist
} = settings;

stopLoaders(segmentLoader, mediaType);
dzianis-dashkevich marked this conversation as resolved.
Show resolved Hide resolved

// switch back to default audio track
const activeTrack = mediaType.activeTrack();
const activeGroup = mediaType.activeGroup();
Expand Down Expand Up @@ -295,15 +292,12 @@ export const onError = {
*/
SUBTITLES: (type, settings) => () => {
const {
segmentLoaders: { [type]: segmentLoader},
mediaTypes: { [type]: mediaType }
} = settings;

videojs.log.warn('Problem encountered loading the subtitle track.' +
'Disabling subtitle track.');

stopLoaders(segmentLoader, mediaType);

const track = mediaType.activeTrack();

if (track) {
Expand Down
Loading
Loading