mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-15 16:06:41 +03:00
da03f4391a
These patches and IPR support in general will be revisited when we have a better model for how this should work. Closes #463 Change-Id: I2c5fe372986a9af232b052c67f8e5c3ba6585cee
481 lines
16 KiB
JavaScript
481 lines
16 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2016 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
goog.provide('shaka.dash.MpdUtils');
|
|
|
|
goog.require('goog.Uri');
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.media.SegmentReference');
|
|
goog.require('shaka.util.Functional');
|
|
goog.require('shaka.util.XmlUtils');
|
|
|
|
|
|
/**
|
|
* @namespace shaka.dash.MpdUtils
|
|
* @summary MPD processing utility functions.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Specifies how tolerant the player is to inaccurate segment start times and
|
|
* end times within a manifest. For example, gaps or overlaps between segments
|
|
* in a SegmentTimeline which are greater than or equal to this value will
|
|
* result in a warning message.
|
|
*
|
|
* @const {number}
|
|
*/
|
|
shaka.dash.MpdUtils.GAP_OVERLAP_TOLERANCE_SECONDS = 1 / 15;
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* start: number,
|
|
* end: number
|
|
* }}
|
|
*
|
|
* @description
|
|
* Defines a time range of a media segment. Times are in seconds.
|
|
*
|
|
* @property {number} start
|
|
* The start time of the range.
|
|
* @property {number} end
|
|
* The end time (exclusive) of the range.
|
|
*/
|
|
shaka.dash.MpdUtils.TimeRange;
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* timescale: number,
|
|
* segmentDuration: ?number,
|
|
* startNumber: number,
|
|
* presentationTimeOffset: number,
|
|
* timeline: Array.<shaka.dash.MpdUtils.TimeRange>
|
|
* }}
|
|
*
|
|
* @description
|
|
* Contains common information between SegmentList and SegmentTemplate items.
|
|
*
|
|
* @property {number} timescale
|
|
* The time-scale of the representation.
|
|
* @property {?number} segmentDuration
|
|
* The duration of the segments in seconds, if given.
|
|
* @property {number} startNumber
|
|
* The start number of the segments; 1 or greater.
|
|
* @property {number} presentationTimeOffset
|
|
* The presentationTimeOffset of the representation, in seconds.
|
|
* @property {Array.<shaka.dash.MpdUtils.TimeRange>} timeline
|
|
* The timeline of the representation, if given. Times in seconds.
|
|
*/
|
|
shaka.dash.MpdUtils.SegmentInfo;
|
|
|
|
|
|
/**
|
|
* Fills a SegmentTemplate URI template. This function does not validate the
|
|
* resulting URI.
|
|
*
|
|
* @param {string} uriTemplate
|
|
* @param {?string} representationId
|
|
* @param {?number} number
|
|
* @param {?number} bandwidth
|
|
* @param {?number} time
|
|
* @return {string} A URI string.
|
|
* @see ISO/IEC 23009-1:2014 section 5.3.9.4.4
|
|
*/
|
|
shaka.dash.MpdUtils.fillUriTemplate = function(
|
|
uriTemplate, representationId, number, bandwidth, time) {
|
|
if (time !== null) {
|
|
goog.asserts.assert(Math.abs(time - Math.round(time)) < 0.2,
|
|
'Calculated $Time$ values must be close to integers!');
|
|
time = Math.round(time);
|
|
}
|
|
|
|
/** @type {!Object.<string, ?number|?string>} */
|
|
var valueTable = {
|
|
'RepresentationID': representationId,
|
|
'Number': number,
|
|
'Bandwidth': bandwidth,
|
|
'Time': time
|
|
};
|
|
|
|
var re = /\$(RepresentationID|Number|Bandwidth|Time)?(?:%0([0-9]+)d)?\$/g;
|
|
var uri = uriTemplate.replace(re, function(match, name, widthString) {
|
|
if (match == '$$') {
|
|
return '$';
|
|
}
|
|
|
|
var value = valueTable[name];
|
|
goog.asserts.assert(value !== undefined, 'Unrecognized identifier');
|
|
|
|
// Note that |value| may be 0 or ''.
|
|
if (value == null) {
|
|
shaka.log.warning(
|
|
'URL template does not have an available substitution for identifier',
|
|
'"' + name + '":',
|
|
uriTemplate);
|
|
return match;
|
|
}
|
|
|
|
if (name == 'RepresentationID' && widthString) {
|
|
shaka.log.warning(
|
|
'URL template should not contain a width specifier for identifier',
|
|
'"RepresentationID":',
|
|
uriTemplate);
|
|
widthString = undefined;
|
|
}
|
|
|
|
var valueString = value.toString();
|
|
|
|
// Create padding string.
|
|
var width = window.parseInt(widthString, 10) || 1;
|
|
var paddingSize = Math.max(0, width - valueString.length);
|
|
var padding = (new Array(paddingSize + 1)).join('0');
|
|
|
|
return padding + valueString;
|
|
});
|
|
|
|
return uri;
|
|
};
|
|
|
|
|
|
/**
|
|
* Expands a SegmentTimeline into an array-based timeline. The results are in
|
|
* seconds.
|
|
*
|
|
* @param {!Element} segmentTimeline
|
|
* @param {number} timescale
|
|
* @param {number} presentationTimeOffset
|
|
* @param {number} periodDuration The Period's duration in seconds.
|
|
* POSITIVE_INFINITY indicates that the Period continues indefinitely.
|
|
* @return {!Array.<shaka.dash.MpdUtils.TimeRange>}
|
|
*/
|
|
shaka.dash.MpdUtils.createTimeline = function(
|
|
segmentTimeline, timescale, presentationTimeOffset, periodDuration) {
|
|
goog.asserts.assert(
|
|
timescale > 0 && timescale < Number.POSITIVE_INFINITY,
|
|
'timescale must be a positive, finite integer');
|
|
goog.asserts.assert(periodDuration > 0,
|
|
'period duration must be a positive integer');
|
|
|
|
// Alias.
|
|
var XmlUtils = shaka.util.XmlUtils;
|
|
|
|
var timePoints = XmlUtils.findChildren(segmentTimeline, 'S');
|
|
|
|
/** @type {!Array.<shaka.dash.MpdUtils.TimeRange>} */
|
|
var timeline = [];
|
|
var lastEndTime = 0;
|
|
|
|
for (var i = 0; i < timePoints.length; ++i) {
|
|
var timePoint = timePoints[i];
|
|
var t = XmlUtils.parseAttr(timePoint, 't', XmlUtils.parseNonNegativeInt);
|
|
var d = XmlUtils.parseAttr(timePoint, 'd', XmlUtils.parseNonNegativeInt);
|
|
var r = XmlUtils.parseAttr(timePoint, 'r', XmlUtils.parseInt);
|
|
|
|
// Adjust start considering the presentation time offset
|
|
if (t != null)
|
|
t -= presentationTimeOffset;
|
|
|
|
if (!d) {
|
|
shaka.log.warning(
|
|
'"S" element must have a duration:',
|
|
'ignoring the remaining "S" elements.',
|
|
timePoint);
|
|
return timeline;
|
|
}
|
|
|
|
var startTime = t != null ? t : lastEndTime;
|
|
|
|
var repeat = r || 0;
|
|
if (repeat < 0) {
|
|
if (i + 1 < timePoints.length) {
|
|
var nextTimePoint = timePoints[i + 1];
|
|
var nextStartTime = XmlUtils.parseAttr(
|
|
nextTimePoint, 't', XmlUtils.parseNonNegativeInt);
|
|
if (nextStartTime == null) {
|
|
shaka.log.warning(
|
|
'"S" element cannot have a negative repeat',
|
|
'if the next "S" element does not have a valid start time:',
|
|
'ignoring the remaining "S" elements.',
|
|
timePoint);
|
|
return timeline;
|
|
} else if (startTime >= nextStartTime) {
|
|
shaka.log.warning(
|
|
'"S" element cannot have a negative repeat',
|
|
'if its start time exceeds the next "S" element\'s start time:',
|
|
'ignoring the remaining "S" elements.',
|
|
timePoint);
|
|
return timeline;
|
|
}
|
|
repeat = Math.ceil((nextStartTime - startTime) / d) - 1;
|
|
} else {
|
|
if (periodDuration == Number.POSITIVE_INFINITY) {
|
|
// The DASH spec. actually allows the last "S" element to have a
|
|
// negative repeat value even when the Period has an infinite
|
|
// duration. No one uses this feature and no one ever should, ever.
|
|
shaka.log.warning(
|
|
'The last "S" element cannot have a negative repeat',
|
|
'if the Period has an infinite duration:',
|
|
'ignoring the last "S" element.',
|
|
timePoint);
|
|
return timeline;
|
|
} else if (startTime / timescale >= periodDuration) {
|
|
shaka.log.warning(
|
|
'The last "S" element cannot have a negative repeat',
|
|
'if its start time exceeds the Period\'s duration:',
|
|
'igoring the last "S" element.',
|
|
timePoint);
|
|
return timeline;
|
|
}
|
|
repeat = Math.ceil((periodDuration * timescale - startTime) / d) - 1;
|
|
}
|
|
}
|
|
|
|
// The end of the last segment may end before the start of the current
|
|
// segment (a gap) or may end after the start of the current segment (an
|
|
// overlap). If there is a gap/overlap then stretch/compress the end of
|
|
// the last segment to the start of the current segment.
|
|
//
|
|
// Note: it is possible to move the start of the current segment to the
|
|
// end of the last segment, but this would complicate the computation of
|
|
// the $Time$ placeholder later on.
|
|
if ((timeline.length > 0) && (startTime != lastEndTime)) {
|
|
var delta = startTime - lastEndTime;
|
|
|
|
if (Math.abs(delta / timescale) >=
|
|
shaka.dash.MpdUtils.GAP_OVERLAP_TOLERANCE_SECONDS) {
|
|
shaka.log.warning(
|
|
'SegmentTimeline contains a large gap/overlap:',
|
|
'the content may have errors in it.',
|
|
timePoint);
|
|
}
|
|
|
|
timeline[timeline.length - 1].end = startTime / timescale;
|
|
}
|
|
|
|
for (var j = 0; j <= repeat; ++j) {
|
|
var endTime = startTime + d;
|
|
timeline.push(
|
|
{start: (startTime / timescale), end: (endTime / timescale)});
|
|
|
|
startTime = endTime;
|
|
lastEndTime = endTime;
|
|
}
|
|
}
|
|
|
|
return timeline;
|
|
};
|
|
|
|
|
|
/**
|
|
* Expands the first SegmentReference so it begins at the start of its Period
|
|
* if it already begins close to the start of its Period, and expands or
|
|
* contracts the last SegmentReference so it ends at the end of its Period for
|
|
* VOD presentations.
|
|
*
|
|
* @param {?number} periodDuration
|
|
* @param {!Array.<!shaka.media.SegmentReference>} references
|
|
*/
|
|
shaka.dash.MpdUtils.fitSegmentReferences = function(
|
|
periodDuration, references) {
|
|
if (references.length == 0)
|
|
return;
|
|
|
|
/** @const {number} */
|
|
var tolerance = shaka.dash.MpdUtils.GAP_OVERLAP_TOLERANCE_SECONDS;
|
|
|
|
var firstReference = references[0];
|
|
if (firstReference.startTime <= tolerance) {
|
|
// Note: if the segment actually starts past 0, the video element should
|
|
// automatically jump the gap since the gap is small.
|
|
references[0] =
|
|
new shaka.media.SegmentReference(
|
|
firstReference.position,
|
|
0, firstReference.endTime,
|
|
firstReference.getUris,
|
|
firstReference.startByte, firstReference.endByte);
|
|
}
|
|
|
|
if (periodDuration == null || periodDuration == Number.POSITIVE_INFINITY)
|
|
return;
|
|
|
|
var lastReference = references[references.length - 1];
|
|
|
|
// Sanity check.
|
|
goog.asserts.assert(
|
|
lastReference.startTime < periodDuration,
|
|
'lastReference cannot begin after the end of the Period');
|
|
if (lastReference.startTime > periodDuration) return;
|
|
|
|
// Log warning if necessary.
|
|
if (lastReference.endTime <= periodDuration - tolerance) {
|
|
shaka.log.warning(
|
|
'The last segment should not end before the end of the Period.',
|
|
lastReference);
|
|
} else if (lastReference.endTime >= periodDuration + tolerance) {
|
|
shaka.log.warning(
|
|
'The last segment should not end after the end of the Period.',
|
|
lastReference);
|
|
}
|
|
|
|
// Adjust the last SegmentReference.
|
|
references[references.length - 1] =
|
|
new shaka.media.SegmentReference(
|
|
lastReference.position,
|
|
lastReference.startTime, periodDuration,
|
|
lastReference.getUris,
|
|
lastReference.startByte, lastReference.endByte);
|
|
};
|
|
|
|
|
|
/**
|
|
* Resolves an array of relative URIs to the given base URIs. This will result
|
|
* in M*N number of URIs.
|
|
*
|
|
* @param {!Array.<string>} baseUris
|
|
* @param {!Array.<string>} relativeUris
|
|
* @return {!Array.<string>}
|
|
*/
|
|
shaka.dash.MpdUtils.resolveUris = function(baseUris, relativeUris) {
|
|
var Functional = shaka.util.Functional;
|
|
if (relativeUris.length == 0)
|
|
return baseUris;
|
|
|
|
var relativeAsGoog =
|
|
relativeUris.map(function(uri) { return new goog.Uri(uri); });
|
|
// Resolve each URI relative to each base URI, creating an Array of Arrays.
|
|
// Then flatten the Arrays into a single Array.
|
|
return baseUris.map(function(uri) { return new goog.Uri(uri); })
|
|
.map(function(base) { return relativeAsGoog.map(base.resolve.bind(base)); })
|
|
.reduce(Functional.collapseArrays, [])
|
|
.map(function(uri) { return uri.toString(); });
|
|
};
|
|
|
|
|
|
/**
|
|
* Parses common segment info for SegmentList and SegmentTemplate.
|
|
*
|
|
* @param {shaka.dash.DashParser.Context} context
|
|
* @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
|
|
* Gets the element that contains the segment info.
|
|
* @return {shaka.dash.MpdUtils.SegmentInfo}
|
|
*/
|
|
shaka.dash.MpdUtils.parseSegmentInfo = function(context, callback) {
|
|
goog.asserts.assert(
|
|
callback(context.representation),
|
|
'There must be at least one element of the given type.');
|
|
var MpdUtils = shaka.dash.MpdUtils;
|
|
var XmlUtils = shaka.util.XmlUtils;
|
|
|
|
var timescaleStr = MpdUtils.inheritAttribute(context, callback, 'timescale');
|
|
var timescale = 1;
|
|
if (timescaleStr) {
|
|
timescale = XmlUtils.parsePositiveInt(timescaleStr) || 1;
|
|
}
|
|
|
|
var durationStr = MpdUtils.inheritAttribute(context, callback, 'duration');
|
|
var segmentDuration = XmlUtils.parsePositiveInt(durationStr || '');
|
|
if (segmentDuration) {
|
|
segmentDuration /= timescale;
|
|
}
|
|
|
|
var startNumberStr =
|
|
MpdUtils.inheritAttribute(context, callback, 'startNumber');
|
|
var presentationTimeOffset =
|
|
MpdUtils.inheritAttribute(context, callback, 'presentationTimeOffset');
|
|
var startNumber = XmlUtils.parseNonNegativeInt(startNumberStr || '');
|
|
if (startNumberStr == null || startNumber == null)
|
|
startNumber = 1;
|
|
|
|
var timelineNode =
|
|
MpdUtils.inheritChild(context, callback, 'SegmentTimeline');
|
|
/** @type {Array.<shaka.dash.MpdUtils.TimeRange>} */
|
|
var timeline = null;
|
|
if (timelineNode) {
|
|
timeline = MpdUtils.createTimeline(
|
|
timelineNode, timescale, Number(presentationTimeOffset),
|
|
context.periodInfo.duration || Number.POSITIVE_INFINITY);
|
|
}
|
|
|
|
var pto = (Number(presentationTimeOffset) / timescale) || 0;
|
|
return {
|
|
timescale: timescale,
|
|
segmentDuration: segmentDuration,
|
|
startNumber: startNumber,
|
|
presentationTimeOffset: pto,
|
|
timeline: timeline
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
* Searches the inheritance for a Segment* with the given attribute.
|
|
*
|
|
* @param {shaka.dash.DashParser.Context} context
|
|
* @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
|
|
* Gets the Element that contains the attribute to inherit.
|
|
* @param {string} attribute
|
|
* @return {?string}
|
|
*/
|
|
shaka.dash.MpdUtils.inheritAttribute = function(context, callback, attribute) {
|
|
var Functional = shaka.util.Functional;
|
|
goog.asserts.assert(
|
|
callback(context.representation),
|
|
'There must be at least one element of the given type');
|
|
|
|
/** @type {!Array.<!Element>} */
|
|
var nodes = [
|
|
callback(context.representation),
|
|
callback(context.adaptationSet),
|
|
callback(context.period)
|
|
].filter(Functional.isNotNull);
|
|
|
|
return nodes
|
|
.map(function(s) { return s.getAttribute(attribute); })
|
|
.reduce(function(all, part) { return all || part; });
|
|
};
|
|
|
|
|
|
/**
|
|
* Searches the inheritance for a Segment* with the given child.
|
|
*
|
|
* @param {shaka.dash.DashParser.Context} context
|
|
* @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
|
|
* Gets the Element that contains the child to inherit.
|
|
* @param {string} child
|
|
* @return {Element}
|
|
*/
|
|
shaka.dash.MpdUtils.inheritChild = function(context, callback, child) {
|
|
var Functional = shaka.util.Functional;
|
|
goog.asserts.assert(
|
|
callback(context.representation),
|
|
'There must be at least one element of the given type');
|
|
|
|
/** @type {!Array.<!Element>} */
|
|
var nodes = [
|
|
callback(context.representation),
|
|
callback(context.adaptationSet),
|
|
callback(context.period)
|
|
].filter(Functional.isNotNull);
|
|
|
|
var XmlUtils = shaka.util.XmlUtils;
|
|
return nodes
|
|
.map(function(s) { return XmlUtils.findChild(s, child); })
|
|
.reduce(function(all, part) { return all || part; });
|
|
};
|