Skip to content

Commit 0156528

Browse files
authored
feat: parse content steering info (#174)
1 parent 9ece4ae commit 0156528

11 files changed

+928
-69
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ const manifestUri = 'https://example.com/dash.xml';
4141
const res = await fetch(manifestUri);
4242
const manifest = await res.text();
4343

44-
var parsedManifest = mpdParser.parse(manifest, { manifestUri });
44+
// A callback function to handle events like errors or warnings
45+
const eventHandler = ({ type, message }) => console.log(`${type}: ${message}`);
46+
47+
var parsedManifest = mpdParser.parse(manifest, { manifestUri, eventHandler });
4548
```
4649

4750
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:
6366
```js
6467
Manifest {
6568
allowCache: boolean,
69+
contentSteering: {
70+
defaultServiceLocation: string,
71+
proxyServerURL: string,
72+
queryBeforeStart: boolean,
73+
serverURL: string
74+
},
6675
endList: boolean,
6776
mediaSequence: number,
6877
discontinuitySequence: number,

package-lock.json

Lines changed: 62 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"karma": "^5.2.3",
6868
"rollup": "^2.38.0",
6969
"rollup-plugin-string": "^3.0.0",
70-
"sinon": "^9.2.3",
70+
"sinon": "^11.1.1",
7171
"videojs-generate-karma-config": "^8.0.1",
7272
"videojs-generate-rollup-config": "~7.0.0",
7373
"videojs-generator-verify": "~3.0.2",

src/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export default {
22
INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
3+
INVALID_NUMBER_OF_CONTENT_STEERING: 'INVALID_NUMBER_OF_CONTENT_STEERING',
34
DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST',
45
DASH_INVALID_XML: 'DASH_INVALID_XML',
56
NO_BASE_URL: 'NO_BASE_URL',

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const parse = (manifestString, options = {}) => {
2828
return toM3u8({
2929
dashPlaylists: playlists,
3030
locations: parsedManifestInfo.locations,
31+
contentSteering: parsedManifestInfo.contentSteeringInfo,
3132
sidxMapping: options.sidxMapping,
3233
previousManifest: options.previousManifest,
3334
eventStream: parsedManifestInfo.eventStream

src/inheritAttributes.js

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,21 @@ const keySystemsMap = {
1616
/**
1717
* Builds a list of urls that is the product of the reference urls and BaseURL values
1818
*
19-
* @param {string[]} referenceUrls
20-
* List of reference urls to resolve to
19+
* @param {Object[]} references
20+
* List of objects containing the reference URL as well as its attributes
2121
* @param {Node[]} baseUrlElements
2222
* List of BaseURL nodes from the mpd
23-
* @return {string[]}
24-
* List of resolved urls
23+
* @return {Object[]}
24+
* List of objects with resolved urls and attributes
2525
*/
26-
export const buildBaseUrls = (referenceUrls, baseUrlElements) => {
26+
export const buildBaseUrls = (references, baseUrlElements) => {
2727
if (!baseUrlElements.length) {
28-
return referenceUrls;
28+
return references;
2929
}
3030

31-
return flatten(referenceUrls.map(function(reference) {
31+
return flatten(references.map(function(reference) {
3232
return baseUrlElements.map(function(baseUrlElement) {
33-
return resolveUrl(reference, getContent(baseUrlElement));
33+
return merge(parseAttributes(baseUrlElement), { baseUrl: resolveUrl(reference.baseUrl, getContent(baseUrlElement)) });
3434
});
3535
}));
3636
};
@@ -140,8 +140,9 @@ export const getSegmentInformation = (adaptationSet) => {
140140
*
141141
* @param {Object} adaptationSetAttributes
142142
* Contains attributes inherited by the AdaptationSet
143-
* @param {string[]} adaptationSetBaseUrls
144-
* Contains list of resolved base urls inherited by the AdaptationSet
143+
* @param {Object[]} adaptationSetBaseUrls
144+
* List of objects containing resolved base URLs and attributes
145+
* inherited by the AdaptationSet
145146
* @param {SegmentInformation} adaptationSetSegmentInfo
146147
* Contains Segment information for the AdaptationSet
147148
* @return {inheritBaseUrlsCallback}
@@ -158,7 +159,7 @@ export const inheritBaseUrls =
158159
return repBaseUrls.map(baseUrl => {
159160
return {
160161
segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
161-
attributes: merge(attributes, { baseUrl })
162+
attributes: merge(attributes, baseUrl)
162163
};
163164
});
164165
};
@@ -340,8 +341,9 @@ export const toEventStream = (period) => {
340341
*
341342
* @param {Object} periodAttributes
342343
* Contains attributes inherited by the Period
343-
* @param {string[]} periodBaseUrls
344-
* Contains list of resolved base urls inherited by the Period
344+
* @param {Object[]} periodBaseUrls
345+
* Contains list of objects with resolved base urls and attributes
346+
* inherited by the Period
345347
* @param {string[]} periodSegmentInfo
346348
* Contains Segment Information at the period level
347349
* @return {toRepresentationsCallback}
@@ -421,8 +423,9 @@ export const toRepresentations =
421423
*
422424
* @param {Object} mpdAttributes
423425
* Contains attributes inherited by the mpd
424-
* @param {string[]} mpdBaseUrls
425-
* Contains list of resolved base urls inherited by the mpd
426+
* @param {Object[]} mpdBaseUrls
427+
* Contains list of objects with resolved base urls and attributes
428+
* inherited by the mpd
426429
* @return {toAdaptationSetsCallback}
427430
* Callback map function
428431
*/
@@ -441,6 +444,41 @@ export const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index)
441444
return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo)));
442445
};
443446

447+
/**
448+
* Tranforms an array of content steering nodes into an object
449+
* containing CDN content steering information from the MPD manifest.
450+
*
451+
* For more information on the DASH spec for Content Steering parsing, see:
452+
* https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf
453+
*
454+
* @param {Node[]} contentSteeringNodes
455+
* Content steering nodes
456+
* @param {Function} eventHandler
457+
* The event handler passed into the parser options to handle warnings
458+
* @return {Object}
459+
* Object containing content steering data
460+
*/
461+
export const generateContentSteeringInformation = (contentSteeringNodes, eventHandler) => {
462+
// If there are more than one ContentSteering tags, throw an error
463+
if (contentSteeringNodes.length > 1) {
464+
eventHandler({ type: 'warn', message: 'The MPD manifest should contain no more than one ContentSteering tag' });
465+
}
466+
467+
// Return a null value if there are no ContentSteering tags
468+
if (!contentSteeringNodes.length) {
469+
return null;
470+
}
471+
472+
const infoFromContentSteeringTag =
473+
merge({serverURL: getContent(contentSteeringNodes[0])}, parseAttributes(contentSteeringNodes[0]));
474+
475+
// Converts `queryBeforeStart` to a boolean, as well as setting the default value
476+
// to `false` if it doesn't exist
477+
infoFromContentSteeringTag.queryBeforeStart = (infoFromContentSteeringTag.queryBeforeStart === 'true');
478+
479+
return infoFromContentSteeringTag;
480+
};
481+
444482
/**
445483
* Gets Period@start property for a given period.
446484
*
@@ -518,7 +556,14 @@ export const inheritAttributes = (mpd, options = {}) => {
518556
const {
519557
manifestUri = '',
520558
NOW = Date.now(),
521-
clientOffset = 0
559+
clientOffset = 0,
560+
// TODO: For now, we are expecting an eventHandler callback function
561+
// to be passed into the mpd parser as an option.
562+
// In the future, we should enable stream parsing by using the Stream class from vhs-utils.
563+
// This will support new features including a standardized event handler.
564+
// See the m3u8 parser for examples of how stream parsing is currently used for HLS parsing.
565+
// https://github.com/videojs/vhs-utils/blob/88d6e10c631e57a5af02c5a62bc7376cd456b4f5/src/stream.js#L9
566+
eventHandler = function() {}
522567
} = options;
523568
const periodNodes = findChildren(mpd, 'Period');
524569

@@ -530,6 +575,7 @@ export const inheritAttributes = (mpd, options = {}) => {
530575

531576
const mpdAttributes = parseAttributes(mpd);
532577
const mpdBaseUrls = buildBaseUrls([ manifestUri ], findChildren(mpd, 'BaseURL'));
578+
const contentSteeringNodes = findChildren(mpd, 'ContentSteering');
533579

534580
// See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'.
535581
mpdAttributes.type = mpdAttributes.type || 'static';
@@ -567,6 +613,14 @@ export const inheritAttributes = (mpd, options = {}) => {
567613

568614
return {
569615
locations: mpdAttributes.locations,
616+
contentSteeringInfo: generateContentSteeringInformation(contentSteeringNodes, eventHandler),
617+
// TODO: There are occurences where this `representationInfo` array contains undesired
618+
// duplicates. This generally occurs when there are multiple BaseURL nodes that are
619+
// direct children of the MPD node. When we attempt to resolve URLs from a combination of the
620+
// parent BaseURL and a child BaseURL, and the value does not resolve,
621+
// we end up returning the child BaseURL multiple times.
622+
// We need to determine a way to remove these duplicates in a safe way.
623+
// See: https://github.com/videojs/mpd-parser/pull/17#discussion_r162750527
570624
representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))),
571625
eventStream: flatten(periods.map(toEventStream))
572626
};

src/stringToMpdXml.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const stringToMpdXml = (manifestString) => {
1515
mpd = xml && xml.documentElement.tagName === 'MPD' ?
1616
xml.documentElement : null;
1717
} catch (e) {
18-
// ie 11 throwsw on invalid xml
18+
// ie 11 throws on invalid xml
1919
}
2020

2121
if (!mpd || mpd &&

src/toM3u8.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ export const flattenMediaGroupPlaylists = (mediaGroupObject) => {
392392
export const toM3u8 = ({
393393
dashPlaylists,
394394
locations,
395+
contentSteering,
395396
sidxMapping = {},
396397
previousManifest,
397398
eventStream
@@ -437,6 +438,10 @@ export const toM3u8 = ({
437438
manifest.locations = locations;
438439
}
439440

441+
if (contentSteering) {
442+
manifest.contentSteering = contentSteering;
443+
}
444+
440445
if (type === 'dynamic') {
441446
manifest.suggestedPresentationDelay = suggestedPresentationDelay;
442447
}

0 commit comments

Comments
 (0)