You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1900 lines
61 KiB

/*! @name mpd-parser @version 0.15.4 @license Apache-2.0 */
import resolveUrl from '@videojs/vhs-utils/es/resolve-url';
import window from 'global/window';
import decodeB64ToUint8Array from '@videojs/vhs-utils/es/decode-b64-to-uint8-array';
import { DOMParser } from 'xmldom';
var version = "0.15.4";
var isObject = function isObject(obj) {
return !!obj && typeof obj === 'object';
};
var merge = function merge() {
for (var _len = arguments.length, objects = new Array(_len), _key = 0; _key < _len; _key++) {
objects[_key] = arguments[_key];
}
return objects.reduce(function (result, source) {
if (typeof source !== 'object') {
return result;
}
Object.keys(source).forEach(function (key) {
if (Array.isArray(result[key]) && Array.isArray(source[key])) {
result[key] = result[key].concat(source[key]);
} else if (isObject(result[key]) && isObject(source[key])) {
result[key] = merge(result[key], source[key]);
} else {
result[key] = source[key];
}
});
return result;
}, {});
};
var values = function values(o) {
return Object.keys(o).map(function (k) {
return o[k];
});
};
var range = function range(start, end) {
var result = [];
for (var i = start; i < end; i++) {
result.push(i);
}
return result;
};
var flatten = function flatten(lists) {
return lists.reduce(function (x, y) {
return x.concat(y);
}, []);
};
var from = function from(list) {
if (!list.length) {
return [];
}
var result = [];
for (var i = 0; i < list.length; i++) {
result.push(list[i]);
}
return result;
};
var findIndexes = function findIndexes(l, key) {
return l.reduce(function (a, e, i) {
if (e[key]) {
a.push(i);
}
return a;
}, []);
};
var errors = {
INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST',
DASH_INVALID_XML: 'DASH_INVALID_XML',
NO_BASE_URL: 'NO_BASE_URL',
MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION',
SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED',
UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME'
};
/**
* @typedef {Object} SingleUri
* @property {string} uri - relative location of segment
* @property {string} resolvedUri - resolved location of segment
* @property {Object} byterange - Object containing information on how to make byte range
* requests following byte-range-spec per RFC2616.
* @property {String} byterange.length - length of range request
* @property {String} byterange.offset - byte offset of range request
*
* @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
*/
/**
* Converts a URLType node (5.3.9.2.3 Table 13) to a segment object
* that conforms to how m3u8-parser is structured
*
* @see https://github.com/videojs/m3u8-parser
*
* @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes
* @param {string} source - source url for segment
* @param {string} range - optional range used for range calls,
* follows RFC 2616, Clause 14.35.1
* @return {SingleUri} full segment information transformed into a format similar
* to m3u8-parser
*/
var urlTypeToSegment = function urlTypeToSegment(_ref) {
var _ref$baseUrl = _ref.baseUrl,
baseUrl = _ref$baseUrl === void 0 ? '' : _ref$baseUrl,
_ref$source = _ref.source,
source = _ref$source === void 0 ? '' : _ref$source,
_ref$range = _ref.range,
range = _ref$range === void 0 ? '' : _ref$range,
_ref$indexRange = _ref.indexRange,
indexRange = _ref$indexRange === void 0 ? '' : _ref$indexRange;
var segment = {
uri: source,
resolvedUri: resolveUrl(baseUrl || '', source)
};
if (range || indexRange) {
var rangeStr = range ? range : indexRange;
var ranges = rangeStr.split('-');
var startRange = parseInt(ranges[0], 10);
var endRange = parseInt(ranges[1], 10); // byterange should be inclusive according to
// RFC 2616, Clause 14.35.1
segment.byterange = {
length: endRange - startRange + 1,
offset: startRange
};
}
return segment;
};
var byteRangeToString = function byteRangeToString(byterange) {
// `endRange` is one less than `offset + length` because the HTTP range
// header uses inclusive ranges
var endRange = byterange.offset + byterange.length - 1;
return byterange.offset + "-" + endRange;
};
/**
* parse the end number attribue that can be a string
* number, or undefined.
*
* @param {string|number|undefined} endNumber
* The end number attribute.
*
* @return {number|null}
* The result of parsing the end number.
*/
var parseEndNumber = function parseEndNumber(endNumber) {
if (endNumber && typeof endNumber !== 'number') {
endNumber = parseInt(endNumber, 10);
}
if (isNaN(endNumber)) {
return null;
}
return endNumber;
};
/**
* Functions for calculating the range of available segments in static and dynamic
* manifests.
*/
var segmentRange = {
/**
* Returns the entire range of available segments for a static MPD
*
* @param {Object} attributes
* Inheritied MPD attributes
* @return {{ start: number, end: number }}
* The start and end numbers for available segments
*/
static: function _static(attributes) {
var duration = attributes.duration,
_attributes$timescale = attributes.timescale,
timescale = _attributes$timescale === void 0 ? 1 : _attributes$timescale,
sourceDuration = attributes.sourceDuration;
var endNumber = parseEndNumber(attributes.endNumber);
return {
start: 0,
end: typeof endNumber === 'number' ? endNumber : Math.ceil(sourceDuration / (duration / timescale))
};
},
/**
* Returns the current live window range of available segments for a dynamic MPD
*
* @param {Object} attributes
* Inheritied MPD attributes
* @return {{ start: number, end: number }}
* The start and end numbers for available segments
*/
dynamic: function dynamic(attributes) {
var NOW = attributes.NOW,
clientOffset = attributes.clientOffset,
availabilityStartTime = attributes.availabilityStartTime,
_attributes$timescale2 = attributes.timescale,
timescale = _attributes$timescale2 === void 0 ? 1 : _attributes$timescale2,
duration = attributes.duration,
_attributes$start = attributes.start,
start = _attributes$start === void 0 ? 0 : _attributes$start,
_attributes$minimumUp = attributes.minimumUpdatePeriod,
minimumUpdatePeriod = _attributes$minimumUp === void 0 ? 0 : _attributes$minimumUp,
_attributes$timeShift = attributes.timeShiftBufferDepth,
timeShiftBufferDepth = _attributes$timeShift === void 0 ? Infinity : _attributes$timeShift;
var endNumber = parseEndNumber(attributes.endNumber);
var now = (NOW + clientOffset) / 1000;
var periodStartWC = availabilityStartTime + start;
var periodEndWC = now + minimumUpdatePeriod;
var periodDuration = periodEndWC - periodStartWC;
var segmentCount = Math.ceil(periodDuration * timescale / duration);
var availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration);
var availableEnd = Math.floor((now - periodStartWC) * timescale / duration);
return {
start: Math.max(0, availableStart),
end: typeof endNumber === 'number' ? endNumber : Math.min(segmentCount, availableEnd)
};
}
};
/**
* Maps a range of numbers to objects with information needed to build the corresponding
* segment list
*
* @name toSegmentsCallback
* @function
* @param {number} number
* Number of the segment
* @param {number} index
* Index of the number in the range list
* @return {{ number: Number, duration: Number, timeline: Number, time: Number }}
* Object with segment timing and duration info
*/
/**
* Returns a callback for Array.prototype.map for mapping a range of numbers to
* information needed to build the segment list.
*
* @param {Object} attributes
* Inherited MPD attributes
* @return {toSegmentsCallback}
* Callback map function
*/
var toSegments = function toSegments(attributes) {
return function (number, index) {
var duration = attributes.duration,
_attributes$timescale3 = attributes.timescale,
timescale = _attributes$timescale3 === void 0 ? 1 : _attributes$timescale3,
periodIndex = attributes.periodIndex,
_attributes$startNumb = attributes.startNumber,
startNumber = _attributes$startNumb === void 0 ? 1 : _attributes$startNumb;
return {
number: startNumber + number,
duration: duration / timescale,
timeline: periodIndex,
time: index * duration
};
};
};
/**
* Returns a list of objects containing segment timing and duration info used for
* building the list of segments. This uses the @duration attribute specified
* in the MPD manifest to derive the range of segments.
*
* @param {Object} attributes
* Inherited MPD attributes
* @return {{number: number, duration: number, time: number, timeline: number}[]}
* List of Objects with segment timing and duration info
*/
var parseByDuration = function parseByDuration(attributes) {
var _attributes$type = attributes.type,
type = _attributes$type === void 0 ? 'static' : _attributes$type,
duration = attributes.duration,
_attributes$timescale4 = attributes.timescale,
timescale = _attributes$timescale4 === void 0 ? 1 : _attributes$timescale4,
sourceDuration = attributes.sourceDuration;
var _segmentRange$type = segmentRange[type](attributes),
start = _segmentRange$type.start,
end = _segmentRange$type.end;
var segments = range(start, end).map(toSegments(attributes));
if (type === 'static') {
var index = segments.length - 1; // final segment may be less than full segment duration
segments[index].duration = sourceDuration - duration / timescale * index;
}
return segments;
};
/**
* Translates SegmentBase into a set of segments.
* (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each
* node should be translated into segment.
*
* @param {Object} attributes
* Object containing all inherited attributes from parent elements with attribute
* names as keys
* @return {Object.<Array>} list of segments
*/
var segmentsFromBase = function segmentsFromBase(attributes) {
var baseUrl = attributes.baseUrl,
_attributes$initializ = attributes.initialization,
initialization = _attributes$initializ === void 0 ? {} : _attributes$initializ,
sourceDuration = attributes.sourceDuration,
_attributes$indexRang = attributes.indexRange,
indexRange = _attributes$indexRang === void 0 ? '' : _attributes$indexRang,
duration = attributes.duration; // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1)
if (!baseUrl) {
throw new Error(errors.NO_BASE_URL);
}
var initSegment = urlTypeToSegment({
baseUrl: baseUrl,
source: initialization.sourceURL,
range: initialization.range
});
var segment = urlTypeToSegment({
baseUrl: baseUrl,
source: baseUrl,
indexRange: indexRange
});
segment.map = initSegment; // If there is a duration, use it, otherwise use the given duration of the source
// (since SegmentBase is only for one total segment)
if (duration) {
var segmentTimeInfo = parseByDuration(attributes);
if (segmentTimeInfo.length) {
segment.duration = segmentTimeInfo[0].duration;
segment.timeline = segmentTimeInfo[0].timeline;
}
} else if (sourceDuration) {
segment.duration = sourceDuration;
segment.timeline = 0;
} // This is used for mediaSequence
segment.number = 0;
return [segment];
};
/**
* Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist
* according to the sidx information given.
*
* playlist.sidx has metadadata about the sidx where-as the sidx param
* is the parsed sidx box itself.
*
* @param {Object} playlist the playlist to update the sidx information for
* @param {Object} sidx the parsed sidx box
* @return {Object} the playlist object with the updated sidx information
*/
var addSegmentsToPlaylist = function addSegmentsToPlaylist(playlist, sidx, baseUrl) {
// Retain init segment information
var initSegment = playlist.sidx.map ? playlist.sidx.map : null; // Retain source duration from initial master manifest parsing
var sourceDuration = playlist.sidx.duration; // Retain source timeline
var timeline = playlist.timeline || 0;
var sidxByteRange = playlist.sidx.byterange;
var sidxEnd = sidxByteRange.offset + sidxByteRange.length; // Retain timescale of the parsed sidx
var timescale = sidx.timescale; // referenceType 1 refers to other sidx boxes
var mediaReferences = sidx.references.filter(function (r) {
return r.referenceType !== 1;
});
var segments = []; // firstOffset is the offset from the end of the sidx box
var startIndex = sidxEnd + sidx.firstOffset;
for (var i = 0; i < mediaReferences.length; i++) {
var reference = sidx.references[i]; // size of the referenced (sub)segment
var size = reference.referencedSize; // duration of the referenced (sub)segment, in the timescale
// this will be converted to seconds when generating segments
var duration = reference.subsegmentDuration; // should be an inclusive range
var endIndex = startIndex + size - 1;
var indexRange = startIndex + "-" + endIndex;
var attributes = {
baseUrl: baseUrl,
timescale: timescale,
timeline: timeline,
// this is used in parseByDuration
periodIndex: timeline,
duration: duration,
sourceDuration: sourceDuration,
indexRange: indexRange
};
var segment = segmentsFromBase(attributes)[0];
if (initSegment) {
segment.map = initSegment;
}
segments.push(segment);
startIndex += size;
}
playlist.segments = segments;
return playlist;
};
var mergeDiscontiguousPlaylists = function mergeDiscontiguousPlaylists(playlists) {
var mergedPlaylists = values(playlists.reduce(function (acc, playlist) {
// assuming playlist IDs are the same across periods
// TODO: handle multiperiod where representation sets are not the same
// across periods
var name = playlist.attributes.id + (playlist.attributes.lang || ''); // Periods after first
if (acc[name]) {
var _acc$name$segments;
// first segment of subsequent periods signal a discontinuity
if (playlist.segments[0]) {
playlist.segments[0].discontinuity = true;
}
(_acc$name$segments = acc[name].segments).push.apply(_acc$name$segments, 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;
}
} else {
// first Period
acc[name] = playlist;
}
return acc;
}, {}));
return mergedPlaylists.map(function (playlist) {
playlist.discontinuityStarts = findIndexes(playlist.segments, 'discontinuity');
return playlist;
});
};
var addSegmentInfoFromSidx = function addSegmentInfoFromSidx(playlists, sidxMapping) {
if (sidxMapping === void 0) {
sidxMapping = {};
}
if (!Object.keys(sidxMapping).length) {
return playlists;
}
for (var i in playlists) {
var playlist = playlists[i];
if (!playlist.sidx) {
continue;
}
var sidxKey = playlist.sidx.uri + '-' + byteRangeToString(playlist.sidx.byterange);
var sidxMatch = sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;
if (playlist.sidx && sidxMatch) {
addSegmentsToPlaylist(playlist, sidxMatch, playlist.sidx.resolvedUri);
}
}
return playlists;
};
var formatAudioPlaylist = function formatAudioPlaylist(_ref) {
var _attributes;
var attributes = _ref.attributes,
segments = _ref.segments,
sidx = _ref.sidx;
var playlist = {
attributes: (_attributes = {
NAME: attributes.id,
BANDWIDTH: attributes.bandwidth,
CODECS: attributes.codecs
}, _attributes['PROGRAM-ID'] = 1, _attributes),
uri: '',
endList: (attributes.type || 'static') === 'static',
timeline: attributes.periodIndex,
resolvedUri: '',
targetDuration: attributes.duration,
segments: segments,
mediaSequence: segments.length ? segments[0].number : 1
};
if (attributes.contentProtection) {
playlist.contentProtection = attributes.contentProtection;
}
if (sidx) {
playlist.sidx = sidx;
}
return playlist;
};
var formatVttPlaylist = function formatVttPlaylist(_ref2) {
var _m3u8Attributes;
var attributes = _ref2.attributes,
segments = _ref2.segments;
if (typeof segments === 'undefined') {
// vtt tracks may use single file in BaseURL
segments = [{
uri: attributes.baseUrl,
timeline: attributes.periodIndex,
resolvedUri: attributes.baseUrl || '',
duration: attributes.sourceDuration,
number: 0
}]; // targetDuration should be the same duration as the only segment
attributes.duration = attributes.sourceDuration;
}
var m3u8Attributes = (_m3u8Attributes = {
NAME: attributes.id,
BANDWIDTH: attributes.bandwidth
}, _m3u8Attributes['PROGRAM-ID'] = 1, _m3u8Attributes);
if (attributes.codecs) {
m3u8Attributes.CODECS = attributes.codecs;
}
return {
attributes: m3u8Attributes,
uri: '',
endList: (attributes.type || 'static') === 'static',
timeline: attributes.periodIndex,
resolvedUri: attributes.baseUrl || '',
targetDuration: attributes.duration,
segments: segments,
mediaSequence: segments.length ? segments[0].number : 1
};
};
var organizeAudioPlaylists = function organizeAudioPlaylists(playlists, sidxMapping) {
if (sidxMapping === void 0) {
sidxMapping = {};
}
var mainPlaylist;
var formattedPlaylists = playlists.reduce(function (a, playlist) {
var role = playlist.attributes.role && playlist.attributes.role.value || '';
var language = playlist.attributes.lang || '';
var label = 'main';
if (language) {
var roleLabel = role ? " (" + role + ")" : '';
label = "" + playlist.attributes.lang + roleLabel;
} // skip if we already have the highest quality audio for a language
if (a[label] && a[label].playlists[0].attributes.BANDWIDTH > playlist.attributes.bandwidth) {
return a;
}
a[label] = {
language: language,
autoselect: true,
default: role === 'main',
playlists: addSegmentInfoFromSidx([formatAudioPlaylist(playlist)], sidxMapping),
uri: ''
};
if (typeof mainPlaylist === 'undefined' && role === 'main') {
mainPlaylist = playlist;
mainPlaylist.default = true;
}
return a;
}, {}); // if no playlists have role "main", mark the first as main
if (!mainPlaylist) {
var firstLabel = Object.keys(formattedPlaylists)[0];
formattedPlaylists[firstLabel].default = true;
}
return formattedPlaylists;
};
var organizeVttPlaylists = function organizeVttPlaylists(playlists, sidxMapping) {
if (sidxMapping === void 0) {
sidxMapping = {};
}
return playlists.reduce(function (a, playlist) {
var label = playlist.attributes.lang || 'text'; // skip if we already have subtitles
if (a[label]) {
return a;
}
a[label] = {
language: label,
default: false,
autoselect: false,
playlists: addSegmentInfoFromSidx([formatVttPlaylist(playlist)], sidxMapping),
uri: ''
};
return a;
}, {});
};
var formatVideoPlaylist = function formatVideoPlaylist(_ref3) {
var _attributes2;
var attributes = _ref3.attributes,
segments = _ref3.segments,
sidx = _ref3.sidx;
var playlist = {
attributes: (_attributes2 = {
NAME: attributes.id,
AUDIO: 'audio',
SUBTITLES: 'subs',
RESOLUTION: {
width: attributes.width,
height: attributes.height
},
CODECS: attributes.codecs,
BANDWIDTH: attributes.bandwidth
}, _attributes2['PROGRAM-ID'] = 1, _attributes2),
uri: '',
endList: (attributes.type || 'static') === 'static',
timeline: attributes.periodIndex,
resolvedUri: '',
targetDuration: attributes.duration,
segments: segments,
mediaSequence: segments.length ? segments[0].number : 1
};
if (attributes.contentProtection) {
playlist.contentProtection = attributes.contentProtection;
}
if (sidx) {
playlist.sidx = sidx;
}
return playlist;
};
var toM3u8 = function toM3u8(dashPlaylists, locations, sidxMapping) {
var _mediaGroups;
if (sidxMapping === void 0) {
sidxMapping = {};
}
if (!dashPlaylists.length) {
return {};
} // grab all master attributes
var _dashPlaylists$0$attr = dashPlaylists[0].attributes,
duration = _dashPlaylists$0$attr.sourceDuration,
_dashPlaylists$0$attr2 = _dashPlaylists$0$attr.type,
type = _dashPlaylists$0$attr2 === void 0 ? 'static' : _dashPlaylists$0$attr2,
suggestedPresentationDelay = _dashPlaylists$0$attr.suggestedPresentationDelay,
minimumUpdatePeriod = _dashPlaylists$0$attr.minimumUpdatePeriod;
var videoOnly = function videoOnly(_ref4) {
var attributes = _ref4.attributes;
return attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video';
};
var audioOnly = function audioOnly(_ref5) {
var attributes = _ref5.attributes;
return attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio';
};
var vttOnly = function vttOnly(_ref6) {
var attributes = _ref6.attributes;
return attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';
};
var videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist);
var audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly));
var vttPlaylists = dashPlaylists.filter(vttOnly);
var master = {
allowCache: true,
discontinuityStarts: [],
segments: [],
endList: true,
mediaGroups: (_mediaGroups = {
AUDIO: {},
VIDEO: {}
}, _mediaGroups['CLOSED-CAPTIONS'] = {}, _mediaGroups.SUBTITLES = {}, _mediaGroups),
uri: '',
duration: duration,
playlists: addSegmentInfoFromSidx(videoPlaylists, sidxMapping)
};
if (minimumUpdatePeriod >= 0) {
master.minimumUpdatePeriod = minimumUpdatePeriod * 1000;
}
if (locations) {
master.locations = locations;
}
if (type === 'dynamic') {
master.suggestedPresentationDelay = suggestedPresentationDelay;
}
if (audioPlaylists.length) {
master.mediaGroups.AUDIO.audio = organizeAudioPlaylists(audioPlaylists, sidxMapping);
}
if (vttPlaylists.length) {
master.mediaGroups.SUBTITLES.subs = organizeVttPlaylists(vttPlaylists, sidxMapping);
}
return master;
};
/**
* Calculates the R (repetition) value for a live stream (for the final segment
* in a manifest where the r value is negative 1)
*
* @param {Object} attributes
* Object containing all inherited attributes from parent elements with attribute
* names as keys
* @param {number} time
* current time (typically the total time up until the final segment)
* @param {number} duration
* duration property for the given <S />
*
* @return {number}
* R value to reach the end of the given period
*/
var getLiveRValue = function getLiveRValue(attributes, time, duration) {
var NOW = attributes.NOW,
clientOffset = attributes.clientOffset,
availabilityStartTime = attributes.availabilityStartTime,
_attributes$timescale = attributes.timescale,
timescale = _attributes$timescale === void 0 ? 1 : _attributes$timescale,
_attributes$start = attributes.start,
start = _attributes$start === void 0 ? 0 : _attributes$start,
_attributes$minimumUp = attributes.minimumUpdatePeriod,
minimumUpdatePeriod = _attributes$minimumUp === void 0 ? 0 : _attributes$minimumUp;
var now = (NOW + clientOffset) / 1000;
var periodStartWC = availabilityStartTime + start;
var periodEndWC = now + minimumUpdatePeriod;
var periodDuration = periodEndWC - periodStartWC;
return Math.ceil((periodDuration * timescale - time) / duration);
};
/**
* Uses information provided by SegmentTemplate.SegmentTimeline to determine segment
* timing and duration
*
* @param {Object} attributes
* Object containing all inherited attributes from parent elements with attribute
* names as keys
* @param {Object[]} segmentTimeline
* List of objects representing the attributes of each S element contained within
*
* @return {{number: number, duration: number, time: number, timeline: number}[]}
* List of Objects with segment timing and duration info
*/
var parseByTimeline = function parseByTimeline(attributes, segmentTimeline) {
var _attributes$type = attributes.type,
type = _attributes$type === void 0 ? 'static' : _attributes$type,
_attributes$minimumUp2 = attributes.minimumUpdatePeriod,
minimumUpdatePeriod = _attributes$minimumUp2 === void 0 ? 0 : _attributes$minimumUp2,
_attributes$media = attributes.media,
media = _attributes$media === void 0 ? '' : _attributes$media,
sourceDuration = attributes.sourceDuration,
_attributes$timescale2 = attributes.timescale,
timescale = _attributes$timescale2 === void 0 ? 1 : _attributes$timescale2,
_attributes$startNumb = attributes.startNumber,
startNumber = _attributes$startNumb === void 0 ? 1 : _attributes$startNumb,
timeline = attributes.periodIndex;
var segments = [];
var time = -1;
for (var sIndex = 0; sIndex < segmentTimeline.length; sIndex++) {
var S = segmentTimeline[sIndex];
var duration = S.d;
var repeat = S.r || 0;
var segmentTime = S.t || 0;
if (time < 0) {
// first segment
time = segmentTime;
}
if (segmentTime && segmentTime > time) {
// discontinuity
// TODO: How to handle this type of discontinuity
// timeline++ here would treat it like HLS discontuity and content would
// get appended without gap
// E.G.
// <S t="0" d="1" />
// <S d="1" />
// <S d="1" />
// <S t="5" d="1" />
// would have $Time$ values of [0, 1, 2, 5]
// should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY)
// or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP)
// does the value of sourceDuration consider this when calculating arbitrary
// negative @r repeat value?
// E.G. Same elements as above with this added at the end
// <S d="1" r="-1" />
// with a sourceDuration of 10
// Would the 2 gaps be included in the time duration calculations resulting in
// 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments
// with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ?
time = segmentTime;
}
var count = void 0;
if (repeat < 0) {
var nextS = sIndex + 1;
if (nextS === segmentTimeline.length) {
// last segment
if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) {
count = getLiveRValue(attributes, time, duration);
} else {
// TODO: This may be incorrect depending on conclusion of TODO above
count = (sourceDuration * timescale - time) / duration;
}
} else {
count = (segmentTimeline[nextS].t - time) / duration;
}
} else {
count = repeat + 1;
}
var end = startNumber + segments.length + count;
var number = startNumber + segments.length;
while (number < end) {
segments.push({
number: number,
duration: duration / timescale,
time: time,
timeline: timeline
});
time += duration;
number++;
}
}
return segments;
};
var identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g;
/**
* Replaces template identifiers with corresponding values. To be used as the callback
* for String.prototype.replace
*
* @name replaceCallback
* @function
* @param {string} match
* Entire match of identifier
* @param {string} identifier
* Name of matched identifier
* @param {string} format
* Format tag string. Its presence indicates that padding is expected
* @param {string} width
* Desired length of the replaced value. Values less than this width shall be left
* zero padded
* @return {string}
* Replacement for the matched identifier
*/
/**
* Returns a function to be used as a callback for String.prototype.replace to replace
* template identifiers
*
* @param {Obect} values
* Object containing values that shall be used to replace known identifiers
* @param {number} values.RepresentationID
* Value of the Representation@id attribute
* @param {number} values.Number
* Number of the corresponding segment
* @param {number} values.Bandwidth
* Value of the Representation@bandwidth attribute.
* @param {number} values.Time
* Timestamp value of the corresponding segment
* @return {replaceCallback}
* Callback to be used with String.prototype.replace to replace identifiers
*/
var identifierReplacement = function identifierReplacement(values) {
return function (match, identifier, format, width) {
if (match === '$$') {
// escape sequence
return '$';
}
if (typeof values[identifier] === 'undefined') {
return match;
}
var value = '' + values[identifier];
if (identifier === 'RepresentationID') {
// Format tag shall not be present with RepresentationID
return value;
}
if (!format) {
width = 1;
} else {
width = parseInt(width, 10);
}
if (value.length >= width) {
return value;
}
return "" + new Array(width - value.length + 1).join('0') + value;
};
};
/**
* Constructs a segment url from a template string
*
* @param {string} url
* Template string to construct url from
* @param {Obect} values
* Object containing values that shall be used to replace known identifiers
* @param {number} values.RepresentationID
* Value of the Representation@id attribute
* @param {number} values.Number
* Number of the corresponding segment
* @param {number} values.Bandwidth
* Value of the Representation@bandwidth attribute.
* @param {number} values.Time
* Timestamp value of the corresponding segment
* @return {string}
* Segment url with identifiers replaced
*/
var constructTemplateUrl = function constructTemplateUrl(url, values) {
return url.replace(identifierPattern, identifierReplacement(values));
};
/**
* Generates a list of objects containing timing and duration information about each
* segment needed to generate segment uris and the complete segment object
*
* @param {Object} attributes
* Object containing all inherited attributes from parent elements with attribute
* names as keys
* @param {Object[]|undefined} segmentTimeline
* List of objects representing the attributes of each S element contained within
* the SegmentTimeline element
* @return {{number: number, duration: number, time: number, timeline: number}[]}
* List of Objects with segment timing and duration info
*/
var parseTemplateInfo = function parseTemplateInfo(attributes, segmentTimeline) {
if (!attributes.duration && !segmentTimeline) {
// if neither @duration or SegmentTimeline are present, then there shall be exactly
// one media segment
return [{
number: attributes.startNumber || 1,
duration: attributes.sourceDuration,
time: 0,
timeline: attributes.periodIndex
}];
}
if (attributes.duration) {
return parseByDuration(attributes);
}
return parseByTimeline(attributes, segmentTimeline);
};
/**
* Generates a list of segments using information provided by the SegmentTemplate element
*
* @param {Object} attributes
* Object containing all inherited attributes from parent elements with attribute
* names as keys
* @param {Object[]|undefined} segmentTimeline
* List of objects representing the attributes of each S element contained within
* the SegmentTimeline element
* @return {Object[]}
* List of segment objects
*/
var segmentsFromTemplate = function segmentsFromTemplate(attributes, segmentTimeline) {
var templateValues = {
RepresentationID: attributes.id,
Bandwidth: attributes.bandwidth || 0
};
var _attributes$initializ = attributes.initialization,
initialization = _attributes$initializ === void 0 ? {
sourceURL: '',
range: ''
} : _attributes$initializ;
var mapSegment = urlTypeToSegment({
baseUrl: attributes.baseUrl,
source: constructTemplateUrl(initialization.sourceURL, templateValues),
range: initialization.range
});
var segments = parseTemplateInfo(attributes, segmentTimeline);
return segments.map(function (segment) {
templateValues.Number = segment.number;
templateValues.Time = segment.time;
var uri = constructTemplateUrl(attributes.media || '', templateValues);
return {
uri: uri,
timeline: segment.timeline,
duration: segment.duration,
resolvedUri: resolveUrl(attributes.baseUrl || '', uri),
map: mapSegment,
number: segment.number
};
});
};
/**
* Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14)
* to an object that matches the output of a segment in videojs/mpd-parser
*
* @param {Object} attributes
* Object containing all inherited attributes from parent elements with attribute
* names as keys
* @param {Object} segmentUrl
* <SegmentURL> node to translate into a segment object
* @return {Object} translated segment object
*/
var SegmentURLToSegmentObject = function SegmentURLToSegmentObject(attributes, segmentUrl) {
var baseUrl = attributes.baseUrl,
_attributes$initializ = attributes.initialization,
initialization = _attributes$initializ === void 0 ? {} : _attributes$initializ;
var initSegment = urlTypeToSegment({
baseUrl: baseUrl,
source: initialization.sourceURL,
range: initialization.range
});
var segment = urlTypeToSegment({
baseUrl: baseUrl,
source: segmentUrl.media,
range: segmentUrl.mediaRange
});
segment.map = initSegment;
return segment;
};
/**
* Generates a list of segments using information provided by the SegmentList element
* SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each
* node should be translated into segment.
*
* @param {Object} attributes
* Object containing all inherited attributes from parent elements with attribute
* names as keys
* @param {Object[]|undefined} segmentTimeline
* List of objects representing the attributes of each S element contained within
* the SegmentTimeline element
* @return {Object.<Array>} list of segments
*/
var segmentsFromList = function segmentsFromList(attributes, segmentTimeline) {
var duration = attributes.duration,
_attributes$segmentUr = attributes.segmentUrls,
segmentUrls = _attributes$segmentUr === void 0 ? [] : _attributes$segmentUr; // Per spec (5.3.9.2.1) no way to determine segment duration OR
// if both SegmentTimeline and @duration are defined, it is outside of spec.
if (!duration && !segmentTimeline || duration && segmentTimeline) {
throw new Error(errors.SEGMENT_TIME_UNSPECIFIED);
}
var segmentUrlMap = segmentUrls.map(function (segmentUrlObject) {
return SegmentURLToSegmentObject(attributes, segmentUrlObject);
});
var segmentTimeInfo;
if (duration) {
segmentTimeInfo = parseByDuration(attributes);
}
if (segmentTimeline) {
segmentTimeInfo = parseByTimeline(attributes, segmentTimeline);
}
var segments = segmentTimeInfo.map(function (segmentTime, index) {
if (segmentUrlMap[index]) {
var segment = segmentUrlMap[index];
segment.timeline = segmentTime.timeline;
segment.duration = segmentTime.duration;
segment.number = segmentTime.number;
return segment;
} // Since we're mapping we should get rid of any blank segments (in case
// the given SegmentTimeline is handling for more elements than we have
// SegmentURLs for).
}).filter(function (segment) {
return segment;
});
return segments;
};
var generateSegments = function generateSegments(_ref) {
var attributes = _ref.attributes,
segmentInfo = _ref.segmentInfo;
var segmentAttributes;
var segmentsFn;
if (segmentInfo.template) {
segmentsFn = segmentsFromTemplate;
segmentAttributes = merge(attributes, segmentInfo.template);
} else if (segmentInfo.base) {
segmentsFn = segmentsFromBase;
segmentAttributes = merge(attributes, segmentInfo.base);
} else if (segmentInfo.list) {
segmentsFn = segmentsFromList;
segmentAttributes = merge(attributes, segmentInfo.list);
}
var segmentsInfo = {
attributes: attributes
};
if (!segmentsFn) {
return segmentsInfo;
}
var segments = segmentsFn(segmentAttributes, segmentInfo.timeline); // The @duration attribute will be used to determin the playlist's targetDuration which
// must be in seconds. Since we've generated the segment list, we no longer need
// @duration to be in @timescale units, so we can convert it here.
if (segmentAttributes.duration) {
var _segmentAttributes = segmentAttributes,
duration = _segmentAttributes.duration,
_segmentAttributes$ti = _segmentAttributes.timescale,
timescale = _segmentAttributes$ti === void 0 ? 1 : _segmentAttributes$ti;
segmentAttributes.duration = duration / timescale;
} else if (segments.length) {
// if there is no @duration attribute, use the largest segment duration as
// as target duration
segmentAttributes.duration = segments.reduce(function (max, segment) {
return Math.max(max, Math.ceil(segment.duration));
}, 0);
} else {
segmentAttributes.duration = 0;
}
segmentsInfo.attributes = segmentAttributes;
segmentsInfo.segments = segments; // This is a sidx box without actual segment information
if (segmentInfo.base && segmentAttributes.indexRange) {
segmentsInfo.sidx = segments[0];
segmentsInfo.segments = [];
}
return segmentsInfo;
};
var toPlaylists = function toPlaylists(representations) {
return representations.map(generateSegments);
};
var findChildren = function findChildren(element, name) {
return from(element.childNodes).filter(function (_ref) {
var tagName = _ref.tagName;
return tagName === name;
});
};
var getContent = function getContent(element) {
return element.textContent.trim();
};
var parseDuration = function parseDuration(str) {
var SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
var SECONDS_IN_MONTH = 30 * 24 * 60 * 60;
var SECONDS_IN_DAY = 24 * 60 * 60;
var SECONDS_IN_HOUR = 60 * 60;
var SECONDS_IN_MIN = 60; // P10Y10M10DT10H10M10.1S
var durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/;
var match = durationRegex.exec(str);
if (!match) {
return 0;
}
var _match$slice = match.slice(1),
year = _match$slice[0],
month = _match$slice[1],
day = _match$slice[2],
hour = _match$slice[3],
minute = _match$slice[4],
second = _match$slice[5];
return parseFloat(year || 0) * SECONDS_IN_YEAR + parseFloat(month || 0) * SECONDS_IN_MONTH + parseFloat(day || 0) * SECONDS_IN_DAY + parseFloat(hour || 0) * SECONDS_IN_HOUR + parseFloat(minute || 0) * SECONDS_IN_MIN + parseFloat(second || 0);
};
var parseDate = function parseDate(str) {
// Date format without timezone according to ISO 8601
// YYY-MM-DDThh:mm:ss.ssssss
var dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/; // If the date string does not specifiy a timezone, we must specifiy UTC. This is
// expressed by ending with 'Z'
if (dateRegex.test(str)) {
str += 'Z';
}
return Date.parse(str);
};
var parsers = {
/**
* Specifies the duration of the entire Media Presentation. Format is a duration string
* as specified in ISO 8601
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The duration in seconds
*/
mediaPresentationDuration: function mediaPresentationDuration(value) {
return parseDuration(value);
},
/**
* Specifies the Segment availability start time for all Segments referred to in this
* MPD. For a dynamic manifest, it specifies the anchor for the earliest availability
* time. Format is a date string as specified in ISO 8601
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The date as seconds from unix epoch
*/
availabilityStartTime: function availabilityStartTime(value) {
return parseDate(value) / 1000;
},
/**
* Specifies the smallest period between potential changes to the MPD. Format is a
* duration string as specified in ISO 8601
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The duration in seconds
*/
minimumUpdatePeriod: function minimumUpdatePeriod(value) {
return parseDuration(value);
},
/**
* Specifies the suggested presentation delay. Format is a
* duration string as specified in ISO 8601
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The duration in seconds
*/
suggestedPresentationDelay: function suggestedPresentationDelay(value) {
return parseDuration(value);
},
/**
* specifices the type of mpd. Can be either "static" or "dynamic"
*
* @param {string} value
* value of attribute as a string
*
* @return {string}
* The type as a string
*/
type: function type(value) {
return value;
},
/**
* Specifies the duration of the smallest time shifting buffer for any Representation
* in the MPD. Format is a duration string as specified in ISO 8601
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The duration in seconds
*/
timeShiftBufferDepth: function timeShiftBufferDepth(value) {
return parseDuration(value);
},
/**
* Specifies the PeriodStart time of the Period relative to the availabilityStarttime.
* Format is a duration string as specified in ISO 8601
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The duration in seconds
*/
start: function start(value) {
return parseDuration(value);
},
/**
* Specifies the width of the visual presentation
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The parsed width
*/
width: function width(value) {
return parseInt(value, 10);
},
/**
* Specifies the height of the visual presentation
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The parsed height
*/
height: function height(value) {
return parseInt(value, 10);
},
/**
* Specifies the bitrate of the representation
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The parsed bandwidth
*/
bandwidth: function bandwidth(value) {
return parseInt(value, 10);
},
/**
* Specifies the number of the first Media Segment in this Representation in the Period
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The parsed number
*/
startNumber: function startNumber(value) {
return parseInt(value, 10);
},
/**
* Specifies the timescale in units per seconds
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The aprsed timescale
*/
timescale: function timescale(value) {
return parseInt(value, 10);
},
/**
* Specifies the constant approximate Segment duration
* NOTE: The <Period> element also contains an @duration attribute. This duration
* specifies the duration of the Period. This attribute is currently not
* supported by the rest of the parser, however we still check for it to prevent
* errors.
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The parsed duration
*/
duration: function duration(value) {
var parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) {
return parseDuration(value);
}
return parsedValue;
},
/**
* Specifies the Segment duration, in units of the value of the @timescale.
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The parsed duration
*/
d: function d(value) {
return parseInt(value, 10);
},
/**
* Specifies the MPD start time, in @timescale units, the first Segment in the series
* starts relative to the beginning of the Period
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The parsed time
*/
t: function t(value) {
return parseInt(value, 10);
},
/**
* Specifies the repeat count of the number of following contiguous Segments with the
* same duration expressed by the value of @d
*
* @param {string} value
* value of attribute as a string
* @return {number}
* The parsed number
*/
r: function r(value) {
return parseInt(value, 10);
},
/**
* Default parser for all other attributes. Acts as a no-op and just returns the value
* as a string
*
* @param {string} value
* value of attribute as a string
* @return {string}
* Unparsed value
*/
DEFAULT: function DEFAULT(value) {
return value;
}
};
/**
* Gets all the attributes and values of the provided node, parses attributes with known
* types, and returns an object with attribute names mapped to values.
*
* @param {Node} el
* The node to parse attributes from
* @return {Object}
* Object with all attributes of el parsed
*/
var parseAttributes = function parseAttributes(el) {
if (!(el && el.attributes)) {
return {};
}
return from(el.attributes).reduce(function (a, e) {
var parseFn = parsers[e.name] || parsers.DEFAULT;
a[e.name] = parseFn(e.value);
return a;
}, {});
};
var keySystemsMap = {
'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey',
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha',
'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready',
'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime'
};
/**
* Builds a list of urls that is the product of the reference urls and BaseURL values
*
* @param {string[]} referenceUrls
* List of reference urls to resolve to
* @param {Node[]} baseUrlElements
* List of BaseURL nodes from the mpd
* @return {string[]}
* List of resolved urls
*/
var buildBaseUrls = function buildBaseUrls(referenceUrls, baseUrlElements) {
if (!baseUrlElements.length) {
return referenceUrls;
}
return flatten(referenceUrls.map(function (reference) {
return baseUrlElements.map(function (baseUrlElement) {
return resolveUrl(reference, getContent(baseUrlElement));
});
}));
};
/**
* Contains all Segment information for its containing AdaptationSet
*
* @typedef {Object} SegmentInformation
* @property {Object|undefined} template
* Contains the attributes for the SegmentTemplate node
* @property {Object[]|undefined} timeline
* Contains a list of atrributes for each S node within the SegmentTimeline node
* @property {Object|undefined} list
* Contains the attributes for the SegmentList node
* @property {Object|undefined} base
* Contains the attributes for the SegmentBase node
*/
/**
* Returns all available Segment information contained within the AdaptationSet node
*
* @param {Node} adaptationSet
* The AdaptationSet node to get Segment information from
* @return {SegmentInformation}
* The Segment information contained within the provided AdaptationSet
*/
var getSegmentInformation = function getSegmentInformation(adaptationSet) {
var segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0];
var segmentList = findChildren(adaptationSet, 'SegmentList')[0];
var segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(function (s) {
return merge({
tag: 'SegmentURL'
}, parseAttributes(s));
});
var segmentBase = findChildren(adaptationSet, 'SegmentBase')[0];
var segmentTimelineParentNode = segmentList || segmentTemplate;
var segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0];
var segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate;
var segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; // SegmentTemplate is handled slightly differently, since it can have both
// @initialization and an <Initialization> node. @initialization can be templated,
// while the node can have a url and range specified. If the <SegmentTemplate> has
// both @initialization and an <Initialization> subelement we opt to override with
// the node, as this interaction is not defined in the spec.
var template = segmentTemplate && parseAttributes(segmentTemplate);
if (template && segmentInitialization) {
template.initialization = segmentInitialization && parseAttributes(segmentInitialization);
} else if (template && template.initialization) {
// If it is @initialization we convert it to an object since this is the format that
// later functions will rely on for the initialization segment. This is only valid
// for <SegmentTemplate>
template.initialization = {
sourceURL: template.initialization
};
}
var segmentInfo = {
template: template,
timeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(function (s) {
return parseAttributes(s);
}),
list: segmentList && merge(parseAttributes(segmentList), {
segmentUrls: segmentUrls,
initialization: parseAttributes(segmentInitialization)
}),
base: segmentBase && merge(parseAttributes(segmentBase), {
initialization: parseAttributes(segmentInitialization)
})
};
Object.keys(segmentInfo).forEach(function (key) {
if (!segmentInfo[key]) {
delete segmentInfo[key];
}
});
return segmentInfo;
};
/**
* Contains Segment information and attributes needed to construct a Playlist object
* from a Representation
*
* @typedef {Object} RepresentationInformation
* @property {SegmentInformation} segmentInfo
* Segment information for this Representation
* @property {Object} attributes
* Inherited attributes for this Representation
*/
/**
* Maps a Representation node to an object containing Segment information and attributes
*
* @name inheritBaseUrlsCallback
* @function
* @param {Node} representation
* Representation node from the mpd
* @return {RepresentationInformation}
* Representation information needed to construct a Playlist object
*/
/**
* Returns a callback for Array.prototype.map for mapping Representation nodes to
* Segment information and attributes using inherited BaseURL nodes.
*
* @param {Object} adaptationSetAttributes
* Contains attributes inherited by the AdaptationSet
* @param {string[]} adaptationSetBaseUrls
* Contains list of resolved base urls inherited by the AdaptationSet
* @param {SegmentInformation} adaptationSetSegmentInfo
* Contains Segment information for the AdaptationSet
* @return {inheritBaseUrlsCallback}
* Callback map function
*/
var inheritBaseUrls = function inheritBaseUrls(adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) {
return function (representation) {
var repBaseUrlElements = findChildren(representation, 'BaseURL');
var repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements);
var attributes = merge(adaptationSetAttributes, parseAttributes(representation));
var representationSegmentInfo = getSegmentInformation(representation);
return repBaseUrls.map(function (baseUrl) {
return {
segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
attributes: merge(attributes, {
baseUrl: baseUrl
})
};
});
};
};
/**
* Tranforms a series of content protection nodes to
* an object containing pssh data by key system
*
* @param {Node[]} contentProtectionNodes
* Content protection nodes
* @return {Object}
* Object containing pssh data by key system
*/
var generateKeySystemInformation = function generateKeySystemInformation(contentProtectionNodes) {
return contentProtectionNodes.reduce(function (acc, node) {
var attributes = parseAttributes(node);
var keySystem = keySystemsMap[attributes.schemeIdUri];
if (keySystem) {
acc[keySystem] = {
attributes: attributes
};
var psshNode = findChildren(node, 'cenc:pssh')[0];
if (psshNode) {
var pssh = getContent(psshNode);
var psshBuffer = pssh && decodeB64ToUint8Array(pssh);
acc[keySystem].pssh = psshBuffer;
}
}
return acc;
}, {});
};
/**
* Maps an AdaptationSet node to a list of Representation information objects
*
* @name toRepresentationsCallback
* @function
* @param {Node} adaptationSet
* AdaptationSet node from the mpd
* @return {RepresentationInformation[]}
* List of objects containing Representaion information
*/
/**
* Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of
* Representation information objects
*
* @param {Object} periodAttributes
* Contains attributes inherited by the Period
* @param {string[]} periodBaseUrls
* Contains list of resolved base urls inherited by the Period
* @param {string[]} periodSegmentInfo
* Contains Segment Information at the period level
* @return {toRepresentationsCallback}
* Callback map function
*/
var toRepresentations = function toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo) {
return function (adaptationSet) {
var adaptationSetAttributes = parseAttributes(adaptationSet);
var adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL'));
var role = findChildren(adaptationSet, 'Role')[0];
var roleAttributes = {
role: parseAttributes(role)
};
var attrs = merge(periodAttributes, adaptationSetAttributes, roleAttributes);
var contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection'));
if (Object.keys(contentProtection).length) {
attrs = merge(attrs, {
contentProtection: contentProtection
});
}
var segmentInfo = getSegmentInformation(adaptationSet);
var representations = findChildren(adaptationSet, 'Representation');
var adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo);
return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo)));
};
};
/**
* Maps an Period node to a list of Representation inforamtion objects for all
* AdaptationSet nodes contained within the Period
*
* @name toAdaptationSetsCallback
* @function
* @param {Node} period
* Period node from the mpd
* @param {number} periodIndex
* Index of the Period within the mpd
* @return {RepresentationInformation[]}
* List of objects containing Representaion information
*/
/**
* Returns a callback for Array.prototype.map for mapping Period nodes to a list of
* Representation information objects
*
* @param {Object} mpdAttributes
* Contains attributes inherited by the mpd
* @param {string[]} mpdBaseUrls
* Contains list of resolved base urls inherited by the mpd
* @return {toAdaptationSetsCallback}
* Callback map function
*/
var toAdaptationSets = function toAdaptationSets(mpdAttributes, mpdBaseUrls) {
return function (period, index) {
var periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period, 'BaseURL'));
var periodAtt = parseAttributes(period);
var parsedPeriodId = parseInt(periodAtt.id, 10); // fallback to mapping index if Period@id is not a number
var periodIndex = window.isNaN(parsedPeriodId) ? index : parsedPeriodId;
var periodAttributes = merge(mpdAttributes, {
periodIndex: periodIndex
});
var adaptationSets = findChildren(period, 'AdaptationSet');
var periodSegmentInfo = getSegmentInformation(period);
return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo)));
};
};
/**
* Traverses the mpd xml tree to generate a list of Representation information objects
* that have inherited attributes from parent nodes
*
* @param {Node} mpd
* The root node of the mpd
* @param {Object} options
* Available options for inheritAttributes
* @param {string} options.manifestUri
* The uri source of the mpd
* @param {number} options.NOW
* Current time per DASH IOP. Default is current time in ms since epoch
* @param {number} options.clientOffset
* Client time difference from NOW (in milliseconds)
* @return {RepresentationInformation[]}
* List of objects containing Representation information
*/
var inheritAttributes = function inheritAttributes(mpd, options) {
if (options === void 0) {
options = {};
}
var _options = options,
_options$manifestUri = _options.manifestUri,
manifestUri = _options$manifestUri === void 0 ? '' : _options$manifestUri,
_options$NOW = _options.NOW,
NOW = _options$NOW === void 0 ? Date.now() : _options$NOW,
_options$clientOffset = _options.clientOffset,
clientOffset = _options$clientOffset === void 0 ? 0 : _options$clientOffset;
var periods = findChildren(mpd, 'Period');
if (!periods.length) {
throw new Error(errors.INVALID_NUMBER_OF_PERIOD);
}
var locations = findChildren(mpd, 'Location');
var mpdAttributes = parseAttributes(mpd);
var mpdBaseUrls = buildBaseUrls([manifestUri], findChildren(mpd, 'BaseURL'));
mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0;
mpdAttributes.NOW = NOW;
mpdAttributes.clientOffset = clientOffset;
if (locations.length) {
mpdAttributes.locations = locations.map(getContent);
}
return {
locations: mpdAttributes.locations,
representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls)))
};
};
var stringToMpdXml = function stringToMpdXml(manifestString) {
if (manifestString === '') {
throw new Error(errors.DASH_EMPTY_MANIFEST);
}
var parser = new DOMParser();
var xml;
var mpd;
try {
xml = parser.parseFromString(manifestString, 'application/xml');
mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null;
} catch (e) {// ie 11 throwsw on invalid xml
}
if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) {
throw new Error(errors.DASH_INVALID_XML);
}
return mpd;
};
/**
* Parses the manifest for a UTCTiming node, returning the nodes attributes if found
*
* @param {string} mpd
* XML string of the MPD manifest
* @return {Object|null}
* Attributes of UTCTiming node specified in the manifest. Null if none found
*/
var parseUTCTimingScheme = function parseUTCTimingScheme(mpd) {
var UTCTimingNode = findChildren(mpd, 'UTCTiming')[0];
if (!UTCTimingNode) {
return null;
}
var attributes = parseAttributes(UTCTimingNode);
switch (attributes.schemeIdUri) {
case 'urn:mpeg:dash:utc:http-head:2014':
case 'urn:mpeg:dash:utc:http-head:2012':
attributes.method = 'HEAD';
break;
case 'urn:mpeg:dash:utc:http-xsdate:2014':
case 'urn:mpeg:dash:utc:http-iso:2014':
case 'urn:mpeg:dash:utc:http-xsdate:2012':
case 'urn:mpeg:dash:utc:http-iso:2012':
attributes.method = 'GET';
break;
case 'urn:mpeg:dash:utc:direct:2014':
case 'urn:mpeg:dash:utc:direct:2012':
attributes.method = 'DIRECT';
attributes.value = Date.parse(attributes.value);
break;
case 'urn:mpeg:dash:utc:http-ntp:2014':
case 'urn:mpeg:dash:utc:ntp:2014':
case 'urn:mpeg:dash:utc:sntp:2014':
default:
throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME);
}
return attributes;
};
var VERSION = version;
var parse = function parse(manifestString, options) {
if (options === void 0) {
options = {};
}
var parsedManifestInfo = inheritAttributes(stringToMpdXml(manifestString), options);
var playlists = toPlaylists(parsedManifestInfo.representationInfo);
return toM3u8(playlists, parsedManifestInfo.locations, options.sidxMapping);
};
/**
* Parses the manifest for a UTCTiming node, returning the nodes attributes if found
*
* @param {string} manifestString
* XML string of the MPD manifest
* @return {Object|null}
* Attributes of UTCTiming node specified in the manifest. Null if none found
*/
var parseUTCTiming = function parseUTCTiming(manifestString) {
return parseUTCTimingScheme(stringToMpdXml(manifestString));
};
var addSidxSegmentsToPlaylist = addSegmentsToPlaylist;
export { VERSION, addSidxSegmentsToPlaylist, inheritAttributes, parse, parseUTCTiming, stringToMpdXml, toM3u8, toPlaylists };