Skip to content

Commit

Permalink
feat: parse content steering info (#174)
Browse files Browse the repository at this point in the history
  • Loading branch information
wseymour15 authored Aug 15, 2023
1 parent 9ece4ae commit 0156528
Show file tree
Hide file tree
Showing 11 changed files with 928 additions and 69 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
93 changes: 62 additions & 31 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/errors.js
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 70 additions & 16 deletions src/inheritAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) });
});
}));
};
Expand Down Expand Up @@ -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}
Expand All @@ -158,7 +159,7 @@ export const inheritBaseUrls =
return repBaseUrls.map(baseUrl => {
return {
segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
attributes: merge(attributes, { baseUrl })
attributes: merge(attributes, baseUrl)
};
});
};
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
*/
Expand All @@ -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.
*
Expand Down Expand Up @@ -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');

Expand All @@ -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';
Expand Down Expand Up @@ -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))
};
Expand Down
2 changes: 1 addition & 1 deletion src/stringToMpdXml.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
5 changes: 5 additions & 0 deletions src/toM3u8.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ export const flattenMediaGroupPlaylists = (mediaGroupObject) => {
export const toM3u8 = ({
dashPlaylists,
locations,
contentSteering,
sidxMapping = {},
previousManifest,
eventStream
Expand Down Expand Up @@ -437,6 +438,10 @@ export const toM3u8 = ({
manifest.locations = locations;
}

if (contentSteering) {
manifest.contentSteering = contentSteering;
}

if (type === 'dynamic') {
manifest.suggestedPresentationDelay = suggestedPresentationDelay;
}
Expand Down
Loading

0 comments on commit 0156528

Please sign in to comment.