1
0
Fork 0
mirror of https://github.com/DanielnetoDotCom/YouPHPTube synced 2025-10-04 02:09:22 +02:00
Daniel Neto 2023-10-25 10:14:46 -03:00
parent b6d47e94c8
commit 65f15c7e46
2882 changed files with 382239 additions and 10785 deletions

25
node_modules/mpd-parser/CHANGELOG.md generated vendored
View file

@ -1,3 +1,28 @@
<a name="1.2.2"></a>
## [1.2.2](https://github.com/videojs/mpd-parser/compare/v1.2.1...v1.2.2) (2023-08-30)
### Bug Fixes
* improve serviceLocation for content steering ([#177](https://github.com/videojs/mpd-parser/issues/177)) ([fee1870](https://github.com/videojs/mpd-parser/commit/fee1870))
<a name="1.2.1"></a>
## [1.2.1](https://github.com/videojs/mpd-parser/compare/v1.2.0...v1.2.1) (2023-08-16)
### Bug Fixes
* use manifest uri in base urls ([#176](https://github.com/videojs/mpd-parser/issues/176)) ([5faf232](https://github.com/videojs/mpd-parser/commit/5faf232))
<a name="1.2.0"></a>
# [1.2.0](https://github.com/videojs/mpd-parser/compare/v1.1.1...v1.2.0) (2023-08-15)
### Features
* parse content steering info ([#174](https://github.com/videojs/mpd-parser/issues/174)) ([0156528](https://github.com/videojs/mpd-parser/commit/0156528))
### Chores
* Update CI and release workflows ([#173](https://github.com/videojs/mpd-parser/issues/173)) ([9ece4ae](https://github.com/videojs/mpd-parser/commit/9ece4ae))
<a name="1.1.1"></a>
## [1.1.1](https://github.com/videojs/mpd-parser/compare/v1.1.0...v1.1.1) (2023-03-31)

11
node_modules/mpd-parser/README.md generated vendored
View file

@ -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,

View file

@ -1,4 +1,4 @@
/*! @name mpd-parser @version 1.1.1 @license Apache-2.0 */
/*! @name mpd-parser @version 1.2.2 @license Apache-2.0 */
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
@ -15,7 +15,7 @@ var resolveUrl__default = /*#__PURE__*/_interopDefaultLegacy(resolveUrl);
var window__default = /*#__PURE__*/_interopDefaultLegacy(window);
var decodeB64ToUint8Array__default = /*#__PURE__*/_interopDefaultLegacy(decodeB64ToUint8Array);
var version = "1.1.1";
var version = "1.2.2";
const isObject = obj => {
return !!obj && typeof obj === 'object';
@ -91,6 +91,7 @@ const union = (lists, keyFunction) => {
var errors = {
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',
@ -757,43 +758,56 @@ const positionManifestOnTimeline = ({
const generateSidxKey = sidx => sidx && sidx.uri + '-' + byteRangeToString(sidx.byterange);
const mergeDiscontiguousPlaylists = playlists => {
const mergedPlaylists = values(playlists.reduce((acc, playlist) => {
// assuming playlist IDs are the same across periods
// TODO: handle multiperiod where representation sets are not the same
// across periods
const name = playlist.attributes.id + (playlist.attributes.lang || '');
if (!acc[name]) {
// First Period
acc[name] = playlist;
acc[name].attributes.timelineStarts = [];
} else {
// Subsequent Periods
if (playlist.segments) {
// first segment of subsequent periods signal a discontinuity
if (playlist.segments[0]) {
playlist.segments[0].discontinuity = true;
}
acc[name].segments.push(...playlist.segments);
} // bubble up contentProtection, this assumes all DRM content
// has the same contentProtection
if (playlist.attributes.contentProtection) {
acc[name].attributes.contentProtection = playlist.attributes.contentProtection;
}
// Break out playlists into groups based on their baseUrl
const playlistsByBaseUrl = playlists.reduce(function (acc, cur) {
if (!acc[cur.attributes.baseUrl]) {
acc[cur.attributes.baseUrl] = [];
}
acc[name].attributes.timelineStarts.push({
// Although they represent the same number, it's important to have both to make it
// compatible with HLS potentially having a similar attribute.
start: playlist.attributes.periodStart,
timeline: playlist.attributes.periodStart
});
acc[cur.attributes.baseUrl].push(cur);
return acc;
}, {}));
return mergedPlaylists.map(playlist => {
}, {});
let allPlaylists = [];
Object.values(playlistsByBaseUrl).forEach(playlistGroup => {
const mergedPlaylists = values(playlistGroup.reduce((acc, playlist) => {
// assuming playlist IDs are the same across periods
// TODO: handle multiperiod where representation sets are not the same
// across periods
const name = playlist.attributes.id + (playlist.attributes.lang || '');
if (!acc[name]) {
// First Period
acc[name] = playlist;
acc[name].attributes.timelineStarts = [];
} else {
// Subsequent Periods
if (playlist.segments) {
// first segment of subsequent periods signal a discontinuity
if (playlist.segments[0]) {
playlist.segments[0].discontinuity = true;
}
acc[name].segments.push(...playlist.segments);
} // bubble up contentProtection, this assumes all DRM content
// has the same contentProtection
if (playlist.attributes.contentProtection) {
acc[name].attributes.contentProtection = playlist.attributes.contentProtection;
}
}
acc[name].attributes.timelineStarts.push({
// Although they represent the same number, it's important to have both to make it
// compatible with HLS potentially having a similar attribute.
start: playlist.attributes.periodStart,
timeline: playlist.attributes.periodStart
});
return acc;
}, {}));
allPlaylists = allPlaylists.concat(mergedPlaylists);
});
return allPlaylists.map(playlist => {
playlist.discontinuityStarts = findIndexes(playlist.segments || [], 'discontinuity');
return playlist;
});
@ -838,7 +852,7 @@ const formatAudioPlaylist = ({
uri: '',
endList: attributes.type === 'static',
timeline: attributes.periodStart,
resolvedUri: '',
resolvedUri: attributes.baseUrl || '',
targetDuration: attributes.duration,
discontinuitySequence,
discontinuityStarts,
@ -851,6 +865,10 @@ const formatAudioPlaylist = ({
playlist.contentProtection = attributes.contentProtection;
}
if (attributes.serviceLocation) {
playlist.attributes.serviceLocation = attributes.serviceLocation;
}
if (sidx) {
playlist.sidx = sidx;
}
@ -892,7 +910,7 @@ const formatVttPlaylist = ({
m3u8Attributes.CODECS = attributes.codecs;
}
return {
const vttPlaylist = {
attributes: m3u8Attributes,
uri: '',
endList: attributes.type === 'static',
@ -905,6 +923,12 @@ const formatVttPlaylist = ({
mediaSequence,
segments
};
if (attributes.serviceLocation) {
vttPlaylist.attributes.serviceLocation = attributes.serviceLocation;
}
return vttPlaylist;
};
const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => {
let mainPlaylist;
@ -1019,7 +1043,7 @@ const formatVideoPlaylist = ({
uri: '',
endList: attributes.type === 'static',
timeline: attributes.periodStart,
resolvedUri: '',
resolvedUri: attributes.baseUrl || '',
targetDuration: attributes.duration,
discontinuityStarts,
timelineStarts: attributes.timelineStarts,
@ -1034,6 +1058,10 @@ const formatVideoPlaylist = ({
playlist.contentProtection = attributes.contentProtection;
}
if (attributes.serviceLocation) {
playlist.attributes.serviceLocation = attributes.serviceLocation;
}
if (sidx) {
playlist.sidx = sidx;
}
@ -1126,6 +1154,7 @@ const flattenMediaGroupPlaylists = mediaGroupObject => {
const toM3u8 = ({
dashPlaylists,
locations,
contentSteering,
sidxMapping = {},
previousManifest,
eventStream
@ -1169,6 +1198,10 @@ const toM3u8 = ({
manifest.locations = locations;
}
if (contentSteering) {
manifest.contentSteering = contentSteering;
}
if (type === 'dynamic') {
manifest.suggestedPresentationDelay = suggestedPresentationDelay;
}
@ -2003,22 +2036,33 @@ 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
*/
const buildBaseUrls = (referenceUrls, baseUrlElements) => {
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__default['default'](reference, getContent(baseUrlElement));
const initialBaseUrl = getContent(baseUrlElement);
const resolvedBaseUrl = resolveUrl__default['default'](reference.baseUrl, initialBaseUrl);
const finalBaseUrl = merge(parseAttributes(baseUrlElement), {
baseUrl: resolvedBaseUrl
}); // If the URL is resolved, we want to get the serviceLocation from the reference
// assuming there is no serviceLocation on the initialBaseUrl
if (resolvedBaseUrl !== initialBaseUrl && !finalBaseUrl.serviceLocation && reference.serviceLocation) {
finalBaseUrl.serviceLocation = reference.serviceLocation;
}
return finalBaseUrl;
});
}));
};
@ -2120,8 +2164,9 @@ 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}
@ -2136,9 +2181,7 @@ const inheritBaseUrls = (adaptationSetAttributes, adaptationSetBaseUrls, adaptat
return repBaseUrls.map(baseUrl => {
return {
segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
attributes: merge(attributes, {
baseUrl
})
attributes: merge(attributes, baseUrl)
};
});
};
@ -2305,8 +2348,9 @@ 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}
@ -2382,8 +2426,9 @@ const toRepresentations = (periodAttributes, periodBaseUrls, periodSegmentInfo)
*
* @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
*/
@ -2402,6 +2447,43 @@ const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index) => {
const periodSegmentInfo = getSegmentInformation(period.node);
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
*/
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.
*
@ -2481,7 +2563,14 @@ 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');
@ -2491,7 +2580,10 @@ const inheritAttributes = (mpd, options = {}) => {
const locations = findChildren(mpd, 'Location');
const mpdAttributes = parseAttributes(mpd);
const mpdBaseUrls = buildBaseUrls([manifestUri], findChildren(mpd, 'BaseURL')); // See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'.
const mpdBaseUrls = buildBaseUrls([{
baseUrl: 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';
mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0;
@ -2524,6 +2616,14 @@ 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))
};
@ -2541,7 +2641,7 @@ const stringToMpdXml = manifestString => {
try {
xml = parser.parseFromString(manifestString, 'application/xml');
mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null;
} catch (e) {// ie 11 throwsw on invalid xml
} catch (e) {// ie 11 throws on invalid xml
}
if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) {
@ -2619,6 +2719,7 @@ const parse = (manifestString, options = {}) => {
return toM3u8({
dashPlaylists: playlists,
locations: parsedManifestInfo.locations,
contentSteering: parsedManifestInfo.contentSteeringInfo,
sidxMapping: options.sidxMapping,
previousManifest: options.previousManifest,
eventStream: parsedManifestInfo.eventStream

View file

@ -1,11 +1,11 @@
/*! @name mpd-parser @version 1.1.1 @license Apache-2.0 */
/*! @name mpd-parser @version 1.2.2 @license Apache-2.0 */
import resolveUrl from '@videojs/vhs-utils/es/resolve-url';
import window from 'global/window';
import { forEachMediaGroup } from '@videojs/vhs-utils/es/media-groups';
import decodeB64ToUint8Array from '@videojs/vhs-utils/es/decode-b64-to-uint8-array';
import { DOMParser } from '@xmldom/xmldom';
var version = "1.1.1";
var version = "1.2.2";
const isObject = obj => {
return !!obj && typeof obj === 'object';
@ -81,6 +81,7 @@ const union = (lists, keyFunction) => {
var errors = {
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',
@ -747,43 +748,56 @@ const positionManifestOnTimeline = ({
const generateSidxKey = sidx => sidx && sidx.uri + '-' + byteRangeToString(sidx.byterange);
const mergeDiscontiguousPlaylists = playlists => {
const mergedPlaylists = values(playlists.reduce((acc, playlist) => {
// assuming playlist IDs are the same across periods
// TODO: handle multiperiod where representation sets are not the same
// across periods
const name = playlist.attributes.id + (playlist.attributes.lang || '');
if (!acc[name]) {
// First Period
acc[name] = playlist;
acc[name].attributes.timelineStarts = [];
} else {
// Subsequent Periods
if (playlist.segments) {
// first segment of subsequent periods signal a discontinuity
if (playlist.segments[0]) {
playlist.segments[0].discontinuity = true;
}
acc[name].segments.push(...playlist.segments);
} // bubble up contentProtection, this assumes all DRM content
// has the same contentProtection
if (playlist.attributes.contentProtection) {
acc[name].attributes.contentProtection = playlist.attributes.contentProtection;
}
// Break out playlists into groups based on their baseUrl
const playlistsByBaseUrl = playlists.reduce(function (acc, cur) {
if (!acc[cur.attributes.baseUrl]) {
acc[cur.attributes.baseUrl] = [];
}
acc[name].attributes.timelineStarts.push({
// Although they represent the same number, it's important to have both to make it
// compatible with HLS potentially having a similar attribute.
start: playlist.attributes.periodStart,
timeline: playlist.attributes.periodStart
});
acc[cur.attributes.baseUrl].push(cur);
return acc;
}, {}));
return mergedPlaylists.map(playlist => {
}, {});
let allPlaylists = [];
Object.values(playlistsByBaseUrl).forEach(playlistGroup => {
const mergedPlaylists = values(playlistGroup.reduce((acc, playlist) => {
// assuming playlist IDs are the same across periods
// TODO: handle multiperiod where representation sets are not the same
// across periods
const name = playlist.attributes.id + (playlist.attributes.lang || '');
if (!acc[name]) {
// First Period
acc[name] = playlist;
acc[name].attributes.timelineStarts = [];
} else {
// Subsequent Periods
if (playlist.segments) {
// first segment of subsequent periods signal a discontinuity
if (playlist.segments[0]) {
playlist.segments[0].discontinuity = true;
}
acc[name].segments.push(...playlist.segments);
} // bubble up contentProtection, this assumes all DRM content
// has the same contentProtection
if (playlist.attributes.contentProtection) {
acc[name].attributes.contentProtection = playlist.attributes.contentProtection;
}
}
acc[name].attributes.timelineStarts.push({
// Although they represent the same number, it's important to have both to make it
// compatible with HLS potentially having a similar attribute.
start: playlist.attributes.periodStart,
timeline: playlist.attributes.periodStart
});
return acc;
}, {}));
allPlaylists = allPlaylists.concat(mergedPlaylists);
});
return allPlaylists.map(playlist => {
playlist.discontinuityStarts = findIndexes(playlist.segments || [], 'discontinuity');
return playlist;
});
@ -828,7 +842,7 @@ const formatAudioPlaylist = ({
uri: '',
endList: attributes.type === 'static',
timeline: attributes.periodStart,
resolvedUri: '',
resolvedUri: attributes.baseUrl || '',
targetDuration: attributes.duration,
discontinuitySequence,
discontinuityStarts,
@ -841,6 +855,10 @@ const formatAudioPlaylist = ({
playlist.contentProtection = attributes.contentProtection;
}
if (attributes.serviceLocation) {
playlist.attributes.serviceLocation = attributes.serviceLocation;
}
if (sidx) {
playlist.sidx = sidx;
}
@ -882,7 +900,7 @@ const formatVttPlaylist = ({
m3u8Attributes.CODECS = attributes.codecs;
}
return {
const vttPlaylist = {
attributes: m3u8Attributes,
uri: '',
endList: attributes.type === 'static',
@ -895,6 +913,12 @@ const formatVttPlaylist = ({
mediaSequence,
segments
};
if (attributes.serviceLocation) {
vttPlaylist.attributes.serviceLocation = attributes.serviceLocation;
}
return vttPlaylist;
};
const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => {
let mainPlaylist;
@ -1009,7 +1033,7 @@ const formatVideoPlaylist = ({
uri: '',
endList: attributes.type === 'static',
timeline: attributes.periodStart,
resolvedUri: '',
resolvedUri: attributes.baseUrl || '',
targetDuration: attributes.duration,
discontinuityStarts,
timelineStarts: attributes.timelineStarts,
@ -1024,6 +1048,10 @@ const formatVideoPlaylist = ({
playlist.contentProtection = attributes.contentProtection;
}
if (attributes.serviceLocation) {
playlist.attributes.serviceLocation = attributes.serviceLocation;
}
if (sidx) {
playlist.sidx = sidx;
}
@ -1116,6 +1144,7 @@ const flattenMediaGroupPlaylists = mediaGroupObject => {
const toM3u8 = ({
dashPlaylists,
locations,
contentSteering,
sidxMapping = {},
previousManifest,
eventStream
@ -1159,6 +1188,10 @@ const toM3u8 = ({
manifest.locations = locations;
}
if (contentSteering) {
manifest.contentSteering = contentSteering;
}
if (type === 'dynamic') {
manifest.suggestedPresentationDelay = suggestedPresentationDelay;
}
@ -1993,22 +2026,33 @@ 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
*/
const buildBaseUrls = (referenceUrls, baseUrlElements) => {
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));
const initialBaseUrl = getContent(baseUrlElement);
const resolvedBaseUrl = resolveUrl(reference.baseUrl, initialBaseUrl);
const finalBaseUrl = merge(parseAttributes(baseUrlElement), {
baseUrl: resolvedBaseUrl
}); // If the URL is resolved, we want to get the serviceLocation from the reference
// assuming there is no serviceLocation on the initialBaseUrl
if (resolvedBaseUrl !== initialBaseUrl && !finalBaseUrl.serviceLocation && reference.serviceLocation) {
finalBaseUrl.serviceLocation = reference.serviceLocation;
}
return finalBaseUrl;
});
}));
};
@ -2110,8 +2154,9 @@ 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}
@ -2126,9 +2171,7 @@ const inheritBaseUrls = (adaptationSetAttributes, adaptationSetBaseUrls, adaptat
return repBaseUrls.map(baseUrl => {
return {
segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
attributes: merge(attributes, {
baseUrl
})
attributes: merge(attributes, baseUrl)
};
});
};
@ -2295,8 +2338,9 @@ 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}
@ -2372,8 +2416,9 @@ const toRepresentations = (periodAttributes, periodBaseUrls, periodSegmentInfo)
*
* @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
*/
@ -2392,6 +2437,43 @@ const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index) => {
const periodSegmentInfo = getSegmentInformation(period.node);
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
*/
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.
*
@ -2471,7 +2553,14 @@ 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');
@ -2481,7 +2570,10 @@ const inheritAttributes = (mpd, options = {}) => {
const locations = findChildren(mpd, 'Location');
const mpdAttributes = parseAttributes(mpd);
const mpdBaseUrls = buildBaseUrls([manifestUri], findChildren(mpd, 'BaseURL')); // See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'.
const mpdBaseUrls = buildBaseUrls([{
baseUrl: 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';
mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0;
@ -2514,6 +2606,14 @@ 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))
};
@ -2531,7 +2631,7 @@ const stringToMpdXml = manifestString => {
try {
xml = parser.parseFromString(manifestString, 'application/xml');
mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null;
} catch (e) {// ie 11 throwsw on invalid xml
} catch (e) {// ie 11 throws on invalid xml
}
if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) {
@ -2609,6 +2709,7 @@ const parse = (manifestString, options = {}) => {
return toM3u8({
dashPlaylists: playlists,
locations: parsedManifestInfo.locations,
contentSteering: parsedManifestInfo.contentSteeringInfo,
sidxMapping: options.sidxMapping,
previousManifest: options.previousManifest,
eventStream: parsedManifestInfo.eventStream

View file

@ -1,11 +1,11 @@
/*! @name mpd-parser @version 1.1.1 @license Apache-2.0 */
/*! @name mpd-parser @version 1.2.2 @license Apache-2.0 */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@xmldom/xmldom')) :
typeof define === 'function' && define.amd ? define(['exports', '@xmldom/xmldom'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.mpdParser = {}, global.window));
}(this, (function (exports, xmldom) { 'use strict';
var version = "1.1.1";
var version = "1.2.2";
const isObject = obj => {
return !!obj && typeof obj === 'object';
@ -81,6 +81,7 @@
var errors = {
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',
@ -987,43 +988,56 @@
const generateSidxKey = sidx => sidx && sidx.uri + '-' + byteRangeToString(sidx.byterange);
const mergeDiscontiguousPlaylists = playlists => {
const mergedPlaylists = values(playlists.reduce((acc, playlist) => {
// assuming playlist IDs are the same across periods
// TODO: handle multiperiod where representation sets are not the same
// across periods
const name = playlist.attributes.id + (playlist.attributes.lang || '');
if (!acc[name]) {
// First Period
acc[name] = playlist;
acc[name].attributes.timelineStarts = [];
} else {
// Subsequent Periods
if (playlist.segments) {
// first segment of subsequent periods signal a discontinuity
if (playlist.segments[0]) {
playlist.segments[0].discontinuity = true;
}
acc[name].segments.push(...playlist.segments);
} // bubble up contentProtection, this assumes all DRM content
// has the same contentProtection
if (playlist.attributes.contentProtection) {
acc[name].attributes.contentProtection = playlist.attributes.contentProtection;
}
// Break out playlists into groups based on their baseUrl
const playlistsByBaseUrl = playlists.reduce(function (acc, cur) {
if (!acc[cur.attributes.baseUrl]) {
acc[cur.attributes.baseUrl] = [];
}
acc[name].attributes.timelineStarts.push({
// Although they represent the same number, it's important to have both to make it
// compatible with HLS potentially having a similar attribute.
start: playlist.attributes.periodStart,
timeline: playlist.attributes.periodStart
});
acc[cur.attributes.baseUrl].push(cur);
return acc;
}, {}));
return mergedPlaylists.map(playlist => {
}, {});
let allPlaylists = [];
Object.values(playlistsByBaseUrl).forEach(playlistGroup => {
const mergedPlaylists = values(playlistGroup.reduce((acc, playlist) => {
// assuming playlist IDs are the same across periods
// TODO: handle multiperiod where representation sets are not the same
// across periods
const name = playlist.attributes.id + (playlist.attributes.lang || '');
if (!acc[name]) {
// First Period
acc[name] = playlist;
acc[name].attributes.timelineStarts = [];
} else {
// Subsequent Periods
if (playlist.segments) {
// first segment of subsequent periods signal a discontinuity
if (playlist.segments[0]) {
playlist.segments[0].discontinuity = true;
}
acc[name].segments.push(...playlist.segments);
} // bubble up contentProtection, this assumes all DRM content
// has the same contentProtection
if (playlist.attributes.contentProtection) {
acc[name].attributes.contentProtection = playlist.attributes.contentProtection;
}
}
acc[name].attributes.timelineStarts.push({
// Although they represent the same number, it's important to have both to make it
// compatible with HLS potentially having a similar attribute.
start: playlist.attributes.periodStart,
timeline: playlist.attributes.periodStart
});
return acc;
}, {}));
allPlaylists = allPlaylists.concat(mergedPlaylists);
});
return allPlaylists.map(playlist => {
playlist.discontinuityStarts = findIndexes(playlist.segments || [], 'discontinuity');
return playlist;
});
@ -1068,7 +1082,7 @@
uri: '',
endList: attributes.type === 'static',
timeline: attributes.periodStart,
resolvedUri: '',
resolvedUri: attributes.baseUrl || '',
targetDuration: attributes.duration,
discontinuitySequence,
discontinuityStarts,
@ -1081,6 +1095,10 @@
playlist.contentProtection = attributes.contentProtection;
}
if (attributes.serviceLocation) {
playlist.attributes.serviceLocation = attributes.serviceLocation;
}
if (sidx) {
playlist.sidx = sidx;
}
@ -1122,7 +1140,7 @@
m3u8Attributes.CODECS = attributes.codecs;
}
return {
const vttPlaylist = {
attributes: m3u8Attributes,
uri: '',
endList: attributes.type === 'static',
@ -1135,6 +1153,12 @@
mediaSequence,
segments
};
if (attributes.serviceLocation) {
vttPlaylist.attributes.serviceLocation = attributes.serviceLocation;
}
return vttPlaylist;
};
const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => {
let mainPlaylist;
@ -1249,7 +1273,7 @@
uri: '',
endList: attributes.type === 'static',
timeline: attributes.periodStart,
resolvedUri: '',
resolvedUri: attributes.baseUrl || '',
targetDuration: attributes.duration,
discontinuityStarts,
timelineStarts: attributes.timelineStarts,
@ -1264,6 +1288,10 @@
playlist.contentProtection = attributes.contentProtection;
}
if (attributes.serviceLocation) {
playlist.attributes.serviceLocation = attributes.serviceLocation;
}
if (sidx) {
playlist.sidx = sidx;
}
@ -1356,6 +1384,7 @@
const toM3u8 = ({
dashPlaylists,
locations,
contentSteering,
sidxMapping = {},
previousManifest,
eventStream
@ -1399,6 +1428,10 @@
manifest.locations = locations;
}
if (contentSteering) {
manifest.contentSteering = contentSteering;
}
if (type === 'dynamic') {
manifest.suggestedPresentationDelay = suggestedPresentationDelay;
}
@ -2248,22 +2281,33 @@
/**
* 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
*/
const buildBaseUrls = (referenceUrls, baseUrlElements) => {
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));
const initialBaseUrl = getContent(baseUrlElement);
const resolvedBaseUrl = resolveUrl(reference.baseUrl, initialBaseUrl);
const finalBaseUrl = merge(parseAttributes(baseUrlElement), {
baseUrl: resolvedBaseUrl
}); // If the URL is resolved, we want to get the serviceLocation from the reference
// assuming there is no serviceLocation on the initialBaseUrl
if (resolvedBaseUrl !== initialBaseUrl && !finalBaseUrl.serviceLocation && reference.serviceLocation) {
finalBaseUrl.serviceLocation = reference.serviceLocation;
}
return finalBaseUrl;
});
}));
};
@ -2365,8 +2409,9 @@
*
* @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}
@ -2381,9 +2426,7 @@
return repBaseUrls.map(baseUrl => {
return {
segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
attributes: merge(attributes, {
baseUrl
})
attributes: merge(attributes, baseUrl)
};
});
};
@ -2550,8 +2593,9 @@
*
* @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}
@ -2627,8 +2671,9 @@
*
* @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
*/
@ -2647,6 +2692,43 @@
const periodSegmentInfo = getSegmentInformation(period.node);
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
*/
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.
*
@ -2726,7 +2808,14 @@
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');
@ -2736,7 +2825,10 @@
const locations = findChildren(mpd, 'Location');
const mpdAttributes = parseAttributes(mpd);
const mpdBaseUrls = buildBaseUrls([manifestUri], findChildren(mpd, 'BaseURL')); // See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'.
const mpdBaseUrls = buildBaseUrls([{
baseUrl: 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';
mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0;
@ -2769,6 +2861,14 @@
});
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))
};
@ -2786,7 +2886,7 @@
try {
xml = parser.parseFromString(manifestString, 'application/xml');
mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null;
} catch (e) {// ie 11 throwsw on invalid xml
} catch (e) {// ie 11 throws on invalid xml
}
if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) {
@ -2864,6 +2964,7 @@
return toM3u8({
dashPlaylists: playlists,
locations: parsedManifestInfo.locations,
contentSteering: parsedManifestInfo.contentSteeringInfo,
sidxMapping: options.sidxMapping,
previousManifest: options.previousManifest,
eventStream: parsedManifestInfo.eventStream

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
{
"name": "mpd-parser",
"version": "1.1.1",
"version": "1.2.2",
"description": "mpd parser",
"main": "dist/mpd-parser.cjs.js",
"module": "dist/mpd-parser.es.js",
@ -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",

View file

@ -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',

View file

@ -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

View file

@ -16,21 +16,35 @@ 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));
const initialBaseUrl = getContent(baseUrlElement);
const resolvedBaseUrl = resolveUrl(reference.baseUrl, initialBaseUrl);
const finalBaseUrl = merge(
parseAttributes(baseUrlElement),
{ baseUrl: resolvedBaseUrl }
);
// If the URL is resolved, we want to get the serviceLocation from the reference
// assuming there is no serviceLocation on the initialBaseUrl
if (resolvedBaseUrl !== initialBaseUrl && !finalBaseUrl.serviceLocation && reference.serviceLocation) {
finalBaseUrl.serviceLocation = reference.serviceLocation;
}
return finalBaseUrl;
});
}));
};
@ -140,8 +154,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 +173,7 @@ export const inheritBaseUrls =
return repBaseUrls.map(baseUrl => {
return {
segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
attributes: merge(attributes, { baseUrl })
attributes: merge(attributes, baseUrl)
};
});
};
@ -340,8 +355,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 +437,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 +458,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 +570,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');
@ -529,7 +588,8 @@ export const inheritAttributes = (mpd, options = {}) => {
const locations = findChildren(mpd, 'Location');
const mpdAttributes = parseAttributes(mpd);
const mpdBaseUrls = buildBaseUrls([ manifestUri ], findChildren(mpd, 'BaseURL'));
const mpdBaseUrls = buildBaseUrls([{ baseUrl: 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 +627,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))
};

View file

@ -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 &&

111
node_modules/mpd-parser/src/toM3u8.js generated vendored
View file

@ -11,45 +11,62 @@ export const generateSidxKey = (sidx) => sidx &&
sidx.uri + '-' + byteRangeToString(sidx.byterange);
const mergeDiscontiguousPlaylists = playlists => {
const mergedPlaylists = values(playlists.reduce((acc, playlist) => {
// assuming playlist IDs are the same across periods
// TODO: handle multiperiod where representation sets are not the same
// across periods
const name = playlist.attributes.id + (playlist.attributes.lang || '');
if (!acc[name]) {
// First Period
acc[name] = playlist;
acc[name].attributes.timelineStarts = [];
} else {
// Subsequent Periods
if (playlist.segments) {
// first segment of subsequent periods signal a discontinuity
if (playlist.segments[0]) {
playlist.segments[0].discontinuity = true;
}
acc[name].segments.push(...playlist.segments);
}
// bubble up contentProtection, this assumes all DRM content
// has the same contentProtection
if (playlist.attributes.contentProtection) {
acc[name].attributes.contentProtection =
playlist.attributes.contentProtection;
}
// Break out playlists into groups based on their baseUrl
const playlistsByBaseUrl = playlists.reduce(function(acc, cur) {
if (!acc[cur.attributes.baseUrl]) {
acc[cur.attributes.baseUrl] = [];
}
acc[name].attributes.timelineStarts.push({
// Although they represent the same number, it's important to have both to make it
// compatible with HLS potentially having a similar attribute.
start: playlist.attributes.periodStart,
timeline: playlist.attributes.periodStart
});
acc[cur.attributes.baseUrl].push(cur);
return acc;
}, {}));
}, {});
return mergedPlaylists.map(playlist => {
let allPlaylists = [];
Object.values(playlistsByBaseUrl).forEach((playlistGroup) => {
const mergedPlaylists = values(playlistGroup.reduce((acc, playlist) => {
// assuming playlist IDs are the same across periods
// TODO: handle multiperiod where representation sets are not the same
// across periods
const name = playlist.attributes.id + (playlist.attributes.lang || '');
if (!acc[name]) {
// First Period
acc[name] = playlist;
acc[name].attributes.timelineStarts = [];
} else {
// Subsequent Periods
if (playlist.segments) {
// first segment of subsequent periods signal a discontinuity
if (playlist.segments[0]) {
playlist.segments[0].discontinuity = true;
}
acc[name].segments.push(...playlist.segments);
}
// bubble up contentProtection, this assumes all DRM content
// has the same contentProtection
if (playlist.attributes.contentProtection) {
acc[name].attributes.contentProtection =
playlist.attributes.contentProtection;
}
}
acc[name].attributes.timelineStarts.push({
// Although they represent the same number, it's important to have both to make it
// compatible with HLS potentially having a similar attribute.
start: playlist.attributes.periodStart,
timeline: playlist.attributes.periodStart
});
return acc;
}, {}));
allPlaylists = allPlaylists.concat(mergedPlaylists);
});
return allPlaylists.map(playlist => {
playlist.discontinuityStarts =
findIndexes(playlist.segments || [], 'discontinuity');
@ -98,7 +115,7 @@ export const formatAudioPlaylist = ({
uri: '',
endList: attributes.type === 'static',
timeline: attributes.periodStart,
resolvedUri: '',
resolvedUri: attributes.baseUrl || '',
targetDuration: attributes.duration,
discontinuitySequence,
discontinuityStarts,
@ -111,6 +128,10 @@ export const formatAudioPlaylist = ({
playlist.contentProtection = attributes.contentProtection;
}
if (attributes.serviceLocation) {
playlist.attributes.serviceLocation = attributes.serviceLocation;
}
if (sidx) {
playlist.sidx = sidx;
}
@ -139,6 +160,7 @@ export const formatVttPlaylist = ({
duration: attributes.sourceDuration,
number: 0
}];
// targetDuration should be the same duration as the only segment
attributes.duration = attributes.sourceDuration;
}
@ -152,7 +174,7 @@ export const formatVttPlaylist = ({
if (attributes.codecs) {
m3u8Attributes.CODECS = attributes.codecs;
}
return {
const vttPlaylist = {
attributes: m3u8Attributes,
uri: '',
endList: attributes.type === 'static',
@ -165,6 +187,12 @@ export const formatVttPlaylist = ({
mediaSequence,
segments
};
if (attributes.serviceLocation) {
vttPlaylist.attributes.serviceLocation = attributes.serviceLocation;
}
return vttPlaylist;
};
export const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => {
@ -289,7 +317,7 @@ export const formatVideoPlaylist = ({
uri: '',
endList: attributes.type === 'static',
timeline: attributes.periodStart,
resolvedUri: '',
resolvedUri: attributes.baseUrl || '',
targetDuration: attributes.duration,
discontinuityStarts,
timelineStarts: attributes.timelineStarts,
@ -304,6 +332,10 @@ export const formatVideoPlaylist = ({
playlist.contentProtection = attributes.contentProtection;
}
if (attributes.serviceLocation) {
playlist.attributes.serviceLocation = attributes.serviceLocation;
}
if (sidx) {
playlist.sidx = sidx;
}
@ -392,6 +424,7 @@ export const flattenMediaGroupPlaylists = (mediaGroupObject) => {
export const toM3u8 = ({
dashPlaylists,
locations,
contentSteering,
sidxMapping = {},
previousManifest,
eventStream
@ -437,6 +470,10 @@ export const toM3u8 = ({
manifest.locations = locations;
}
if (contentSteering) {
manifest.contentSteering = contentSteering;
}
if (type === 'dynamic') {
manifest.suggestedPresentationDelay = suggestedPresentationDelay;
}

View file

@ -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: [{
@ -593,6 +631,82 @@ QUnit.test('end to end - basic', function(assert) {
assert.deepEqual(actual, expected);
});
QUnit.test('end to end - basic using manifest uri', function(assert) {
const NOW = Date.now();
const actual = inheritAttributes(stringToMpdXml(`
<MPD mediaPresentationDuration="PT30S" >
<BaseURL>base/</BaseURL>
<Period>
<AdaptationSet mimeType="video/mp4" >
<Role value="main"></Role>
<SegmentTemplate></SegmentTemplate>
<Representation
bandwidth="5000000"
codecs="avc1.64001e"
height="404"
id="test"
width="720">
</Representation>
</AdaptationSet>
<AdaptationSet mimeType="text/vtt" lang="en">
<Representation bandwidth="256" id="en">
<BaseURL>en.vtt</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>
`), { NOW, manifestUri: 'https://www.test.com' });
const expected = {
contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
attributes: {
bandwidth: 5000000,
baseUrl: 'https://www.test.com/base/',
codecs: 'avc1.64001e',
height: 404,
id: 'test',
mediaPresentationDuration: 30,
mimeType: 'video/mp4',
periodStart: 0,
role: {
value: 'main'
},
sourceDuration: 30,
type: 'static',
width: 720,
NOW,
clientOffset: 0
},
segmentInfo: {
template: {}
}
}, {
attributes: {
bandwidth: 256,
baseUrl: 'https://www.test.com/base/en.vtt',
id: 'en',
lang: 'en',
mediaPresentationDuration: 30,
mimeType: 'text/vtt',
periodStart: 0,
role: {},
sourceDuration: 30,
type: 'static',
NOW,
clientOffset: 0
},
segmentInfo: {}
}]
};
assert.equal(actual.representationInfo.length, 2);
assert.deepEqual(actual, expected);
});
QUnit.test('end to end - basic dynamic', function(assert) {
const NOW = Date.now();
@ -621,6 +735,7 @@ QUnit.test('end to end - basic dynamic', function(assert) {
`), { NOW });
const expected = {
contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@ -666,6 +781,303 @@ 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(`
<MPD type="dyanmic">
<ContentSteering defaultServiceLocation="beta" queryBeforeStart="false" proxyServerURL="http://127.0.0.1:3455/steer">https://example.com/app/url</ContentSteering>
<BaseURL serviceLocation="alpha">https://cdn1.example.com/</BaseURL>
<BaseURL serviceLocation="beta">https://cdn2.example.com/</BaseURL>
<Period start="PT0S">
<AdaptationSet mimeType="video/mp4">
<Role value="main"></Role>
<SegmentTemplate></SegmentTemplate>
<Representation
bandwidth="5000000"
codecs="avc1.64001e"
height="404"
id="test"
width="720">
</Representation>
</AdaptationSet>
<AdaptationSet mimeType="text/vtt" lang="en">
<Representation bandwidth="256" id="en">
<BaseURL>https://example.com/en.vtt</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>
`), { 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(`
<MPD type="dyanmic">
<ContentSteering defaultServiceLocation="beta" queryBeforeStart="false" proxyServerURL="http://127.0.0.1:3455/steer">https://example.com/app/url</ContentSteering>
<BaseURL serviceLocation="alpha">https://cdn1.example.com/</BaseURL>
<BaseURL serviceLocation="beta">https://cdn2.example.com/</BaseURL>
<Period start="PT0S">
<AdaptationSet mimeType="video/mp4">
<Role value="main"></Role>
<SegmentTemplate></SegmentTemplate>
<Representation
bandwidth="5000000"
codecs="avc1.64001e"
height="404"
id="test"
width="720">
</Representation>
<BaseURL>/video</BaseURL>
</AdaptationSet>
<AdaptationSet mimeType="text/vtt" lang="en">
<Representation bandwidth="256" id="en">
<BaseURL>/vtt</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>
`), { 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/video',
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/video',
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/vtt',
clientOffset: 0,
id: 'en',
lang: 'en',
mimeType: 'text/vtt',
periodStart: 0,
role: {},
serviceLocation: 'alpha',
sourceDuration: 0,
type: 'dyanmic'
},
segmentInfo: {}
},
{
attributes: {
NOW,
bandwidth: 256,
baseUrl: 'https://cdn2.example.com/vtt',
clientOffset: 0,
id: 'en',
lang: 'en',
mimeType: 'text/vtt',
periodStart: 0,
role: {},
serviceLocation: 'beta',
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(`
<MPD type="dyanmic">
<ContentSteering defaultServiceLocation="alpha" queryBeforeStart="false" proxyServerURL="http://127.0.0.1:3455/steer">https://example.com/app/url</ContentSteering>
<ContentSteering defaultServiceLocation="beta" queryBeforeStart="false" proxyServerURL="http://127.0.0.1:3455/steer">https://example.com/app/url</ContentSteering>
<BaseURL serviceLocation="alpha">https://cdn1.example.com/</BaseURL>
<BaseURL serviceLocation="beta">https://cdn2.example.com/</BaseURL>
<Period start="PT0S">
<AdaptationSet mimeType="video/mp4">
<Role value="main"></Role>
<SegmentTemplate></SegmentTemplate>
<Representation
bandwidth="5000000"
codecs="avc1.64001e"
height="404"
id="test"
width="720">
</Representation>
</AdaptationSet>
<AdaptationSet mimeType="text/vtt" lang="en">
<Representation bandwidth="256" id="en">
<BaseURL>/video</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>
`), { 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 +1115,7 @@ QUnit.test('end to end - basic multiperiod', function(assert) {
`), { NOW });
const expected = {
contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@ -790,6 +1203,7 @@ QUnit.test('end to end - inherits BaseURL from all levels', function(assert) {
`), { NOW });
const expected = {
contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@ -867,6 +1281,7 @@ QUnit.test('end to end - alternate BaseURLs', function(assert) {
`), { NOW });
const expected = {
contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@ -1035,6 +1450,7 @@ QUnit.test(
`), { NOW });
const expected = {
contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@ -1148,6 +1564,7 @@ QUnit.test(
`), { NOW });
const expected = {
contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@ -1272,6 +1689,7 @@ QUnit.test(
`), { NOW });
const expected = {
contentSteeringInfo: null,
eventStream: [],
locations: undefined,
representationInfo: [{
@ -2116,6 +2534,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 +2622,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 +2792,7 @@ QUnit.test('gets eventStream from inheritAttributes', function(assert) {
</Period>
</MPD>`);
const expected = {
contentSteeringInfo: null,
eventStream: [
{
end: 15,
@ -2481,6 +2902,7 @@ QUnit.test('gets eventStream from inheritAttributes with data in Event tags', fu
</Period>
</MPD>`);
const expected = {
contentSteeringInfo: null,
eventStream: [
{
end: 15,

View file

@ -46,7 +46,7 @@ export const parsedManifest = {
'SUBTITLES': 'subs'
},
endList: true,
resolvedUri: '',
resolvedUri: 'https://www.example.com/1080p.ts',
targetDuration: 6,
mediaSequence: 0,
timelineStarts: [{ start: 0, timeline: 0 }],

View file

@ -47,7 +47,7 @@ export const parsedManifest = {
'SUBTITLES': 'subs'
},
endList: true,
resolvedUri: '',
resolvedUri: 'https://www.example.com/1080p.ts',
targetDuration: 6,
mediaSequence: 0,
timelineStarts: [{ start: 0, timeline: 0 }],

View file

@ -27,7 +27,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'http://example.com/audio_en_2c_128k_aac.mp4',
targetDuration: 60,
segments: [],
mediaSequence: 0,
@ -78,7 +78,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'http://example.com/audio_es_2c_128k_aac.mp4',
targetDuration: 60,
segments: [],
mediaSequence: 0,

View file

@ -32,7 +32,7 @@ export const parsedManifest = {
'SUBTITLES': 'subs'
},
endList: true,
resolvedUri: '',
resolvedUri: 'https://www.example.com/1080p.ts',
targetDuration: 6,
mediaSequence: 0,
discontinuitySequence: 0,

View file

@ -33,7 +33,7 @@ export const parsedManifest = {
'SUBTITLES': 'subs'
},
endList: true,
resolvedUri: '',
resolvedUri: 'https://www.example.com/1080p.ts',
targetDuration: 6,
mediaSequence: 0,
discontinuitySequence: 0,

View file

@ -25,7 +25,7 @@ export const parsedManifest = {
timelineStarts: [{ start: 0, timeline: 0 }],
discontinuitySequence: 0,
discontinuityStarts: [],
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.984,
segments: [
{
@ -100,7 +100,7 @@ export const parsedManifest = {
timelineStarts: [{ start: 0, timeline: 0 }],
discontinuitySequence: 0,
discontinuityStarts: [],
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.984,
segments: [
{
@ -183,7 +183,7 @@ export const parsedManifest = {
timelineStarts: [{ start: 0, timeline: 0 }],
discontinuitySequence: 0,
discontinuityStarts: [],
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.984,
segments: [
{
@ -258,7 +258,7 @@ export const parsedManifest = {
timelineStarts: [{ start: 0, timeline: 0 }],
discontinuitySequence: 0,
discontinuityStarts: [],
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.984,
segments: [
{
@ -421,7 +421,7 @@ export const parsedManifest = {
timelineStarts: [{ start: 0, timeline: 0 }],
discontinuitySequence: 0,
discontinuityStarts: [],
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.9185833333333333,
segments: [
{
@ -503,7 +503,7 @@ export const parsedManifest = {
timelineStarts: [{ start: 0, timeline: 0 }],
discontinuitySequence: 0,
discontinuityStarts: [],
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.9185833333333333,
segments: [
{

View file

@ -36,7 +36,7 @@ export const parsedManifest = {
timeline: 6
}],
targetDuration: 3,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
segments: [
{
duration: 3,

View file

@ -30,7 +30,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 5,
segments: [
{
@ -145,7 +145,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 5,
segments: [
{

View file

@ -25,13 +25,13 @@ export const parsedManifest = {
'PROGRAM-ID': 1
},
endList: false,
mediaSequence: 7,
mediaSequence: 3,
discontinuitySequence: 2,
discontinuityStarts: [0],
timelineStarts: [
{ start: 111, timeline: 111}
],
resolvedUri: '',
resolvedUri: 'http://example.com/audio/v0/',
segments: [
{
discontinuity: true,
@ -41,7 +41,7 @@ export const parsedManifest = {
uri: 'init.mp4'
},
presentationTime: 111,
number: 7,
number: 3,
resolvedUri: 'http://example.com/audio/v0/862.m4f',
timeline: 111,
uri: '862.m4f'
@ -53,7 +53,7 @@ export const parsedManifest = {
uri: 'init.mp4'
},
presentationTime: 112,
number: 8,
number: 4,
resolvedUri: 'http://example.com/audio/v0/863.m4f',
timeline: 111,
uri: '863.m4f'
@ -65,7 +65,7 @@ export const parsedManifest = {
uri: 'init.mp4'
},
presentationTime: 113,
number: 9,
number: 5,
resolvedUri: 'http://example.com/audio/v0/864.m4f',
timeline: 111,
uri: '864.m4f'
@ -107,7 +107,7 @@ export const parsedManifest = {
timelineStarts: [
{ start: 111, timeline: 111}
],
resolvedUri: '',
resolvedUri: 'http://example.com/video/D/',
segments: [
{
discontinuity: true,
@ -172,7 +172,7 @@ export const parsedManifest = {
{ start: 111, timeline: 111}
],
discontinuityStarts: [0],
resolvedUri: '',
resolvedUri: 'http://example.com/video/E/',
segments: [
{
discontinuity: true,
@ -231,13 +231,13 @@ export const parsedManifest = {
'SUBTITLES': 'subs'
},
endList: false,
mediaSequence: 7,
mediaSequence: 3,
discontinuitySequence: 2,
timelineStarts: [
{ start: 111, timeline: 111}
],
discontinuityStarts: [0],
resolvedUri: '',
resolvedUri: 'http://example.com/video/F/',
segments: [
{
discontinuity: true,
@ -247,7 +247,7 @@ export const parsedManifest = {
uri: 'F_init.mp4'
},
presentationTime: 111,
number: 7,
number: 3,
resolvedUri: 'http://example.com/video/F/F862.m4f',
timeline: 111,
uri: 'F862.m4f'
@ -259,7 +259,7 @@ export const parsedManifest = {
uri: 'F_init.mp4'
},
presentationTime: 112,
number: 8,
number: 4,
resolvedUri: 'http://example.com/video/F/F863.m4f',
timeline: 111,
uri: 'F863.m4f'
@ -271,7 +271,7 @@ export const parsedManifest = {
uri: 'F_init.mp4'
},
presentationTime: 113,
number: 9,
number: 5,
resolvedUri: 'http://example.com/video/F/F864.m4f',
timeline: 111,
uri: 'F864.m4f'
@ -302,7 +302,7 @@ export const parsedManifest = {
{ start: 111, timeline: 111}
],
discontinuityStarts: [0],
resolvedUri: '',
resolvedUri: 'http://example.com/video/A/',
segments: [
{
discontinuity: true,
@ -367,7 +367,7 @@ export const parsedManifest = {
{ start: 111, timeline: 111}
],
discontinuityStarts: [0],
resolvedUri: '',
resolvedUri: 'http://example.com/video/B/',
segments: [
{
discontinuity: true,
@ -426,13 +426,13 @@ export const parsedManifest = {
'SUBTITLES': 'subs'
},
endList: false,
mediaSequence: 7,
mediaSequence: 3,
discontinuitySequence: 2,
timelineStarts: [
{ start: 111, timeline: 111}
],
discontinuityStarts: [0],
resolvedUri: '',
resolvedUri: 'http://example.com/video/C/',
segments: [
{
discontinuity: true,
@ -442,7 +442,7 @@ export const parsedManifest = {
uri: 'C_init.mp4'
},
presentationTime: 111,
number: 7,
number: 3,
resolvedUri: 'http://example.com/video/C/C862.m4f',
timeline: 111,
uri: 'C862.m4f'
@ -454,7 +454,7 @@ export const parsedManifest = {
uri: 'C_init.mp4'
},
presentationTime: 112,
number: 8,
number: 4,
resolvedUri: 'http://example.com/video/C/C863.m4f',
timeline: 111,
uri: 'C863.m4f'
@ -466,7 +466,7 @@ export const parsedManifest = {
uri: 'C_init.mp4'
},
presentationTime: 113,
number: 9,
number: 5,
resolvedUri: 'http://example.com/video/C/C864.m4f',
timeline: 111,
uri: 'C864.m4f'

View file

@ -27,14 +27,8 @@ export const parsedManifest = {
endList: false,
mediaSequence: 0,
discontinuitySequence: 0,
discontinuityStarts: [3, 5, 7],
timelineStarts: [
{ start: 100, timeline: 100},
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
resolvedUri: '',
discontinuityStarts: [],
resolvedUri: 'http://example.com/audio/1',
segments: [
{
duration: 1,
@ -71,16 +65,36 @@ export const parsedManifest = {
resolvedUri: 'http://example.com/audio/502.m4f',
timeline: 100,
uri: '502.m4f'
},
}
],
targetDuration: 1,
timeline: 100,
timelineStarts: [
{ start: 100, timeline: 100}
],
uri: ''
},
{
attributes: {
'BANDWIDTH': 128352,
'CODECS': 'mp4a.40.5',
'NAME': 'v0',
'PROGRAM-ID': 1
},
discontinuitySequence: 1,
discontinuityStarts: [2, 4],
endList: false,
mediaSequence: 0,
resolvedUri: 'http://example.com/audio/v0/',
segments: [
{
discontinuity: true,
duration: 2,
map: {
resolvedUri: 'http://example.com/audio/v0/init.mp4',
uri: 'init.mp4'
},
presentationTime: 103,
number: 3,
number: 0,
resolvedUri: 'http://example.com/audio/v0/000.m4f',
timeline: 103,
uri: '000.m4f'
@ -92,7 +106,7 @@ export const parsedManifest = {
uri: 'init.mp4'
},
presentationTime: 105,
number: 4,
number: 1,
resolvedUri: 'http://example.com/audio/v0/001.m4f',
timeline: 103,
uri: '001.m4f'
@ -105,7 +119,7 @@ export const parsedManifest = {
uri: 'init.mp4'
},
presentationTime: 107,
number: 5,
number: 2,
resolvedUri: 'http://example.com/audio/v0/000.m4f',
timeline: 107,
uri: '000.m4f'
@ -117,7 +131,7 @@ export const parsedManifest = {
uri: 'init.mp4'
},
presentationTime: 109,
number: 6,
number: 3,
resolvedUri: 'http://example.com/audio/v0/001.m4f',
timeline: 107,
uri: '001.m4f'
@ -130,7 +144,7 @@ export const parsedManifest = {
uri: 'init.mp4'
},
presentationTime: 111,
number: 7,
number: 4,
resolvedUri: 'http://example.com/audio/v0/862.m4f',
timeline: 111,
uri: '862.m4f'
@ -142,7 +156,7 @@ export const parsedManifest = {
uri: 'init.mp4'
},
presentationTime: 112,
number: 8,
number: 5,
resolvedUri: 'http://example.com/audio/v0/863.m4f',
timeline: 111,
uri: '863.m4f'
@ -154,14 +168,19 @@ export const parsedManifest = {
uri: 'init.mp4'
},
presentationTime: 113,
number: 9,
number: 6,
resolvedUri: 'http://example.com/audio/v0/864.m4f',
timeline: 111,
uri: '864.m4f'
}
],
targetDuration: 1,
timeline: 100,
targetDuration: 2,
timeline: 103,
timelineStarts: [
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
uri: ''
}
],
@ -189,17 +208,11 @@ export const parsedManifest = {
},
'SUBTITLES': 'subs'
},
endList: false,
mediaSequence: 0,
discontinuitySequence: 0,
discontinuityStarts: [3, 5, 7],
timelineStarts: [
{ start: 100, timeline: 100},
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
resolvedUri: '',
endList: false,
mediaSequence: 0,
resolvedUri: 'http://example.com/video/D/',
segments: [
{
duration: 1,
@ -327,6 +340,12 @@ export const parsedManifest = {
],
targetDuration: 1,
timeline: 100,
timelineStarts: [
{ start: 100, timeline: 100},
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
uri: ''
},
{
@ -343,17 +362,11 @@ export const parsedManifest = {
},
'SUBTITLES': 'subs'
},
endList: false,
mediaSequence: 0,
discontinuitySequence: 0,
discontinuityStarts: [3, 5, 7],
timelineStarts: [
{ start: 100, timeline: 100},
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
resolvedUri: '',
endList: false,
mediaSequence: 0,
resolvedUri: 'http://example.com/video/E/',
segments: [
{
duration: 1,
@ -481,6 +494,12 @@ export const parsedManifest = {
],
targetDuration: 1,
timeline: 100,
timelineStarts: [
{ start: 100, timeline: 100},
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
uri: ''
},
{
@ -497,17 +516,11 @@ export const parsedManifest = {
},
'SUBTITLES': 'subs'
},
discontinuitySequence: 0,
discontinuityStarts: [],
endList: false,
mediaSequence: 0,
discontinuitySequence: 0,
discontinuityStarts: [3, 5, 7],
timelineStarts: [
{ start: 100, timeline: 100},
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
resolvedUri: '',
resolvedUri: 'http://example.com/video/E/',
segments: [
{
duration: 1,
@ -544,97 +557,77 @@ export const parsedManifest = {
resolvedUri: 'http://example.com/video/E/F502.m4f',
timeline: 100,
uri: 'F502.m4f'
},
{
discontinuity: true,
duration: 2,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 103,
number: 3,
resolvedUri: 'http://example.com/video/F/F000.m4f',
timeline: 103,
uri: 'F000.m4f'
},
{
duration: 2,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 105,
number: 4,
resolvedUri: 'http://example.com/video/F/F001.m4f',
timeline: 103,
uri: 'F001.m4f'
},
{
discontinuity: true,
duration: 1,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 107,
number: 5,
resolvedUri: 'http://example.com/video/F/F000.m4f',
timeline: 107,
uri: 'F000.m4f'
},
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 108,
number: 6,
resolvedUri: 'http://example.com/video/F/F001.m4f',
timeline: 107,
uri: 'F001.m4f'
},
{
discontinuity: true,
duration: 1,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 111,
number: 7,
resolvedUri: 'http://example.com/video/F/F862.m4f',
timeline: 111,
uri: 'F862.m4f'
},
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 112,
number: 8,
resolvedUri: 'http://example.com/video/F/F863.m4f',
timeline: 111,
uri: 'F863.m4f'
},
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 113,
number: 9,
resolvedUri: 'http://example.com/video/F/F864.m4f',
timeline: 111,
uri: 'F864.m4f'
}
],
targetDuration: 1,
timeline: 100,
timelineStarts: [
{ start: 100, timeline: 100}
],
uri: ''
},
{
attributes: {
'AUDIO': 'audio',
'BANDWIDTH': 1277155,
'CODECS': 'avc1.4d001e',
'FRAME-RATE': 30,
'NAME': 'C',
'PROGRAM-ID': 1,
'RESOLUTION': {
height: 540,
width: 960
},
'SUBTITLES': 'subs'
},
discontinuitySequence: 0,
discontinuityStarts: [],
endList: false,
mediaSequence: 0,
resolvedUri: 'http://example.com/video/E/',
segments: [
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/E/C_init.mp4',
uri: 'C_init.mp4'
},
presentationTime: 100,
number: 0,
resolvedUri: 'http://example.com/video/E/C500.m4f',
timeline: 100,
uri: 'C500.m4f'
},
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/E/C_init.mp4',
uri: 'C_init.mp4'
},
presentationTime: 101,
number: 1,
resolvedUri: 'http://example.com/video/E/C501.m4f',
timeline: 100,
uri: 'C501.m4f'
},
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/E/C_init.mp4',
uri: 'C_init.mp4'
},
presentationTime: 102,
number: 2,
resolvedUri: 'http://example.com/video/E/C502.m4f',
timeline: 100,
uri: 'C502.m4f'
}
],
targetDuration: 1,
timeline: 100,
timelineStarts: [
{ start: 100, timeline: 100}
],
uri: ''
},
{
@ -651,17 +644,11 @@ export const parsedManifest = {
},
'SUBTITLES': 'subs'
},
endList: false,
mediaSequence: 0,
discontinuitySequence: 0,
discontinuityStarts: [3, 5, 7],
timelineStarts: [
{ start: 100, timeline: 100},
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
resolvedUri: '',
endList: false,
mediaSequence: 0,
resolvedUri: 'http://example.com/video/A/',
segments: [
{
duration: 1,
@ -789,6 +776,12 @@ export const parsedManifest = {
],
targetDuration: 1,
timeline: 100,
timelineStarts: [
{ start: 100, timeline: 100},
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
uri: ''
},
{
@ -805,17 +798,11 @@ export const parsedManifest = {
},
'SUBTITLES': 'subs'
},
endList: false,
mediaSequence: 0,
discontinuitySequence: 0,
discontinuityStarts: [3, 5, 7],
timelineStarts: [
{ start: 100, timeline: 100},
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
resolvedUri: '',
endList: false,
mediaSequence: 0,
resolvedUri: 'http://example.com/video/B/',
segments: [
{
duration: 1,
@ -943,13 +930,135 @@ export const parsedManifest = {
],
targetDuration: 1,
timeline: 100,
timelineStarts: [
{ start: 100, timeline: 100},
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
uri: ''
},
{
attributes: {
'AUDIO': 'audio',
'BANDWIDTH': 1277155,
'CODECS': 'avc1.4d001e',
'BANDWIDTH': 2215557,
'CODECS': 'avc1.640020',
'FRAME-RATE': 60,
'NAME': 'F',
'PROGRAM-ID': 1,
'RESOLUTION': {
height: 720,
width: 1280
},
'SUBTITLES': 'subs'
},
discontinuitySequence: 1,
discontinuityStarts: [2, 4],
endList: false,
mediaSequence: 0,
resolvedUri: 'http://example.com/video/F/',
segments: [
{
duration: 2,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 103,
number: 0,
resolvedUri: 'http://example.com/video/F/F000.m4f',
timeline: 103,
uri: 'F000.m4f'
},
{
duration: 2,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 105,
number: 1,
resolvedUri: 'http://example.com/video/F/F001.m4f',
timeline: 103,
uri: 'F001.m4f'
},
{
discontinuity: true,
duration: 1,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 107,
number: 2,
resolvedUri: 'http://example.com/video/F/F000.m4f',
timeline: 107,
uri: 'F000.m4f'
},
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 108,
number: 3,
resolvedUri: 'http://example.com/video/F/F001.m4f',
timeline: 107,
uri: 'F001.m4f'
},
{
discontinuity: true,
duration: 1,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 111,
number: 4,
resolvedUri: 'http://example.com/video/F/F862.m4f',
timeline: 111,
uri: 'F862.m4f'
},
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 112,
number: 5,
resolvedUri: 'http://example.com/video/F/F863.m4f',
timeline: 111,
uri: 'F863.m4f'
},
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/F/F_init.mp4',
uri: 'F_init.mp4'
},
presentationTime: 113,
number: 6,
resolvedUri: 'http://example.com/video/F/F864.m4f',
timeline: 111,
uri: 'F864.m4f'
}
],
targetDuration: 2,
timeline: 103,
timelineStarts: [
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
uri: ''
},
{
attributes: {
'AUDIO': 'audio',
'BANDWIDTH': 1048480,
'CODECS': 'avc1.4d001f',
'FRAME-RATE': 30,
'NAME': 'C',
'PROGRAM-ID': 1,
@ -959,63 +1068,20 @@ export const parsedManifest = {
},
'SUBTITLES': 'subs'
},
discontinuitySequence: 1,
discontinuityStarts: [2, 4],
endList: false,
mediaSequence: 0,
discontinuitySequence: 0,
discontinuityStarts: [3, 5, 7],
timelineStarts: [
{ start: 100, timeline: 100},
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
resolvedUri: '',
resolvedUri: 'http://example.com/video/C/',
segments: [
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/E/C_init.mp4',
uri: 'C_init.mp4'
},
presentationTime: 100,
number: 0,
resolvedUri: 'http://example.com/video/E/C500.m4f',
timeline: 100,
uri: 'C500.m4f'
},
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/E/C_init.mp4',
uri: 'C_init.mp4'
},
presentationTime: 101,
number: 1,
resolvedUri: 'http://example.com/video/E/C501.m4f',
timeline: 100,
uri: 'C501.m4f'
},
{
duration: 1,
map: {
resolvedUri: 'http://example.com/video/E/C_init.mp4',
uri: 'C_init.mp4'
},
presentationTime: 102,
number: 2,
resolvedUri: 'http://example.com/video/E/C502.m4f',
timeline: 100,
uri: 'C502.m4f'
},
{
discontinuity: true,
duration: 2,
map: {
resolvedUri: 'http://example.com/video/C/C_init.mp4',
uri: 'C_init.mp4'
},
presentationTime: 103,
number: 3,
number: 0,
resolvedUri: 'http://example.com/video/C/C000.m4f',
timeline: 103,
uri: 'C000.m4f'
@ -1027,7 +1093,7 @@ export const parsedManifest = {
uri: 'C_init.mp4'
},
presentationTime: 105,
number: 4,
number: 1,
resolvedUri: 'http://example.com/video/C/C001.m4f',
timeline: 103,
uri: 'C001.m4f'
@ -1040,7 +1106,7 @@ export const parsedManifest = {
uri: 'C_init.mp4'
},
presentationTime: 107,
number: 5,
number: 2,
resolvedUri: 'http://example.com/video/C/C000.m4f',
timeline: 107,
uri: 'C000.m4f'
@ -1052,7 +1118,7 @@ export const parsedManifest = {
uri: 'C_init.mp4'
},
presentationTime: 109,
number: 6,
number: 3,
resolvedUri: 'http://example.com/video/C/C001.m4f',
timeline: 107,
uri: 'C001.m4f'
@ -1065,7 +1131,7 @@ export const parsedManifest = {
uri: 'C_init.mp4'
},
presentationTime: 111,
number: 7,
number: 4,
resolvedUri: 'http://example.com/video/C/C862.m4f',
timeline: 111,
uri: 'C862.m4f'
@ -1077,7 +1143,7 @@ export const parsedManifest = {
uri: 'C_init.mp4'
},
presentationTime: 112,
number: 8,
number: 5,
resolvedUri: 'http://example.com/video/C/C863.m4f',
timeline: 111,
uri: 'C863.m4f'
@ -1089,14 +1155,19 @@ export const parsedManifest = {
uri: 'C_init.mp4'
},
presentationTime: 113,
number: 9,
number: 6,
resolvedUri: 'http://example.com/video/C/C864.m4f',
timeline: 111,
uri: 'C864.m4f'
}
],
targetDuration: 1,
timeline: 100,
targetDuration: 2,
timeline: 103,
timelineStarts: [
{ start: 103, timeline: 103},
{ start: 107, timeline: 107},
{ start: 111, timeline: 111}
],
uri: ''
}
],

View file

@ -25,7 +25,7 @@ export const parsedManifest = {
'SUBTITLES': 'subs'
},
endList: true,
resolvedUri: '',
resolvedUri: 'https://www.example.com/1080p.ts',
targetDuration: 6,
mediaSequence: 0,
segments: [

View file

@ -27,7 +27,7 @@ export const parsedManifest = {
endList: true,
mediaSequence: 0,
targetDuration: 1,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
segments: [
{
duration: 1,
@ -123,7 +123,7 @@ export const parsedManifest = {
'SUBTITLES': 'subs'
},
endList: true,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
mediaSequence: 0,
targetDuration: 60,
segments: [

View file

@ -25,7 +25,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.984,
segments: [
{
@ -103,7 +103,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.984,
segments: [
{
@ -189,7 +189,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.984,
segments: [
{
@ -267,7 +267,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.984,
segments: [
{
@ -440,7 +440,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.9185833333333333,
segments: [
{
@ -525,7 +525,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 1.9185833333333333,
segments: [
{

View file

@ -22,7 +22,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 4,
segments: [
{
@ -108,7 +108,7 @@ export const parsedManifest = {
uri: '',
endList: true,
timeline: 0,
resolvedUri: '',
resolvedUri: 'https://www.example.com/base',
targetDuration: 4,
segments: [
{

View file

@ -213,6 +213,591 @@ QUnit.test('playlists', function(assert) {
assert.deepEqual(toM3u8({ dashPlaylists }), expected);
});
QUnit.test('playlists with content steering and resolvable URLs', 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/video',
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/video',
uri: ''
},
number: 1,
presentationTime: 0,
resolvedUri: 'https://cdn1.example.com/video',
timeline: 0,
uri: ''
}
]
},
{
attributes: {
bandwidth: 5000000,
baseUrl: 'https://cdn2.example.com/video',
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/video',
uri: ''
},
number: 1,
presentationTime: 0,
resolvedUri: 'https://cdn2.example.com/video',
timeline: 0,
uri: ''
}
]
},
{
attributes: {
bandwidth: 256,
baseUrl: 'https://cdn1.example.com/vtt',
clientOffset: 0,
duration: 0,
id: 'en',
lang: 'en',
mimeType: 'text/vtt',
periodStart: 0,
role: {},
serviceLocation: 'alpha',
sourceDuration: 0,
type: 'dyanmic'
},
segments: [
{
duration: 0,
map: {
resolvedUri: 'https://cdn1.example.com/vtt',
uri: ''
},
number: 1,
presentationTime: 0,
resolvedUri: 'https://cdn1.example.com/vtt',
timeline: 0,
uri: ''
}
]
},
{
attributes: {
bandwidth: 256,
baseUrl: 'https://cdn2.example.com/vtt',
clientOffset: 0,
id: 'en',
lang: 'en',
mimeType: 'text/vtt',
periodStart: 0,
role: {},
serviceLocation: 'beta',
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,
serviceLocation: 'alpha'
},
discontinuitySequence: 0,
discontinuityStarts: [],
endList: false,
mediaSequence: 0,
resolvedUri: 'https://cdn1.example.com/vtt',
segments: [
{
duration: 0,
map: {
resolvedUri: 'https://cdn1.example.com/vtt',
uri: ''
},
number: 0,
presentationTime: 0,
resolvedUri: 'https://cdn1.example.com/vtt',
timeline: 0,
uri: ''
}
],
targetDuration: 0,
timeline: 0,
timelineStarts: [
{
start: 0,
timeline: 0
}
],
uri: ''
},
{
attributes: {
BANDWIDTH: 256,
NAME: 'en',
['PROGRAM-ID']: 1,
serviceLocation: 'beta'
},
discontinuitySequence: 0,
discontinuityStarts: [],
endList: false,
mediaSequence: 0,
resolvedUri: 'https://cdn2.example.com/vtt',
segments: [
{
duration: 0,
number: 0,
resolvedUri: 'https://cdn2.example.com/vtt',
timeline: 0,
uri: 'https://cdn2.example.com/vtt'
}
],
targetDuration: 0,
timeline: 0,
timelineStarts: [
{
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',
serviceLocation: 'alpha'
},
discontinuitySequence: 0,
discontinuityStarts: [],
endList: false,
mediaSequence: 0,
resolvedUri: 'https://cdn1.example.com/video',
segments: [
{
duration: 0,
map: {
resolvedUri: 'https://cdn1.example.com/video',
uri: ''
},
number: 0,
presentationTime: 0,
resolvedUri: 'https://cdn1.example.com/video',
timeline: 0,
uri: ''
}
],
targetDuration: 0,
timeline: 0,
timelineStarts: [
{
start: 0,
timeline: 0
}
],
uri: ''
},
{
attributes: {
AUDIO: 'audio',
BANDWIDTH: 5000000,
CODECS: 'avc1.64001e',
NAME: 'test',
['PROGRAM-ID']: 1,
RESOLUTION: {
height: 404,
width: 720
},
SUBTITLES: 'subs',
serviceLocation: 'beta'
},
discontinuitySequence: 0,
discontinuityStarts: [],
endList: false,
mediaSequence: 0,
resolvedUri: 'https://cdn2.example.com/video',
segments: [
{
duration: 0,
map: {
resolvedUri: 'https://cdn2.example.com/video',
uri: ''
},
number: 0,
presentationTime: 0,
resolvedUri: 'https://cdn2.example.com/video',
timeline: 0,
uri: ''
}
],
targetDuration: 0,
timeline: 0,
timelineStarts: [
{
start: 0,
timeline: 0
}
],
uri: ''
}
],
segments: [],
timelineStarts: [
{
start: 0,
timeline: 0
}
],
uri: ''
};
assert.deepEqual(toM3u8({ dashPlaylists, contentSteering }), 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',
serviceLocation: 'alpha'
},
discontinuitySequence: 0,
discontinuityStarts: [],
endList: false,
mediaSequence: 0,
resolvedUri: 'https://cdn1.example.com/',
segments: [
{
duration: 0,
map: {
resolvedUri: 'https://cdn1.example.com/',
uri: ''
},
number: 0,
presentationTime: 0,
resolvedUri: 'https://cdn1.example.com/',
timeline: 0,
uri: ''
}
],
targetDuration: 0,
timeline: 0,
timelineStarts: [
{
start: 0,
timeline: 0
}
],
uri: ''
},
{
attributes: {
AUDIO: 'audio',
BANDWIDTH: 5000000,
CODECS: 'avc1.64001e',
NAME: 'test',
['PROGRAM-ID']: 1,
RESOLUTION: {
height: 404,
width: 720
},
SUBTITLES: 'subs',
serviceLocation: 'beta'
},
discontinuitySequence: 0,
discontinuityStarts: [],
endList: false,
mediaSequence: 0,
resolvedUri: 'https://cdn2.example.com/',
segments: [
{
duration: 0,
map: {
resolvedUri: 'https://cdn2.example.com/',
uri: ''
},
number: 0,
presentationTime: 0,
resolvedUri: 'https://cdn2.example.com/',
timeline: 0,
uri: ''
}
],
targetDuration: 0,
timeline: 0,
timelineStarts: [
{
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: {

View file

@ -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: {
@ -235,3 +410,204 @@ QUnit.test('presentationTime accounts for presentationTimeOffset', function(asse
assert.deepEqual(toPlaylists(representations), playlists);
});
QUnit.test('playlist with content steering and resolvable BaseURLs', function(assert) {
const representations = [
{
attributes: {
bandwidth: 5000000,
baseUrl: 'https://cdn1.example.com/video',
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/video',
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://cdn1.example.com/vtt',
clientOffset: 0,
id: 'en',
lang: 'en',
mimeType: 'text/vtt',
periodStart: 0,
role: {},
serviceLocation: 'alpha',
sourceDuration: 0,
type: 'dyanmic'
},
segmentInfo: {
template: {}
}
},
{
attributes: {
bandwidth: 256,
baseUrl: 'https://cdn2.example.com/vtt',
clientOffset: 0,
id: 'en',
lang: 'en',
mimeType: 'text/vtt',
periodStart: 0,
role: {},
serviceLocation: 'beta',
sourceDuration: 0,
type: 'dyanmic'
},
segmentInfo: {}
}
];
const playlists = [
{
attributes: {
bandwidth: 5000000,
baseUrl: 'https://cdn1.example.com/video',
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/video',
uri: ''
},
number: 1,
presentationTime: 0,
resolvedUri: 'https://cdn1.example.com/video',
timeline: 0,
uri: ''
}
]
},
{
attributes: {
bandwidth: 5000000,
baseUrl: 'https://cdn2.example.com/video',
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/video',
uri: ''
},
number: 1,
presentationTime: 0,
resolvedUri: 'https://cdn2.example.com/video',
timeline: 0,
uri: ''
}
]
},
{
attributes: {
bandwidth: 256,
baseUrl: 'https://cdn1.example.com/vtt',
clientOffset: 0,
duration: 0,
id: 'en',
lang: 'en',
mimeType: 'text/vtt',
periodStart: 0,
role: {},
serviceLocation: 'alpha',
sourceDuration: 0,
type: 'dyanmic'
},
segments: [
{
duration: 0,
map: {
resolvedUri: 'https://cdn1.example.com/vtt',
uri: ''
},
number: 1,
presentationTime: 0,
resolvedUri: 'https://cdn1.example.com/vtt',
timeline: 0,
uri: ''
}
]
},
{
attributes: {
bandwidth: 256,
baseUrl: 'https://cdn2.example.com/vtt',
clientOffset: 0,
id: 'en',
lang: 'en',
mimeType: 'text/vtt',
periodStart: 0,
role: {},
serviceLocation: 'beta',
sourceDuration: 0,
type: 'dyanmic'
}
}
];
assert.deepEqual(toPlaylists(representations), playlists);
});