diff --git a/README.md b/README.md
index cdf8299..1d13cef 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,10 @@ const manifestUri = 'https://example.com/dash.xml';
const res = await fetch(manifestUri);
const manifest = await res.text();
-var parsedManifest = mpdParser.parse(manifest, { manifestUri });
+// A callback function to handle events like errors or warnings
+const eventHandler = ({ type, message }) => console.log(`${type}: ${message}`);
+
+var parsedManifest = mpdParser.parse(manifest, { manifestUri, eventHandler });
```
If dealing with a live stream, then on subsequent calls to parse, the previously parsed
@@ -63,6 +66,12 @@ The parser ouputs a plain javascript object with the following structure:
```js
Manifest {
allowCache: boolean,
+ contentSteering: {
+ defaultServiceLocation: string,
+ proxyServerURL: string,
+ queryBeforeStart: boolean,
+ serverURL: string
+ },
endList: boolean,
mediaSequence: number,
discontinuitySequence: number,
diff --git a/package-lock.json b/package-lock.json
index 9fc5e1e..154e33f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1525,27 +1525,27 @@
}
},
"@sinonjs/commons": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
- "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==",
+ "version": "1.8.6",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
+ "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==",
"dev": true,
"requires": {
"type-detect": "4.0.8"
}
},
"@sinonjs/fake-timers": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz",
- "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz",
+ "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.7.0"
}
},
"@sinonjs/samsam": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz",
- "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==",
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.3.tgz",
+ "integrity": "sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.6.0",
@@ -1554,9 +1554,9 @@
}
},
"@sinonjs/text-encoding": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
- "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
+ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
"dev": true
},
"@textlint/ast-node-types": {
@@ -3142,9 +3142,9 @@
"dev": true
},
"diff": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
- "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
"dev": true
},
"doctoc": {
@@ -6087,7 +6087,7 @@
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
- "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"dev": true
},
"lodash.ismatch": {
@@ -6681,16 +6681,47 @@
"dev": true
},
"nise": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
- "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==",
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz",
+ "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==",
"dev": true,
"requires": {
- "@sinonjs/commons": "^1.7.0",
- "@sinonjs/fake-timers": "^6.0.0",
+ "@sinonjs/commons": "^2.0.0",
+ "@sinonjs/fake-timers": "^10.0.2",
"@sinonjs/text-encoding": "^0.7.1",
"just-extend": "^4.0.2",
"path-to-regexp": "^1.7.0"
+ },
+ "dependencies": {
+ "@sinonjs/commons": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
+ "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
+ "dev": true,
+ "requires": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "@sinonjs/fake-timers": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
+ "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
+ "dev": true,
+ "requires": {
+ "@sinonjs/commons": "^3.0.0"
+ },
+ "dependencies": {
+ "@sinonjs/commons": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
+ "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
+ "dev": true,
+ "requires": {
+ "type-detect": "4.0.8"
+ }
+ }
+ }
+ }
}
},
"node-releases": {
@@ -7139,7 +7170,7 @@
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"dev": true
}
}
@@ -8183,17 +8214,17 @@
"dev": true
},
"sinon": {
- "version": "9.2.4",
- "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz",
- "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==",
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz",
+ "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==",
"dev": true,
"requires": {
- "@sinonjs/commons": "^1.8.1",
- "@sinonjs/fake-timers": "^6.0.1",
- "@sinonjs/samsam": "^5.3.1",
- "diff": "^4.0.2",
- "nise": "^4.0.4",
- "supports-color": "^7.1.0"
+ "@sinonjs/commons": "^1.8.3",
+ "@sinonjs/fake-timers": "^7.1.2",
+ "@sinonjs/samsam": "^6.0.2",
+ "diff": "^5.0.0",
+ "nise": "^5.1.0",
+ "supports-color": "^7.2.0"
},
"dependencies": {
"has-flag": {
diff --git a/package.json b/package.json
index 6f29910..14c2989 100644
--- a/package.json
+++ b/package.json
@@ -67,7 +67,7 @@
"karma": "^5.2.3",
"rollup": "^2.38.0",
"rollup-plugin-string": "^3.0.0",
- "sinon": "^9.2.3",
+ "sinon": "^11.1.1",
"videojs-generate-karma-config": "^8.0.1",
"videojs-generate-rollup-config": "~7.0.0",
"videojs-generator-verify": "~3.0.2",
diff --git a/src/errors.js b/src/errors.js
index 1fd0928..4e9fd73 100644
--- a/src/errors.js
+++ b/src/errors.js
@@ -1,5 +1,6 @@
export default {
INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
+ INVALID_NUMBER_OF_CONTENT_STEERING: 'INVALID_NUMBER_OF_CONTENT_STEERING',
DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST',
DASH_INVALID_XML: 'DASH_INVALID_XML',
NO_BASE_URL: 'NO_BASE_URL',
diff --git a/src/index.js b/src/index.js
index a63e4d5..818f1df 100644
--- a/src/index.js
+++ b/src/index.js
@@ -28,6 +28,7 @@ const parse = (manifestString, options = {}) => {
return toM3u8({
dashPlaylists: playlists,
locations: parsedManifestInfo.locations,
+ contentSteering: parsedManifestInfo.contentSteeringInfo,
sidxMapping: options.sidxMapping,
previousManifest: options.previousManifest,
eventStream: parsedManifestInfo.eventStream
diff --git a/src/inheritAttributes.js b/src/inheritAttributes.js
index b77e9d0..5b82293 100644
--- a/src/inheritAttributes.js
+++ b/src/inheritAttributes.js
@@ -16,21 +16,21 @@ const keySystemsMap = {
/**
* Builds a list of urls that is the product of the reference urls and BaseURL values
*
- * @param {string[]} referenceUrls
- * List of reference urls to resolve to
+ * @param {Object[]} references
+ * List of objects containing the reference URL as well as its attributes
* @param {Node[]} baseUrlElements
* List of BaseURL nodes from the mpd
- * @return {string[]}
- * List of resolved urls
+ * @return {Object[]}
+ * List of objects with resolved urls and attributes
*/
-export const buildBaseUrls = (referenceUrls, baseUrlElements) => {
+export const buildBaseUrls = (references, baseUrlElements) => {
if (!baseUrlElements.length) {
- return referenceUrls;
+ return references;
}
- return flatten(referenceUrls.map(function(reference) {
+ return flatten(references.map(function(reference) {
return baseUrlElements.map(function(baseUrlElement) {
- return resolveUrl(reference, getContent(baseUrlElement));
+ return merge(parseAttributes(baseUrlElement), { baseUrl: resolveUrl(reference.baseUrl, getContent(baseUrlElement)) });
});
}));
};
@@ -140,8 +140,9 @@ export const getSegmentInformation = (adaptationSet) => {
*
* @param {Object} adaptationSetAttributes
* Contains attributes inherited by the AdaptationSet
- * @param {string[]} adaptationSetBaseUrls
- * Contains list of resolved base urls inherited by the AdaptationSet
+ * @param {Object[]} adaptationSetBaseUrls
+ * List of objects containing resolved base URLs and attributes
+ * inherited by the AdaptationSet
* @param {SegmentInformation} adaptationSetSegmentInfo
* Contains Segment information for the AdaptationSet
* @return {inheritBaseUrlsCallback}
@@ -158,7 +159,7 @@ export const inheritBaseUrls =
return repBaseUrls.map(baseUrl => {
return {
segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
- attributes: merge(attributes, { baseUrl })
+ attributes: merge(attributes, baseUrl)
};
});
};
@@ -340,8 +341,9 @@ export const toEventStream = (period) => {
*
* @param {Object} periodAttributes
* Contains attributes inherited by the Period
- * @param {string[]} periodBaseUrls
- * Contains list of resolved base urls inherited by the Period
+ * @param {Object[]} periodBaseUrls
+ * Contains list of objects with resolved base urls and attributes
+ * inherited by the Period
* @param {string[]} periodSegmentInfo
* Contains Segment Information at the period level
* @return {toRepresentationsCallback}
@@ -421,8 +423,9 @@ export const toRepresentations =
*
* @param {Object} mpdAttributes
* Contains attributes inherited by the mpd
- * @param {string[]} mpdBaseUrls
- * Contains list of resolved base urls inherited by the mpd
+ * @param {Object[]} mpdBaseUrls
+ * Contains list of objects with resolved base urls and attributes
+ * inherited by the mpd
* @return {toAdaptationSetsCallback}
* Callback map function
*/
@@ -441,6 +444,41 @@ export const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index)
return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo)));
};
+/**
+ * Tranforms an array of content steering nodes into an object
+ * containing CDN content steering information from the MPD manifest.
+ *
+ * For more information on the DASH spec for Content Steering parsing, see:
+ * https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf
+ *
+ * @param {Node[]} contentSteeringNodes
+ * Content steering nodes
+ * @param {Function} eventHandler
+ * The event handler passed into the parser options to handle warnings
+ * @return {Object}
+ * Object containing content steering data
+ */
+export const generateContentSteeringInformation = (contentSteeringNodes, eventHandler) => {
+ // If there are more than one ContentSteering tags, throw an error
+ if (contentSteeringNodes.length > 1) {
+ eventHandler({ type: 'warn', message: 'The MPD manifest should contain no more than one ContentSteering tag' });
+ }
+
+ // Return a null value if there are no ContentSteering tags
+ if (!contentSteeringNodes.length) {
+ return null;
+ }
+
+ const infoFromContentSteeringTag =
+ merge({serverURL: getContent(contentSteeringNodes[0])}, parseAttributes(contentSteeringNodes[0]));
+
+ // Converts `queryBeforeStart` to a boolean, as well as setting the default value
+ // to `false` if it doesn't exist
+ infoFromContentSteeringTag.queryBeforeStart = (infoFromContentSteeringTag.queryBeforeStart === 'true');
+
+ return infoFromContentSteeringTag;
+};
+
/**
* Gets Period@start property for a given period.
*
@@ -518,7 +556,14 @@ export const inheritAttributes = (mpd, options = {}) => {
const {
manifestUri = '',
NOW = Date.now(),
- clientOffset = 0
+ clientOffset = 0,
+ // TODO: For now, we are expecting an eventHandler callback function
+ // to be passed into the mpd parser as an option.
+ // In the future, we should enable stream parsing by using the Stream class from vhs-utils.
+ // This will support new features including a standardized event handler.
+ // See the m3u8 parser for examples of how stream parsing is currently used for HLS parsing.
+ // https://github.com/videojs/vhs-utils/blob/88d6e10c631e57a5af02c5a62bc7376cd456b4f5/src/stream.js#L9
+ eventHandler = function() {}
} = options;
const periodNodes = findChildren(mpd, 'Period');
@@ -530,6 +575,7 @@ export const inheritAttributes = (mpd, options = {}) => {
const mpdAttributes = parseAttributes(mpd);
const mpdBaseUrls = buildBaseUrls([ manifestUri ], findChildren(mpd, 'BaseURL'));
+ const contentSteeringNodes = findChildren(mpd, 'ContentSteering');
// See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'.
mpdAttributes.type = mpdAttributes.type || 'static';
@@ -567,6 +613,14 @@ export const inheritAttributes = (mpd, options = {}) => {
return {
locations: mpdAttributes.locations,
+ contentSteeringInfo: generateContentSteeringInformation(contentSteeringNodes, eventHandler),
+ // TODO: There are occurences where this `representationInfo` array contains undesired
+ // duplicates. This generally occurs when there are multiple BaseURL nodes that are
+ // direct children of the MPD node. When we attempt to resolve URLs from a combination of the
+ // parent BaseURL and a child BaseURL, and the value does not resolve,
+ // we end up returning the child BaseURL multiple times.
+ // We need to determine a way to remove these duplicates in a safe way.
+ // See: https://github.com/videojs/mpd-parser/pull/17#discussion_r162750527
representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))),
eventStream: flatten(periods.map(toEventStream))
};
diff --git a/src/stringToMpdXml.js b/src/stringToMpdXml.js
index 8bd1a20..d68f4ab 100644
--- a/src/stringToMpdXml.js
+++ b/src/stringToMpdXml.js
@@ -15,7 +15,7 @@ export const stringToMpdXml = (manifestString) => {
mpd = xml && xml.documentElement.tagName === 'MPD' ?
xml.documentElement : null;
} catch (e) {
- // ie 11 throwsw on invalid xml
+ // ie 11 throws on invalid xml
}
if (!mpd || mpd &&
diff --git a/src/toM3u8.js b/src/toM3u8.js
index 132b96d..01ba625 100644
--- a/src/toM3u8.js
+++ b/src/toM3u8.js
@@ -392,6 +392,7 @@ export const flattenMediaGroupPlaylists = (mediaGroupObject) => {
export const toM3u8 = ({
dashPlaylists,
locations,
+ contentSteering,
sidxMapping = {},
previousManifest,
eventStream
@@ -437,6 +438,10 @@ export const toM3u8 = ({
manifest.locations = locations;
}
+ if (contentSteering) {
+ manifest.contentSteering = contentSteering;
+ }
+
if (type === 'dynamic') {
manifest.suggestedPresentationDelay = suggestedPresentationDelay;
}
diff --git a/test/inheritAttributes.test.js b/test/inheritAttributes.test.js
index 37dde40..8b9f514 100644
--- a/test/inheritAttributes.test.js
+++ b/test/inheritAttributes.test.js
@@ -9,6 +9,7 @@ import {
import { stringToMpdXml } from '../src/stringToMpdXml';
import errors from '../src/errors';
import QUnit from 'qunit';
+import { stub } from 'sinon';
import { toPlaylists } from '../src/toPlaylists';
import decodeB64ToUint8Array from '@videojs/vhs-utils/es/decode-b64-to-uint8-array';
import { findChildren } from '../src/utils/xml';
@@ -16,23 +17,23 @@ import { findChildren } from '../src/utils/xml';
QUnit.module('buildBaseUrls');
QUnit.test('returns reference urls when no BaseURL nodes', function(assert) {
- const reference = ['https://example.com/', 'https://foo.com/'];
+ const reference = [{ baseUrl: 'https://example.com/' }, { baseUrl: 'https://foo.com/' }];
assert.deepEqual(buildBaseUrls(reference, []), reference, 'returns reference urls');
});
QUnit.test('single reference url with single BaseURL node', function(assert) {
- const reference = ['https://example.com'];
+ const reference = [{ baseUrl: 'https://example.com' }];
const node = [{ textContent: 'bar/' }];
- const expected = ['https://example.com/bar/'];
+ const expected = [{ baseUrl: 'https://example.com/bar/' }];
assert.deepEqual(buildBaseUrls(reference, node), expected, 'builds base url');
});
QUnit.test('multiple reference urls with single BaseURL node', function(assert) {
- const reference = ['https://example.com/', 'https://foo.com/'];
+ const reference = [{ baseUrl: 'https://example.com/' }, { baseUrl: 'https://foo.com/' }];
const node = [{ textContent: 'bar/' }];
- const expected = ['https://example.com/bar/', 'https://foo.com/bar/'];
+ const expected = [{ baseUrl: 'https://example.com/bar/' }, { baseUrl: 'https://foo.com/bar/' }];
assert.deepEqual(
buildBaseUrls(reference, node), expected,
@@ -41,36 +42,38 @@ QUnit.test('multiple reference urls with single BaseURL node', function(assert)
});
QUnit.test('multiple BaseURL nodes with single reference url', function(assert) {
- const reference = ['https://example.com/'];
+ const reference = [{ baseUrl: 'https://example.com/' }];
const nodes = [{ textContent: 'bar/' }, { textContent: 'baz/' }];
- const expected = ['https://example.com/bar/', 'https://example.com/baz/'];
+ const expected = [{ baseUrl: 'https://example.com/bar/' }, { baseUrl: 'https://example.com/baz/' }];
assert.deepEqual(buildBaseUrls(reference, nodes), expected, 'base url for each node');
});
QUnit.test('multiple reference urls with multiple BaseURL nodes', function(assert) {
- const reference = ['https://example.com/', 'https://foo.com/', 'http://example.com'];
+ const reference = [
+ { baseUrl: 'https://example.com/' }, { baseUrl: 'https://foo.com/' }, { baseUrl: 'http://example.com' }
+ ];
const nodes =
[{ textContent: 'bar/' }, { textContent: 'baz/' }, { textContent: 'buzz/' }];
const expected = [
- 'https://example.com/bar/',
- 'https://example.com/baz/',
- 'https://example.com/buzz/',
- 'https://foo.com/bar/',
- 'https://foo.com/baz/',
- 'https://foo.com/buzz/',
- 'http://example.com/bar/',
- 'http://example.com/baz/',
- 'http://example.com/buzz/'
+ { baseUrl: 'https://example.com/bar/' },
+ { baseUrl: 'https://example.com/baz/' },
+ { baseUrl: 'https://example.com/buzz/' },
+ { baseUrl: 'https://foo.com/bar/' },
+ { baseUrl: 'https://foo.com/baz/' },
+ { baseUrl: 'https://foo.com/buzz/' },
+ { baseUrl: 'http://example.com/bar/' },
+ { baseUrl: 'http://example.com/baz/' },
+ { baseUrl: 'http://example.com/buzz/' }
];
assert.deepEqual(buildBaseUrls(reference, nodes), expected, 'creates all base urls');
});
QUnit.test('absolute BaseURL overwrites reference', function(assert) {
- const reference = ['https://example.com'];
+ const reference = [{ baseUrl: 'https://example.com' }];
const node = [{ textContent: 'https://foo.com/bar/' }];
- const expected = ['https://foo.com/bar/'];
+ const expected = [{ baseUrl: 'https://foo.com/bar/'}];
assert.deepEqual(
buildBaseUrls(reference, node), expected,
@@ -78,6 +81,40 @@ QUnit.test('absolute BaseURL overwrites reference', function(assert) {
);
});
+QUnit.test('reference attributes are ignored when there is a BaseURL node', function(assert) {
+ const reference = [{ baseUrl: 'https://example.com', attributes: [{ name: 'test', value: 'wow' }] }];
+ const node = [{ textContent: 'https://foo.com/bar/' }];
+ const expected = [{ baseUrl: 'https://foo.com/bar/' }];
+
+ assert.deepEqual(
+ buildBaseUrls(reference, node), expected,
+ 'baseURL attributes are not included'
+ );
+});
+
+QUnit.test('BasURL attributes are still added with a reference', function(assert) {
+ const reference = [{ baseUrl: 'https://example.com' }];
+ const node = [{ textContent: 'https://foo.com/bar/', attributes: [{ name: 'test', value: 'wow' }] }];
+
+ const expected = [{ baseUrl: 'https://foo.com/bar/', test: 'wow' }];
+
+ assert.deepEqual(
+ buildBaseUrls(reference, node), expected,
+ 'baseURL attributes are included'
+ );
+});
+
+QUnit.test('attributes are replaced when both reference and BaseURL have the same attributes', function(assert) {
+ const reference = [{ baseUrl: 'https://example.com', attributes: [{ name: 'test', value: 'old' }] }];
+ const node = [{ textContent: 'https://foo.com/bar/', attributes: [{ name: 'test', value: 'new' }] }];
+ const expected = [{ baseUrl: 'https://foo.com/bar/', test: 'new' }];
+
+ assert.deepEqual(
+ buildBaseUrls(reference, node), expected,
+ 'baseURL attributes are included'
+ );
+});
+
QUnit.module('getPeriodStart');
QUnit.test('gets period start when available', function(assert) {
@@ -546,6 +583,7 @@ QUnit.test('end to end - basic', function(assert) {
`), { NOW });
const expected = {
+ contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@@ -621,6 +659,7 @@ QUnit.test('end to end - basic dynamic', function(assert) {
`), { NOW });
const expected = {
+ contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@@ -666,6 +705,300 @@ QUnit.test('end to end - basic dynamic', function(assert) {
assert.deepEqual(actual, expected);
});
+QUnit.test('end to end - content steering - non resolvable base URLs', function(assert) {
+ const NOW = Date.now();
+
+ const actual = inheritAttributes(stringToMpdXml(`
+
+ https://example.com/app/url
+ https://cdn1.example.com/
+ https://cdn2.example.com/
+
+
+
+
+
+
+
+
+
+ https://example.com/en.vtt
+
+
+
+
+`), { NOW, manifestUri: 'https://www.test.com' });
+
+ // Note that we expect to see the `contentSteeringInfo` object set with the
+ // proper values. We also expect to see the `serviceLocation` property set to
+ // the correct values inside of the correct representations.
+ const expected = {
+ contentSteeringInfo: {
+ defaultServiceLocation: 'beta',
+ proxyServerURL: 'http://127.0.0.1:3455/steer',
+ queryBeforeStart: false,
+ serverURL: 'https://example.com/app/url'
+ },
+ eventStream: [],
+ locations: undefined,
+ representationInfo: [
+ {
+ attributes: {
+ NOW,
+ bandwidth: 5000000,
+ baseUrl: 'https://cdn1.example.com/',
+ clientOffset: 0,
+ codecs: 'avc1.64001e',
+ height: 404,
+ id: 'test',
+ mimeType: 'video/mp4',
+ periodStart: 0,
+ role: {
+ value: 'main'
+ },
+ serviceLocation: 'alpha',
+ sourceDuration: 0,
+ type: 'dyanmic',
+ width: 720
+ },
+ segmentInfo: {
+ template: {}
+ }
+ },
+ {
+ attributes: {
+ NOW,
+ bandwidth: 5000000,
+ baseUrl: 'https://cdn2.example.com/',
+ clientOffset: 0,
+ codecs: 'avc1.64001e',
+ height: 404,
+ id: 'test',
+ mimeType: 'video/mp4',
+ periodStart: 0,
+ role: {
+ value: 'main'
+ },
+ serviceLocation: 'beta',
+ sourceDuration: 0,
+ type: 'dyanmic',
+ width: 720
+ },
+ segmentInfo: {
+ template: {}
+ }
+ },
+ {
+ attributes: {
+ NOW,
+ bandwidth: 256,
+ baseUrl: 'https://example.com/en.vtt',
+ clientOffset: 0,
+ id: 'en',
+ lang: 'en',
+ mimeType: 'text/vtt',
+ periodStart: 0,
+ role: {},
+ sourceDuration: 0,
+ type: 'dyanmic'
+ },
+ segmentInfo: {}
+ },
+ {
+ attributes: {
+ NOW,
+ bandwidth: 256,
+ baseUrl: 'https://example.com/en.vtt',
+ clientOffset: 0,
+ id: 'en',
+ lang: 'en',
+ mimeType: 'text/vtt',
+ periodStart: 0,
+ role: {},
+ sourceDuration: 0,
+ type: 'dyanmic'
+ },
+ segmentInfo: {}
+ }
+ ]
+ };
+
+ assert.equal(actual.representationInfo.length, 4);
+ assert.deepEqual(actual, expected);
+});
+
+QUnit.test('end to end - content steering - resolvable base URLs', function(assert) {
+ const NOW = Date.now();
+
+ const actual = inheritAttributes(stringToMpdXml(`
+
+ https://example.com/app/url
+ https://cdn1.example.com/
+ https://cdn2.example.com/
+
+
+
+
+
+
+
+
+
+ /video
+
+
+
+
+`), { NOW, manifestUri: 'https://www.test.com' });
+
+ // Note that we expect to see the `contentSteeringInfo` object set with the
+ // proper values. We also expect to see the `serviceLocation` property set to
+ // the correct values inside of the correct representations.
+ //
+ // Also note that some of the representations have '/video' appended
+ // to the end of the baseUrls
+ const expected = {
+ contentSteeringInfo: {
+ defaultServiceLocation: 'beta',
+ proxyServerURL: 'http://127.0.0.1:3455/steer',
+ queryBeforeStart: false,
+ serverURL: 'https://example.com/app/url'
+ },
+ eventStream: [],
+ locations: undefined,
+ representationInfo: [
+ {
+ attributes: {
+ NOW,
+ bandwidth: 5000000,
+ baseUrl: 'https://cdn1.example.com/',
+ clientOffset: 0,
+ codecs: 'avc1.64001e',
+ height: 404,
+ id: 'test',
+ mimeType: 'video/mp4',
+ periodStart: 0,
+ role: {
+ value: 'main'
+ },
+ serviceLocation: 'alpha',
+ sourceDuration: 0,
+ type: 'dyanmic',
+ width: 720
+ },
+ segmentInfo: {
+ template: {}
+ }
+ },
+ {
+ attributes: {
+ NOW,
+ bandwidth: 5000000,
+ baseUrl: 'https://cdn2.example.com/',
+ clientOffset: 0,
+ codecs: 'avc1.64001e',
+ height: 404,
+ id: 'test',
+ mimeType: 'video/mp4',
+ periodStart: 0,
+ role: {
+ value: 'main'
+ },
+ serviceLocation: 'beta',
+ sourceDuration: 0,
+ type: 'dyanmic',
+ width: 720
+ },
+ segmentInfo: {
+ template: {}
+ }
+ },
+ {
+ attributes: {
+ NOW,
+ bandwidth: 256,
+ baseUrl: 'https://cdn1.example.com/video',
+ clientOffset: 0,
+ id: 'en',
+ lang: 'en',
+ mimeType: 'text/vtt',
+ periodStart: 0,
+ role: {},
+ sourceDuration: 0,
+ type: 'dyanmic'
+ },
+ segmentInfo: {}
+ },
+ {
+ attributes: {
+ NOW,
+ bandwidth: 256,
+ baseUrl: 'https://cdn2.example.com/video',
+ clientOffset: 0,
+ id: 'en',
+ lang: 'en',
+ mimeType: 'text/vtt',
+ periodStart: 0,
+ role: {},
+ sourceDuration: 0,
+ type: 'dyanmic'
+ },
+ segmentInfo: {}
+ }
+ ]
+ };
+
+ assert.equal(actual.representationInfo.length, 4);
+ assert.deepEqual(actual, expected);
+});
+
+QUnit.test('Too many content steering tags sends a warning to the eventHandler', function(assert) {
+ const handlerStub = stub();
+ const NOW = Date.now();
+
+ inheritAttributes(stringToMpdXml(`
+
+ https://example.com/app/url
+ https://example.com/app/url
+ https://cdn1.example.com/
+ https://cdn2.example.com/
+
+
+
+
+
+
+
+
+
+ /video
+
+
+
+
+ `), { NOW, manifestUri: 'https://www.test.com', eventHandler: handlerStub });
+
+ assert.ok(handlerStub.calledWith({
+ type: 'warn',
+ message: 'The MPD manifest should contain no more than one ContentSteering tag'
+ }));
+});
+
QUnit.test('end to end - basic multiperiod', function(assert) {
const NOW = Date.now();
@@ -703,6 +1036,7 @@ QUnit.test('end to end - basic multiperiod', function(assert) {
`), { NOW });
const expected = {
+ contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@@ -790,6 +1124,7 @@ QUnit.test('end to end - inherits BaseURL from all levels', function(assert) {
`), { NOW });
const expected = {
+ contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@@ -867,6 +1202,7 @@ QUnit.test('end to end - alternate BaseURLs', function(assert) {
`), { NOW });
const expected = {
+ contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@@ -1035,6 +1371,7 @@ QUnit.test(
`), { NOW });
const expected = {
+ contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@@ -1148,6 +1485,7 @@ QUnit.test(
`), { NOW });
const expected = {
+ contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@@ -1272,6 +1610,7 @@ QUnit.test(
`), { NOW });
const expected = {
+ contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@@ -2116,6 +2455,7 @@ QUnit.test('keySystem info for representation - lowercase UUIDs', function(asser
// inconsistent quoting because of quote-props
const expected = {
+ contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@@ -2203,6 +2543,7 @@ QUnit.test('keySystem info for representation - uppercase UUIDs', function(asser
// inconsistent quoting because of quote-props
const expected = {
+ contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@@ -2372,6 +2713,7 @@ QUnit.test('gets eventStream from inheritAttributes', function(assert) {
`);
const expected = {
+ contentSteeringInfo: null,
eventStream: [
{
end: 15,
@@ -2481,6 +2823,7 @@ QUnit.test('gets eventStream from inheritAttributes with data in Event tags', fu
`);
const expected = {
+ contentSteeringInfo: null,
eventStream: [
{
end: 15,
diff --git a/test/toM3u8.test.js b/test/toM3u8.test.js
index 6e57475..2440371 100644
--- a/test/toM3u8.test.js
+++ b/test/toM3u8.test.js
@@ -213,6 +213,246 @@ QUnit.test('playlists', function(assert) {
assert.deepEqual(toM3u8({ dashPlaylists }), expected);
});
+QUnit.test('playlists with content steering', function(assert) {
+ const contentSteering = {
+ defaultServiceLocation: 'beta',
+ proxyServerURL: 'http://127.0.0.1:3455/steer',
+ queryBeforeStart: false,
+ serverURL: 'https://example.com/app/url'
+ };
+
+ const dashPlaylists = [{
+ attributes: {
+ bandwidth: 5000000,
+ baseUrl: 'https://cdn1.example.com/',
+ clientOffset: 0,
+ codecs: 'avc1.64001e',
+ duration: 0,
+ height: 404,
+ id: 'test',
+ mimeType: 'video/mp4',
+ periodStart: 0,
+ role: {
+ value: 'main'
+ },
+ serviceLocation: 'alpha',
+ sourceDuration: 0,
+ type: 'dyanmic',
+ width: 720
+ },
+ segments: [
+ {
+ duration: 0,
+ map: {
+ resolvedUri: 'https://cdn1.example.com/',
+ uri: ''
+ },
+ number: 1,
+ presentationTime: 0,
+ resolvedUri: 'https://cdn1.example.com/',
+ timeline: 0,
+ uri: ''
+ }
+ ]
+ }, {
+ attributes: {
+ bandwidth: 5000000,
+ baseUrl: 'https://cdn2.example.com/',
+ clientOffset: 0,
+ codecs: 'avc1.64001e',
+ duration: 0,
+ height: 404,
+ id: 'test',
+ mimeType: 'video/mp4',
+ periodStart: 0,
+ role: {
+ value: 'main'
+ },
+ serviceLocation: 'beta',
+ sourceDuration: 0,
+ type: 'dyanmic',
+ width: 720
+ },
+ segments: [
+ {
+ duration: 0,
+ map: {
+ resolvedUri: 'https://cdn2.example.com/',
+ uri: ''
+ },
+ number: 1,
+ presentationTime: 0,
+ resolvedUri: 'https://cdn2.example.com/',
+ timeline: 0,
+ uri: ''
+ }
+ ]
+ }, {
+ attributes: {
+ bandwidth: 256,
+ baseUrl: 'https://example.com/en.vtt',
+ clientOffset: 0,
+ id: 'en',
+ lang: 'en',
+ mimeType: 'text/vtt',
+ periodStart: 0,
+ role: {},
+ sourceDuration: 0,
+ type: 'dyanmic'
+ }
+ }, {
+ attributes: {
+ bandwidth: 256,
+ baseUrl: 'https://example.com/en.vtt',
+ clientOffset: 0,
+ id: 'en',
+ lang: 'en',
+ mimeType: 'text/vtt',
+ periodStart: 0,
+ role: {},
+ sourceDuration: 0,
+ type: 'dyanmic'
+ }
+ }];
+
+ const expected = {
+ allowCache: true,
+ contentSteering: {
+ defaultServiceLocation: 'beta',
+ proxyServerURL: 'http://127.0.0.1:3455/steer',
+ queryBeforeStart: false,
+ serverURL: 'https://example.com/app/url'
+ },
+ discontinuityStarts: [],
+ duration: 0,
+ endList: true,
+ mediaGroups: {
+ AUDIO: {},
+ ['CLOSED-CAPTIONS']: {},
+ SUBTITLES: {
+ subs: {
+ en: {
+ autoselect: false,
+ default: false,
+ language: 'en',
+ playlists: [
+ {
+ attributes: {
+ BANDWIDTH: 256,
+ NAME: 'en',
+ ['PROGRAM-ID']: 1
+ },
+ discontinuitySequence: 0,
+ discontinuityStarts: [],
+ endList: false,
+ mediaSequence: 0,
+ resolvedUri: 'https://example.com/en.vtt',
+ segments: [
+ {
+ duration: 0,
+ number: 0,
+ resolvedUri: 'https://example.com/en.vtt',
+ timeline: 0,
+ uri: 'https://example.com/en.vtt'
+ }
+ ],
+ targetDuration: 0,
+ timeline: 0,
+ timelineStarts: [
+ {
+ start: 0,
+ timeline: 0
+ },
+ {
+ start: 0,
+ timeline: 0
+ }
+ ],
+ uri: ''
+ }
+ ],
+ uri: ''
+ }
+ }
+ },
+ VIDEO: {}
+ },
+ playlists: [
+ {
+ attributes: {
+ AUDIO: 'audio',
+ BANDWIDTH: 5000000,
+ CODECS: 'avc1.64001e',
+ NAME: 'test',
+ ['PROGRAM-ID']: 1,
+ RESOLUTION: {
+ height: 404,
+ width: 720
+ },
+ SUBTITLES: 'subs'
+ },
+ discontinuitySequence: 0,
+ discontinuityStarts: [
+ 1
+ ],
+ endList: false,
+ mediaSequence: 0,
+ resolvedUri: '',
+ segments: [
+ {
+ duration: 0,
+ map: {
+ resolvedUri: 'https://cdn1.example.com/',
+ uri: ''
+ },
+ number: 0,
+ presentationTime: 0,
+ resolvedUri: 'https://cdn1.example.com/',
+ timeline: 0,
+ uri: ''
+ },
+ {
+ discontinuity: true,
+ duration: 0,
+ map: {
+ resolvedUri: 'https://cdn2.example.com/',
+ uri: ''
+ },
+ number: 1,
+ presentationTime: 0,
+ resolvedUri: 'https://cdn2.example.com/',
+ timeline: 0,
+ uri: ''
+ }
+ ],
+ targetDuration: 0,
+ timeline: 0,
+ timelineStarts: [
+ {
+ start: 0,
+ timeline: 0
+ },
+ {
+ start: 0,
+ timeline: 0
+ }
+ ],
+ uri: ''
+ }
+ ],
+ segments: [],
+ timelineStarts: [
+ {
+ start: 0,
+ timeline: 0
+ }
+ ],
+ uri: ''
+ };
+
+ assert.deepEqual(toM3u8({ dashPlaylists, contentSteering }), expected);
+});
+
QUnit.test('playlists with segments', function(assert) {
const dashPlaylists = [{
attributes: {
diff --git a/test/toPlaylists.test.js b/test/toPlaylists.test.js
index f41be73..fd0528e 100644
--- a/test/toPlaylists.test.js
+++ b/test/toPlaylists.test.js
@@ -85,6 +85,181 @@ QUnit.test('segment base', function(assert) {
assert.deepEqual(toPlaylists(representations), playlists);
});
+QUnit.test('playlist with content steering BaseURLs', function(assert) {
+ const representations = [
+ {
+ attributes: {
+ bandwidth: 5000000,
+ baseUrl: 'https://cdn1.example.com/',
+ clientOffset: 0,
+ codecs: 'avc1.64001e',
+ height: 404,
+ id: 'test',
+ mimeType: 'video/mp4',
+ periodStart: 0,
+ role: {
+ value: 'main'
+ },
+ serviceLocation: 'alpha',
+ sourceDuration: 0,
+ type: 'dyanmic',
+ width: 720
+ },
+ segmentInfo: {
+ template: {}
+ }
+ },
+ {
+ attributes: {
+ bandwidth: 5000000,
+ baseUrl: 'https://cdn2.example.com/',
+ clientOffset: 0,
+ codecs: 'avc1.64001e',
+ height: 404,
+ id: 'test',
+ mimeType: 'video/mp4',
+ periodStart: 0,
+ role: {
+ value: 'main'
+ },
+ serviceLocation: 'beta',
+ sourceDuration: 0,
+ type: 'dyanmic',
+ width: 720
+ },
+ segmentInfo: {
+ template: {}
+ }
+ },
+ {
+ attributes: {
+ bandwidth: 256,
+ baseUrl: 'https://example.com/en.vtt',
+ clientOffset: 0,
+ id: 'en',
+ lang: 'en',
+ mimeType: 'text/vtt',
+ periodStart: 0,
+ role: {},
+ sourceDuration: 0,
+ type: 'dyanmic'
+ },
+ segmentInfo: {}
+ },
+ {
+ attributes: {
+ bandwidth: 256,
+ baseUrl: 'https://example.com/en.vtt',
+ clientOffset: 0,
+ id: 'en',
+ lang: 'en',
+ mimeType: 'text/vtt',
+ periodStart: 0,
+ role: {},
+ sourceDuration: 0,
+ type: 'dyanmic'
+ },
+ segmentInfo: {}
+ }
+ ];
+
+ const playlists = [{
+ attributes: {
+ bandwidth: 5000000,
+ baseUrl: 'https://cdn1.example.com/',
+ clientOffset: 0,
+ codecs: 'avc1.64001e',
+ duration: 0,
+ height: 404,
+ id: 'test',
+ mimeType: 'video/mp4',
+ periodStart: 0,
+ role: {
+ value: 'main'
+ },
+ serviceLocation: 'alpha',
+ sourceDuration: 0,
+ type: 'dyanmic',
+ width: 720
+ },
+ segments: [
+ {
+ duration: 0,
+ map: {
+ resolvedUri: 'https://cdn1.example.com/',
+ uri: ''
+ },
+ number: 1,
+ presentationTime: 0,
+ resolvedUri: 'https://cdn1.example.com/',
+ timeline: 0,
+ uri: ''
+ }
+ ]
+ }, {
+ attributes: {
+ bandwidth: 5000000,
+ baseUrl: 'https://cdn2.example.com/',
+ clientOffset: 0,
+ codecs: 'avc1.64001e',
+ duration: 0,
+ height: 404,
+ id: 'test',
+ mimeType: 'video/mp4',
+ periodStart: 0,
+ role: {
+ value: 'main'
+ },
+ serviceLocation: 'beta',
+ sourceDuration: 0,
+ type: 'dyanmic',
+ width: 720
+ },
+ segments: [
+ {
+ duration: 0,
+ map: {
+ resolvedUri: 'https://cdn2.example.com/',
+ uri: ''
+ },
+ number: 1,
+ presentationTime: 0,
+ resolvedUri: 'https://cdn2.example.com/',
+ timeline: 0,
+ uri: ''
+ }
+ ]
+ }, {
+ attributes: {
+ bandwidth: 256,
+ baseUrl: 'https://example.com/en.vtt',
+ clientOffset: 0,
+ id: 'en',
+ lang: 'en',
+ mimeType: 'text/vtt',
+ periodStart: 0,
+ role: {},
+ sourceDuration: 0,
+ type: 'dyanmic'
+ }
+ }, {
+ attributes: {
+ bandwidth: 256,
+ baseUrl: 'https://example.com/en.vtt',
+ clientOffset: 0,
+ id: 'en',
+ lang: 'en',
+ mimeType: 'text/vtt',
+ periodStart: 0,
+ role: {},
+ sourceDuration: 0,
+ type: 'dyanmic'
+ }
+ }];
+
+ assert.deepEqual(toPlaylists(representations), playlists);
+});
+
QUnit.test('segment base with sidx', function(assert) {
const representations = [{
attributes: {