diff --git a/src/inheritAttributes.js b/src/inheritAttributes.js
index 12ab7606..9f160c79 100644
--- a/src/inheritAttributes.js
+++ b/src/inheritAttributes.js
@@ -5,75 +5,211 @@ import { findChildren, getContent } from './utils/xml';
import resolveUrl from './resolveUrl';
import errors from './errors';
-export const rep = mpdAttributes => (period, periodIndex) => {
- const adaptationSets = findChildren(period, 'AdaptationSet');
-
- const representationsByAdaptationSet = adaptationSets.map(adaptationSet => {
- const adaptationSetAttributes = getAttributes(adaptationSet);
-
- const role = findChildren(adaptationSet, 'Role')[0];
- const roleAttributes = { role: getAttributes(role) };
-
- const attrs = shallowMerge({ periodIndex },
- mpdAttributes,
- adaptationSetAttributes,
- roleAttributes);
-
- const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0];
- const segmentTimeline =
- segmentTemplate && findChildren(segmentTemplate, 'SegmentTimeline')[0];
- const segmentList = findChildren(adaptationSet, 'SegmentList')[0];
- const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0];
-
- const segmentInfo = {
- template: segmentTemplate && getAttributes(segmentTemplate),
- timeline: segmentTimeline &&
- findChildren(segmentTimeline, 'S').map(s => getAttributes(s)),
- list: segmentList && getAttributes(segmentList),
- base: segmentBase && getAttributes(segmentBase)
- };
+/**
+ * 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 {Node[]} baseUrlElements
+ * List of BaseURL nodes from the mpd
+ * @return {string[]}
+ * List of resolved urls
+ */
+export const buildBaseUrls = (referenceUrls, baseUrlElements) => {
+ if (!baseUrlElements.length) {
+ return referenceUrls;
+ }
- const representations = findChildren(adaptationSet, 'Representation');
+ return flatten(
+ referenceUrls.map(
+ reference => baseUrlElements.map(
+ baseUrlElement => resolveUrl(reference, getContent(baseUrlElement)))));
+};
- const inherit = representation => {
- // vtt tracks may use single file in BaseURL
- const baseUrlElement = findChildren(representation, 'BaseURL')[0];
- const baseUrl = baseUrlElement ? getContent(baseUrlElement) : '';
- const attributes = shallowMerge(attrs,
- getAttributes(representation),
- { url: baseUrl });
+/**
+ * Contains all Segment information for its containing AdaptationSet
+ *
+ * @typedef {Object} SegmentInformation
+ * @property {Object|undefined} template
+ * Contains the attributes for the SegmentTemplate node
+ * @property {Object[]|undefined} timeline
+ * Contains a list of atrributes for each S node within the SegmentTimeline node
+ * @property {Object|undefined} list
+ * Contains the attributes for the SegmentList node
+ * @property {Object|undefined} base
+ * Contains the attributes for the SegmentBase node
+ */
+
+/**
+ * Returns all available Segment information contained within the AdaptationSet node
+ *
+ * @param {Node} adaptationSet
+ * The AdaptationSet node to get Segment information from
+ * @return {SegmentInformation}
+ * The Segment information contained within the provided AdaptationSet
+ */
+export const getSegmentInformation = (adaptationSet) => {
+ const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0];
+ const segmentTimeline =
+ segmentTemplate && findChildren(segmentTemplate, 'SegmentTimeline')[0];
+ const segmentList = findChildren(adaptationSet, 'SegmentList')[0];
+ const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0];
+
+ return {
+ template: segmentTemplate && getAttributes(segmentTemplate),
+ timeline: segmentTimeline &&
+ findChildren(segmentTimeline, 'S').map(s => getAttributes(s)),
+ list: segmentList && getAttributes(segmentList),
+ base: segmentBase && getAttributes(segmentBase)
+ };
+};
- return { attributes, segmentInfo };
+/**
+ * Contains Segment information and attributes needed to construct a Playlist object
+ * from a Representation
+ *
+ * @typedef {Object} RepresentationInformation
+ * @property {SegmentInformation} segmentInfo
+ * Segment information for this Representation
+ * @property {Object} attributes
+ * Inherited attributes for this Representation
+ */
+
+/**
+ * Maps a Representation node to an object containing Segment information and attributes
+ *
+ * @name inheritBaseUrlsCallback
+ * @function
+ * @param {Node} representation
+ * Representation node from the mpd
+ * @return {RepresentationInformation}
+ * Representation information needed to construct a Playlist object
+ */
+
+/**
+ * Returns a callback for Array.prototype.map for mapping Representation nodes to
+ * Segment information and attributes using inherited BaseURL nodes.
+ *
+ * @param {Object} adaptationSetAttributes
+ * Contains attributes inherited by the AdaptationSet
+ * @param {string[]} adaptationSetBaseUrls
+ * Contains list of resolved base urls inherited by the AdaptationSet
+ * @param {SegmentInformation} segmentInfo
+ * Contains Segment information for the AdaptationSet
+ * @return {inheritBaseUrlsCallback}
+ * Callback map function
+ */
+export const inheritBaseUrls =
+(adaptationSetAttributes, adaptationSetBaseUrls, segmentInfo) => (representation) => {
+ const repBaseUrlElements = findChildren(representation, 'BaseURL');
+ const repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements);
+ const attributes = shallowMerge(adaptationSetAttributes, getAttributes(representation));
+
+ return repBaseUrls.map(baseUrl => {
+ return {
+ segmentInfo,
+ attributes: shallowMerge(attributes, { baseUrl })
};
-
- return representations.map(inherit);
});
+};
- return flatten(representationsByAdaptationSet);
+/**
+ * Maps an AdaptationSet node to a list of Representation information objects
+ *
+ * @name toRepresentationsCallback
+ * @function
+ * @param {Node} adaptationSet
+ * AdaptationSet node from the mpd
+ * @return {RepresentationInformation[]}
+ * List of objects containing Representaion information
+ */
+
+/**
+ * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of
+ * Representation information objects
+ *
+ * @param {Object} periodAttributes
+ * Contains attributes inherited by the Period
+ * @param {string[]} periodBaseUrls
+ * Contains list of resolved base urls inherited by the Period
+ * @return {toRepresentationsCallback}
+ * Callback map function
+ */
+export const toRepresentations =
+(periodAttributes, periodBaseUrls) => (adaptationSet) => {
+ const adaptationSetAttributes = getAttributes(adaptationSet);
+ const adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls,
+ findChildren(adaptationSet, 'BaseURL'));
+ const role = findChildren(adaptationSet, 'Role')[0];
+ const roleAttributes = { role: getAttributes(role) };
+ const attrs = shallowMerge(periodAttributes,
+ adaptationSetAttributes,
+ roleAttributes);
+ const segmentInfo = getSegmentInformation(adaptationSet);
+ const representations = findChildren(adaptationSet, 'Representation');
+
+ return flatten(
+ representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, segmentInfo)));
};
-export const representationsByPeriod = (periods, mpdAttributes) => {
- return periods.map(rep(mpdAttributes));
+/**
+ * Maps an Period node to a list of Representation inforamtion objects for all
+ * AdaptationSet nodes contained within the Period
+ *
+ * @name toAdaptationSetsCallback
+ * @function
+ * @param {Node} period
+ * Period node from the mpd
+ * @param {number} periodIndex
+ * Index of the Period within the mpd
+ * @return {RepresentationInformation[]}
+ * List of objects containing Representaion information
+ */
+
+/**
+ * Returns a callback for Array.prototype.map for mapping Period nodes to a list of
+ * Representation information objects
+ *
+ * @param {Object} mpdAttributes
+ * Contains attributes inherited by the mpd
+ * @param {string[]} mpdBaseUrls
+ * Contains list of resolved base urls inherited by the mpd
+ * @return {toAdaptationSetsCallback}
+ * Callback map function
+ */
+export const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, periodIndex) => {
+ const periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period, 'BaseURL'));
+ const periodAttributes = shallowMerge({ periodIndex }, mpdAttributes);
+ const adaptationSets = findChildren(period, 'AdaptationSet');
+
+ return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls)));
};
+/**
+ * Traverses the mpd xml tree to generate a list of Representation information objects
+ * that have inherited attributes from parent nodes
+ *
+ * @param {Node} mpd
+ * The root node of the mpd
+ * @param {string} manifestUri
+ * The uri of the source mpd
+ * @return {RepresentationInformation[]}
+ * List of objects containing Representation information
+ */
export const inheritAttributes = (mpd, manifestUri = '') => {
const periods = findChildren(mpd, 'Period');
- if (!periods.length ||
- periods.length &&
- periods.length !== 1) {
+ if (periods.length !== 1) {
// TODO add support for multiperiod
throw new Error(errors.INVALID_NUMBER_OF_PERIOD);
}
const mpdAttributes = getAttributes(mpd);
- const baseUrlElement = findChildren(mpd, 'BaseURL')[0];
- const baseUrl = baseUrlElement ? getContent(baseUrlElement) : '';
+ const mpdBaseUrls = buildBaseUrls([ manifestUri ], findChildren(mpd, 'BaseURL'));
- mpdAttributes.baseUrl = resolveUrl(manifestUri, baseUrl);
mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration ?
parseDuration(mpdAttributes.mediaPresentationDuration) : 0;
- return flatten(representationsByPeriod(periods, mpdAttributes));
+ return flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls)));
};
diff --git a/src/toM3u8.js b/src/toM3u8.js
index b94bd696..e5ed41bc 100644
--- a/src/toM3u8.js
+++ b/src/toM3u8.js
@@ -16,10 +16,11 @@ export const formatAudioPlaylist = ({ attributes, segments }) => {
export const formatVttPlaylist = ({ attributes, segments }) => {
if (typeof segments === 'undefined') {
+ // vtt tracks may use single file in BaseURL
segments = [{
- uri: attributes.url,
+ uri: attributes.baseUrl,
timeline: attributes.periodIndex,
- resolvedUri: attributes.url || '',
+ resolvedUri: attributes.baseUrl || '',
duration: attributes.sourceDuration
}];
}
@@ -32,7 +33,7 @@ export const formatVttPlaylist = ({ attributes, segments }) => {
uri: '',
endList: true,
timeline: attributes.periodIndex,
- resolvedUri: attributes.url || '',
+ resolvedUri: attributes.baseUrl || '',
segments
};
};
diff --git a/test/inheritAttributes.test.js b/test/inheritAttributes.test.js
index f0cc3f3d..888b3a87 100644
--- a/test/inheritAttributes.test.js
+++ b/test/inheritAttributes.test.js
@@ -1,8 +1,174 @@
-import { inheritAttributes } from '../src/inheritAttributes';
+import {
+ inheritAttributes,
+ buildBaseUrls,
+ getSegmentInformation
+} from '../src/inheritAttributes';
import { stringToMpdXml } from '../src/stringToMpdXml';
import errors from '../src/errors';
import QUnit from 'qunit';
+QUnit.module('buildBaseUrls');
+
+QUnit.test('returns reference urls when no BaseURL nodes', function(assert) {
+ const reference = ['https://example.com/', '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 node = [{ textContent: 'bar/' }];
+ const expected = ['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 node = [{ textContent: 'bar/' }];
+ const expected = ['https://example.com/bar/', 'https://foo.com/bar/'];
+
+ assert.deepEqual(buildBaseUrls(reference, node), expected,
+ 'base url for each reference url');
+});
+
+QUnit.test('multiple BaseURL nodes with single reference url', function(assert) {
+ const reference = ['https://example.com/'];
+ const nodes = [{ textContent: 'bar/' }, { textContent: 'baz/' }];
+ const expected = ['https://example.com/bar/', '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 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/'
+ ];
+
+ assert.deepEqual(buildBaseUrls(reference, nodes), expected, 'creates all base urls');
+});
+
+QUnit.test('absolute BaseURL overwrites reference', function(assert) {
+ const reference = ['https://example.com'];
+ const node = [{ textContent: 'https://foo.com/bar/' }];
+ const expected = ['https://foo.com/bar/'];
+
+ assert.deepEqual(buildBaseUrls(reference, node), expected,
+ 'absolute url overwrites reference');
+});
+
+QUnit.module('getSegmentInformation');
+
+QUnit.test('undefined Segment information when no Segment nodes', function(assert) {
+ const adaptationSet = { childNodes: [] };
+ const expected = {
+ template: void 0,
+ timeline: void 0,
+ list: void 0,
+ base: void 0
+ };
+
+ assert.deepEqual(getSegmentInformation(adaptationSet), expected,
+ 'undefined segment info');
+});
+
+QUnit.test('gets SegmentTemplate attributes', function(assert) {
+ const adaptationSet = {
+ childNodes: [{
+ tagName: 'SegmentTemplate',
+ attributes: [{ name: 'media', value: 'video.mp4' }],
+ childNodes: []
+ }]
+ };
+ const expected = {
+ template: { media: 'video.mp4' },
+ timeline: void 0,
+ list: void 0,
+ base: void 0
+ };
+
+ assert.deepEqual(getSegmentInformation(adaptationSet), expected,
+ 'SegmentTemplate info');
+});
+
+QUnit.test('gets SegmentList attributes', function(assert) {
+ const adaptationSet = {
+ childNodes: [{
+ tagName: 'SegmentList',
+ attributes: [{ name: 'duration', value: '10' }]
+ }]
+ };
+ const expected = {
+ template: void 0,
+ timeline: void 0,
+ list: { duration: '10' },
+ base: void 0
+ };
+
+ assert.deepEqual(getSegmentInformation(adaptationSet), expected,
+ 'SegmentList info');
+});
+
+QUnit.test('gets SegmentBase attributes', function(assert) {
+ const adaptationSet = {
+ childNodes: [{
+ tagName: 'SegmentBase',
+ attributes: [{ name: 'duration', value: '10' }]
+ }]
+ };
+ const expected = {
+ template: void 0,
+ timeline: void 0,
+ list: void 0,
+ base: { duration: '10' }
+ };
+
+ assert.deepEqual(getSegmentInformation(adaptationSet), expected,
+ 'SegmentBase info');
+});
+
+QUnit.test('gets SegmentTemplate and SegmentTimeline attributes', function(assert) {
+ const adaptationSet = {
+ childNodes: [{
+ tagName: 'SegmentTemplate',
+ attributes: [{ name: 'media', value: 'video.mp4' }],
+ childNodes: [{
+ tagName: 'SegmentTimeline',
+ childNodes: [{
+ tagName: 'S',
+ attributes: [{ name: 'd', value: '10' }]
+ }, {
+ tagName: 'S',
+ attributes: [{ name: 'd', value: '5' }]
+ }, {
+ tagName: 'S',
+ attributes: [{ name: 'd', value: '7' }]
+ }]
+ }]
+ }]
+ };
+ const expected = {
+ template: { media: 'video.mp4' },
+ timeline: [{ d: '10' }, { d: '5' }, { d: '7' }],
+ list: void 0,
+ base: void 0
+ };
+
+ assert.deepEqual(getSegmentInformation(adaptationSet), expected,
+ 'SegmentTemplate and SegmentTimeline info');
+});
+
QUnit.module('inheritAttributes');
QUnit.test('needs at least one Period', function(assert) {
@@ -10,13 +176,13 @@ QUnit.test('needs at least one Period', function(assert) {
new RegExp(errors.INVALID_NUMBER_OF_PERIOD));
});
-QUnit.test('end to end', function(assert) {
+QUnit.test('end to end - basic', function(assert) {
const actual = inheritAttributes(stringToMpdXml(
`
- https://www.example.com/base
+ https://www.example.com/base/
-
+
+ https://www.example.com/base/
+
+ foo/
+
+ bar/
+
+
+
+ buzz/
+
+
+
+
+ https://example.com/en.vtt
+
+
+
+
+ `
+ ));
+
+ const expected = [{
+ attributes: {
+ bandwidth: '5000000',
+ baseUrl: 'https://www.example.com/base/foo/bar/buzz/',
+ codecs: 'avc1.64001e',
+ height: '404',
+ id: 'test',
+ mediaPresentationDuration: 'PT30S',
+ mimeType: 'video/mp4',
+ periodIndex: 0,
+ role: {
+ value: 'main'
+ },
sourceDuration: 30,
- url: 'https://example.com/en.vtt'
+ width: '720'
+ },
+ segmentInfo: {
+ base: undefined,
+ list: undefined,
+ template: {},
+ timeline: undefined
+ }
+ }, {
+ attributes: {
+ bandwidth: '256',
+ baseUrl: 'https://example.com/en.vtt',
+ id: 'en',
+ lang: 'en',
+ mediaPresentationDuration: 'PT30S',
+ mimeType: 'text/vtt',
+ periodIndex: 0,
+ role: {},
+ sourceDuration: 30
},
segmentInfo: {
base: undefined,
@@ -84,3 +324,163 @@ QUnit.test('end to end', function(assert) {
assert.equal(actual.length, 2);
assert.deepEqual(actual, expected);
});
+
+QUnit.test('end to end - alternate BaseURLs', function(assert) {
+ const actual = inheritAttributes(stringToMpdXml(
+ `
+
+ https://www.example.com/base/
+ https://www.test.com/base/
+
+
+ segments/
+ media/
+
+
+
+
+
+
+
+ https://example.com/en.vtt
+
+
+
+
+ `
+ ));
+
+ const expected = [{
+ attributes: {
+ bandwidth: '5000000',
+ baseUrl: 'https://www.example.com/base/segments/',
+ codecs: 'avc1.64001e',
+ height: '404',
+ id: 'test',
+ mediaPresentationDuration: 'PT30S',
+ mimeType: 'video/mp4',
+ periodIndex: 0,
+ role: {
+ value: 'main'
+ },
+ sourceDuration: 30,
+ width: '720'
+ },
+ segmentInfo: {
+ base: undefined,
+ list: undefined,
+ template: {},
+ timeline: undefined
+ }
+ }, {
+ attributes: {
+ bandwidth: '5000000',
+ baseUrl: 'https://www.example.com/base/media/',
+ codecs: 'avc1.64001e',
+ height: '404',
+ id: 'test',
+ mediaPresentationDuration: 'PT30S',
+ mimeType: 'video/mp4',
+ periodIndex: 0,
+ role: {
+ value: 'main'
+ },
+ sourceDuration: 30,
+ width: '720'
+ },
+ segmentInfo: {
+ base: undefined,
+ list: undefined,
+ template: {},
+ timeline: undefined
+ }
+ }, {
+ attributes: {
+ bandwidth: '5000000',
+ baseUrl: 'https://www.test.com/base/segments/',
+ codecs: 'avc1.64001e',
+ height: '404',
+ id: 'test',
+ mediaPresentationDuration: 'PT30S',
+ mimeType: 'video/mp4',
+ periodIndex: 0,
+ role: {
+ value: 'main'
+ },
+ sourceDuration: 30,
+ width: '720'
+ },
+ segmentInfo: {
+ base: undefined,
+ list: undefined,
+ template: {},
+ timeline: undefined
+ }
+ }, {
+ attributes: {
+ bandwidth: '5000000',
+ baseUrl: 'https://www.test.com/base/media/',
+ codecs: 'avc1.64001e',
+ height: '404',
+ id: 'test',
+ mediaPresentationDuration: 'PT30S',
+ mimeType: 'video/mp4',
+ periodIndex: 0,
+ role: {
+ value: 'main'
+ },
+ sourceDuration: 30,
+ width: '720'
+ },
+ segmentInfo: {
+ base: undefined,
+ list: undefined,
+ template: {},
+ timeline: undefined
+ }
+ }, {
+ attributes: {
+ bandwidth: '256',
+ baseUrl: 'https://example.com/en.vtt',
+ id: 'en',
+ lang: 'en',
+ mediaPresentationDuration: 'PT30S',
+ mimeType: 'text/vtt',
+ periodIndex: 0,
+ role: {},
+ sourceDuration: 30
+ },
+ segmentInfo: {
+ base: undefined,
+ list: undefined,
+ template: undefined,
+ timeline: undefined
+ }
+ }, {
+ attributes: {
+ bandwidth: '256',
+ baseUrl: 'https://example.com/en.vtt',
+ id: 'en',
+ lang: 'en',
+ mediaPresentationDuration: 'PT30S',
+ mimeType: 'text/vtt',
+ periodIndex: 0,
+ role: {},
+ sourceDuration: 30
+ },
+ segmentInfo: {
+ base: undefined,
+ list: undefined,
+ template: undefined,
+ timeline: undefined
+ }
+ }];
+
+ assert.equal(actual.length, 6);
+ assert.deepEqual(actual, expected);
+});
diff --git a/test/toM3u8.test.js b/test/toM3u8.test.js
index fb5faee6..097afa79 100644
--- a/test/toM3u8.test.js
+++ b/test/toM3u8.test.js
@@ -43,7 +43,7 @@ QUnit.test('playlists', function(assert) {
bandwidth: '20000',
periodIndex: 1,
mimeType: 'text/vtt',
- url: 'https://www.example.com/vtt'
+ baseUrl: 'https://www.example.com/vtt'
}
}, {
attributes: {
@@ -52,7 +52,7 @@ QUnit.test('playlists', function(assert) {
bandwidth: '10000',
periodIndex: 1,
mimeType: 'text/vtt',
- url: 'https://www.example.com/vtt'
+ baseUrl: 'https://www.example.com/vtt'
}
}];
@@ -235,7 +235,7 @@ QUnit.test('playlists with segments', function(assert) {
bandwidth: '20000',
periodIndex: 1,
mimeType: 'text/vtt',
- url: 'https://www.example.com/vtt'
+ baseUrl: 'https://www.example.com/vtt'
},
segments: [{
uri: '',
@@ -263,7 +263,7 @@ QUnit.test('playlists with segments', function(assert) {
bandwidth: '10000',
periodIndex: 1,
mimeType: 'text/vtt',
- url: 'https://www.example.com/vtt'
+ baseUrl: 'https://www.example.com/vtt'
},
segments: [{
uri: '',