From 86d5327a72357c23c89014c5dcac99e65e6c3072 Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Wed, 30 Aug 2023 14:44:41 -0700 Subject: [PATCH] feat: request Content Steering manifest (#1419) --- src/content-steering-controller.js | 325 ++++++++++++++++++++++ test/content-steering-controller.test.js | 340 +++++++++++++++++++++++ 2 files changed, 665 insertions(+) create mode 100644 src/content-steering-controller.js create mode 100644 test/content-steering-controller.test.js diff --git a/src/content-steering-controller.js b/src/content-steering-controller.js new file mode 100644 index 000000000..64ab48663 --- /dev/null +++ b/src/content-steering-controller.js @@ -0,0 +1,325 @@ +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 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. This should only be called after + * assignTagProperties was called with a content steering tag. + */ + requestSteeringManifest() { + // add parameters to the steering uri + const reloadUri = this.steeringManifest.reloadUri; + // 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_ = 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="" and _HLS_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 SteeringManifest object + * + * @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_(); + } + + /** + * Returns the pathway to use for steering decisions + * + * @return returns the current pathway or the default + */ + 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(); + }, 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); + } +} diff --git a/test/content-steering-controller.test.js b/test/content-steering-controller.test.js new file mode 100644 index 000000000..e9db50f34 --- /dev/null +++ b/test/content-steering-controller.test.js @@ -0,0 +1,340 @@ +import QUnit from 'qunit'; +import ContentSteeringController from '../src/content-steering-controller'; +import { useFakeEnvironment } from './test-helpers'; +import xhrFactory from '../src/xhr'; + +QUnit.module('ContentSteering', { + beforeEach(assert) { + this.env = useFakeEnvironment(assert); + this.requests = this.env.requests; + this.fakeVhs = { + xhr: xhrFactory() + }; + this.mockSegmentLoader = { + vhs_: this.fakeVhs, + throughput: { + rate: 0 + } + }; + this.baseURL = 'https://foo.bar'; + this.contentSteeringController = new ContentSteeringController(this.mockSegmentLoader); + // handles a common testing flow of assigning tag properties and requesting the steering manifest immediately. + this.assignAndRequest = (steeringTag) => { + this.contentSteeringController.assignTagProperties(this.baseURL, steeringTag); + this.contentSteeringController.requestSteeringManifest(); + }; + }, + afterEach() { + this.env.restore(); + this.contentSteeringController = null; + } +}); + +// HLS +QUnit.test('Can handle HLS content steering object with serverUri only', function(assert) { + const steeringTag = { + serverUri: 'https://content.steering.hls' + }; + + this.contentSteeringController.assignTagProperties(this.baseURL, steeringTag); + const reloadUri = this.contentSteeringController.steeringManifest.reloadUri; + + assert.equal(reloadUri, steeringTag.serverUri, 'reloadUri is expected value'); +}); + +QUnit.test('Can handle HLS content steering object and manifest with relative serverUri', function(assert) { + const steeringTag = { + serverUri: '/hls/path' + }; + + this.assignAndRequest(steeringTag); + let reloadUri = this.contentSteeringController.steeringManifest.reloadUri; + const steeringResponsePath = 'steering/relative'; + + assert.equal(reloadUri, this.baseURL + steeringTag.serverUri, 'reloadUri is expected value'); + // steering response with relative RELOAD-URI + this.requests[0].respond(200, { 'Content-Type': 'application/json' }, `{ "VERSION": 1, "RELOAD-URI": "${steeringResponsePath}" }`); + reloadUri = this.contentSteeringController.steeringManifest.reloadUri; + assert.equal(reloadUri, this.baseURL + steeringTag.serverUri.slice(0, 5) + steeringResponsePath, 'reloadUri is expected value'); +}); + +QUnit.test('Can handle HLS content steering object with pathwayId', function(assert) { + const steeringTag = { + serverUri: 'https://content.steering.hls', + pathwayId: 'hls-test' + }; + let done; + + // ensure event is fired. + this.contentSteeringController.on('content-steering', function() { + done = assert.async(); + }); + this.assignAndRequest(steeringTag); + // check pathway query param + assert.equal(this.requests[0].uri, steeringTag.serverUri + '/?_HLS_pathway=hls-test', 'query parameters are set'); + assert.equal(this.contentSteeringController.defaultPathway, steeringTag.pathwayId, 'default pathway is expected value'); + assert.ok(done, 'content-steering event was fired'); + done(); +}); + +QUnit.test('Can add HLS pathway and throughput to steering manifest requests', function(assert) { + const steeringTag = { + serverUri: 'https://content.steering.hls', + pathwayId: 'cdn-a' + }; + const expectedThroughputUrl = steeringTag.serverUri + '/?_HLS_pathway=cdn-a&_HLS_throughput=99999'; + + this.contentSteeringController.assignTagProperties(this.baseURL, steeringTag); + this.mockSegmentLoader.throughput.rate = 99999; + assert.equal(this.contentSteeringController.setSteeringParams_(steeringTag.serverUri), expectedThroughputUrl, 'pathway and throughput parameters set as expected'); +}); + +QUnit.test('Can handle HLS content steering object with serverUri encoded as a base64 dataURI', function(assert) { + const steeringTag = { + serverUri: 'data:application/' + + 'vnd.apple.steeringlist;base64,eyJWRVJTSU9OIjoxLCJUVEwiOjMwMCwiUkVMT0FELVVSSSI6Imh0dHBzOi8vZXhhbXB' + + 'sZS5jb20vc3RlZXJpbmc/dmlkZW89MDAwMTImc2Vzc2lvbj0xMjMiLCJQQVRIV0FZLVBSSU9SSVRZIjpbIkNETi1BIiwiQ0ROLUIiXX0=' + }; + const steeringManifest = this.contentSteeringController.steeringManifest; + + this.contentSteeringController.assignTagProperties(this.baseURL, steeringTag); + assert.equal(steeringManifest.reloadUri, 'https://example.com/steering?video=00012&session=123', 'reloadUri is expected value'); + assert.equal(steeringManifest.ttl, 300, 'ttl is expected value'); + assert.deepEqual(steeringManifest.priority, ['CDN-A', 'CDN-B'], 'cdnPriority is expected value'); +}); + +// DASH +QUnit.test('Can handle DASH content steering object with serverURL only', function(assert) { + const steeringTag = { + serverURL: 'https://content.steering.dash' + }; + + this.contentSteeringController.assignTagProperties(this.baseURL, steeringTag); + const reloadUri = this.contentSteeringController.steeringManifest.reloadUri; + + assert.equal(reloadUri, steeringTag.serverURL, 'reloadUri is expected value'); +}); + +QUnit.test('Can handle DASH content steering object and manifest with relative serverURL', function(assert) { + const steeringTag = { + serverURL: '/dash/path' + }; + + this.assignAndRequest(steeringTag); + let reloadUri = this.contentSteeringController.steeringManifest.reloadUri; + const steeringResponsePath = 'steering/relative'; + + assert.equal(reloadUri, this.baseURL + steeringTag.serverURL, 'reloadUri is expected value'); + // steering response with relative RELOAD-URI + this.requests[0].respond(200, { 'Content-Type': 'application/json' }, `{ "VERSION": 1, "RELOAD-URI": "${steeringResponsePath}" }`); + reloadUri = this.contentSteeringController.steeringManifest.reloadUri; + assert.equal(reloadUri, this.baseURL + steeringTag.serverURL.slice(0, 6) + steeringResponsePath, 'reloadUri is expected value'); +}); + +QUnit.test('Can handle DASH content steering object with defaultServiceLocation', function(assert) { + const steeringTag = { + serverURL: 'https://content.steering.dash', + defaultServiceLocation: 'dash-test' + }; + let done; + + // ensure event is fired. + this.contentSteeringController.on('content-steering', function() { + done = assert.async(); + }); + this.assignAndRequest(steeringTag); + assert.equal(this.requests[0].uri, steeringTag.serverURL + '/?_DASH_pathway=dash-test', 'query parameters are set'); + assert.equal(this.contentSteeringController.defaultPathway, steeringTag.defaultServiceLocation, 'default pathway is expected value'); + assert.ok(done, 'content-steering event was fired'); + done(); +}); + +QUnit.test('Can add DASH pathway and throughput to steering manifest requests', function(assert) { + const steeringTag = { + serverURL: 'https://content.steering.dash/?previous=params', + defaultServiceLocation: 'cdn-c' + }; + const expectedThroughputUrl = steeringTag.serverURL + '&_DASH_pathway=cdn-c&_DASH_throughput=9999'; + + this.contentSteeringController.assignTagProperties(this.baseURL, steeringTag); + this.mockSegmentLoader.throughput.rate = 9999; + assert.equal(this.contentSteeringController.setSteeringParams_(steeringTag.serverURL), expectedThroughputUrl, 'pathway and throughput parameters set as expected'); +}); + +QUnit.test('Can set DASH queryBeforeStart property', function(assert) { + const steeringTag = { + serverURL: 'https://content.steering.dash', + queryBeforeStart: true + }; + + this.contentSteeringController.assignTagProperties(this.baseURL, steeringTag); + assert.true(this.contentSteeringController.queryBeforeStart, 'queryBeforeStart is true'); +}); + +QUnit.test('Can handle DASH proxyServerURL', function(assert) { + const steeringTag = { + serverURL: 'https://content.steering.dash/?previous=params', + proxyServerURL: 'https://proxy.url', + defaultServiceLocation: 'dash-cdn' + }; + const expectedProxyUrl = 'https://proxy.url/?url=https%3A%2F%2Fcontent.steering.dash%2F%3Fprevious%3Dparams&_DASH_pathway=dash-cdn&_DASH_throughput=99'; + + this.mockSegmentLoader.throughput.rate = 99; + this.assignAndRequest(steeringTag); + assert.equal(this.requests[0].uri, expectedProxyUrl, 'returns expected proxy server URL'); +}); + +// Common steering manifest tests +QUnit.test('Can handle content steering manifest with VERSION', function(assert) { + const steeringTag = { + serverUri: '/content/steering' + }; + const manifest = this.contentSteeringController.steeringManifest; + + this.assignAndRequest(steeringTag); + this.requests[0].respond(200, { 'Content-Type': 'application/json' }, '{ "VERSION": 1 }'); + assert.equal(manifest.version, 1, 'version is expected value'); + assert.equal(manifest.ttl, 300, 'ttl is 300 by default'); +}); + +QUnit.test('Can handle content steering manifest with RELOAD-URI', function(assert) { + const steeringTag = { + serverURL: 'https://content.steering.dash' + }; + const manifest = this.contentSteeringController.steeringManifest; + + this.assignAndRequest(steeringTag); + assert.equal(manifest.reloadUri, 'https://content.steering.dash', 'reloadUri is expected value'); + this.requests[0].respond(200, { 'Content-Type': 'application/json' }, '{ "VERSION": 1, "RELOAD-URI": "https://reload.uri" }'); + assert.equal(manifest.reloadUri, 'https://reload.uri', 'reloadUri is expected value'); + assert.equal(manifest.ttl, 300, 'ttl is 300 by default'); +}); + +QUnit.test('Can handle content steering manifest with TTL', function(assert) { + const steeringTag = { + serverUri: 'https://content.steering.hls' + }; + + this.assignAndRequest(steeringTag); + this.requests[0].respond(200, { 'Content-Type': 'application/json' }, '{ "VERSION": 1, "TTL": 1 }'); + assert.equal(this.contentSteeringController.steeringManifest.ttl, 1, 'ttl is expected value'); + assert.ok(this.contentSteeringController.ttlTimeout_, 'ttl timeout is set'); +}); + +// HLS +QUnit.test('Can handle HLS content steering manifest with PATHWAY-PRIORITY', function(assert) { + const steeringTag = { + serverUri: 'https://content.steering.hls' + }; + + this.assignAndRequest(steeringTag); + this.requests[0].respond(200, { 'Content-Type': 'application/json' }, '{ "VERSION": 1, "PATHWAY-PRIORITY": ["hls1", "hls2"] }'); + assert.deepEqual(this.contentSteeringController.steeringManifest.priority, ['hls1', 'hls2'], 'priority is expected value'); +}); + +QUnit.test('Can handle HLS content steering manifest with PATHWAY-PRIORITY and tag with pathwayId', function(assert) { + const steeringTag = { + serverUri: 'https://content.steering.hls', + pathwayId: 'hls2' + }; + + this.contentSteeringController.addAvailablePathway('hls1'); + this.contentSteeringController.addAvailablePathway('hls2'); + this.assignAndRequest(steeringTag); + this.requests[0].respond(200, { 'Content-Type': 'application/json' }, '{ "VERSION": 1, "PATHWAY-PRIORITY": ["hls1", "hls2"] }'); + assert.deepEqual(this.contentSteeringController.steeringManifest.priority, ['hls1', 'hls2'], 'priority is expected value'); + assert.equal(this.contentSteeringController.currentPathway, 'hls1', 'current pathway is hls1'); +}); + +// DASH +QUnit.test('Can handle DASH content steering manifest with SERVICE-LOCATION-PRIORITY', function(assert) { + const steeringTag = { + serverURL: 'https://content.steering.dash' + }; + + this.assignAndRequest(steeringTag); + this.requests[0].respond(200, { 'Content-Type': 'application/json' }, '{ "VERSION": 1, "SERVICE-LOCATION-PRIORITY": ["dash1", "dash2", "dash3"] }'); + assert.deepEqual(this.contentSteeringController.steeringManifest.priority, ['dash1', 'dash2', 'dash3'], 'priority is expected value'); +}); + +QUnit.test('Can handle DASH content steering manifest with PATHWAY-PRIORITY and tag with pathwayId', function(assert) { + const steeringTag = { + serverUri: 'https://content.steering.hls', + pathwayId: 'dash3' + }; + + this.contentSteeringController.addAvailablePathway('dash1'); + this.contentSteeringController.addAvailablePathway('dash2'); + this.contentSteeringController.addAvailablePathway('dash3'); + this.assignAndRequest(steeringTag); + this.requests[0].respond(200, { 'Content-Type': 'application/json' }, '{ "VERSION": 1, "SERVICE-LOCATION-PRIORITY": ["dash2", "dash1", "dash3"] }'); + assert.deepEqual(this.contentSteeringController.steeringManifest.priority, ['dash2', 'dash1', 'dash3'], 'priority is expected value'); + assert.equal(this.contentSteeringController.currentPathway, 'dash2', 'current pathway is dash2'); +}); + +// Common abort, dispose and error cases +QUnit.test('Can abort a content steering manifest request', function(assert) { + const steeringTag = { + serverURL: 'https://content.steering.dash' + }; + + this.assignAndRequest(steeringTag); + this.contentSteeringController.abort(); + assert.true(this.requests[0].aborted, 'request is aborted'); + assert.equal(this.contentSteeringController.request, null, 'request is null'); +}); + +QUnit.test('Can abort and clear the TTL timeout for a content steering manifest', function(assert) { + const steeringTag = { + serverUri: 'https://content.steering.hls' + }; + + this.assignAndRequest(steeringTag); + this.contentSteeringController.dispose(); + assert.true(this.requests[0].aborted, 'request is aborted'); + assert.equal(this.contentSteeringController.request_, null, 'request is null'); + assert.equal(this.contentSteeringController.ttlTimeout, null, 'ttl timeout is null'); +}); + +QUnit.test('trigger error on VERSION !== 1', function(assert) { + const steeringTag = { + serverUri: '/content/steering' + }; + const manifest = this.contentSteeringController.steeringManifest; + const done = assert.async(); + + this.contentSteeringController.on('error', function() { + assert.equal(manifest.version, undefined, 'version is undefined'); + assert.equal(manifest.ttl, undefined, 'ttl is undefined'); + done(); + }); + this.assignAndRequest(steeringTag); + this.requests[0].respond(200, { 'Content-Type': 'application/json' }, '{ "VERSION": 0 }'); +}); + +QUnit.test('trigger error when serverUri or serverURL is undefined', function(assert) { + const steeringTag = {}; + const done = assert.async(); + + this.contentSteeringController.on('error', function() { + assert.equal(undefined, this.steeringManifest.reloadUri, 'reloadUri is undefined'); + done(); + }); + this.contentSteeringController.assignTagProperties(this.baseURL, steeringTag); +}); + +QUnit.test('trigger error on steering manifest request error', function(assert) { + const steeringTag = { + serverUri: '/content/steering' + }; + const manifest = this.contentSteeringController.steeringManifest; + const done = assert.async(); + + this.contentSteeringController.on('error', function() { + assert.equal(manifest.version, undefined, 'version is undefined'); + assert.equal(manifest.ttl, undefined, 'ttl is undefined'); + done(); + }); + this.assignAndRequest(steeringTag); + this.requests[0].respond(404); +});