/**
* @license
* Copyright 2015 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('shaka.asserts');
goog.require('shaka.dash.mpd');
goog.require('shaka.log');
goog.require('shaka.util.FailoverUri');
/**
* @namespace shaka.dash.MpdUtils
* @summary A set of MPD processing utility functions.
*/
/**
* Any gap/overlap within a SegmentTimeline that is greater than or equal to
* this value (in seconds) will generate a warning message.
* @const {number}
*/
shaka.dash.MpdUtils.GAP_OVERLAP_WARN_THRESHOLD = 1.0 / 32.0;
/**
* Generates a set of SegmentReferences from a SegmentTemplate with a 'duration'
* attribute.
*
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @param {!shaka.dash.mpd.Representation} representation
* @param {number} firstSegmentNumber The segment number (one-based) of the
* first SegmentReference to generate, relative to the start of the
* Representation's Period.
* @param {number} numSegments The number of SegmentReferences to generate.
* @return {Array.<!shaka.media.SegmentReference>} The SegmentReferences on
* success; otherwise, null.
*/
shaka.dash.MpdUtils.generateSegmentReferences = function(
networkCallback, representation, firstSegmentNumber, numSegments) {
shaka.asserts.assert(representation.segmentTemplate);
shaka.asserts.assert(representation.segmentTemplate.timescale > 0);
shaka.asserts.assert(representation.segmentTemplate.segmentDuration);
shaka.asserts.assert(firstSegmentNumber > 0);
var segmentTemplate = representation.segmentTemplate;
/** @type {!Array.<!shaka.media.SegmentReference>} */
var references = [];
for (var i = 0; i < numSegments; ++i) {
// The current segment number (one-based), relative to the start of the
// Representation's Period.
var segmentNumber = i + firstSegmentNumber;
var startTime = (segmentNumber - 1) * segmentTemplate.segmentDuration;
var endTime = startTime + segmentTemplate.segmentDuration;
var scaledStartTime = startTime / segmentTemplate.timescale;
var scaledEndTime = endTime / segmentTemplate.timescale;
// Compute the media URL template placeholder replacements.
var segmentReplacement = segmentTemplate.startNumber + segmentNumber - 1;
var timeReplacement = startTime;
// Generate the media URL.
var mediaUrl = shaka.dash.MpdUtils.createFromTemplate(
networkCallback, representation, segmentReplacement, timeReplacement,
0 /* startByte */, null /* endByte */);
if (!mediaUrl) {
// An error has already been logged.
return null;
}
references.push(
new shaka.media.SegmentReference(
scaledStartTime,
scaledEndTime,
mediaUrl));
}
return references;
};
/**
* Creates a FailoverUri from a relative template URL.
*
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @param {!shaka.dash.mpd.Representation} representation
* @param {number} number
* @param {number} time
* @param {number} startByte
* @param {?number} endByte
* @return {shaka.util.FailoverUri}
*/
shaka.dash.MpdUtils.createFromTemplate = function(
networkCallback, representation, number, time, startByte, endByte) {
shaka.asserts.assert(representation.segmentTemplate);
if (!representation.segmentTemplate) return null;
var urlTemplate = representation.segmentTemplate.mediaUrlTemplate;
if (!urlTemplate) {
shaka.log.warning(
'The SegmentTemplate\'s media URL template is missing:',
'using the Representation\'s BaseURL instead.',
representation);
return representation.baseUrl ?
new shaka.util.FailoverUri(
networkCallback, representation.baseUrl, startByte, endByte) :
null;
}
var filledUrlTemplate = shaka.dash.MpdUtils.fillUrlTemplate(
urlTemplate,
representation.id,
number,
representation.bandwidth,
time);
if (!filledUrlTemplate) {
// An error has already been logged.
return null;
}
var mediaUrl = shaka.util.FailoverUri.resolve(
representation.baseUrl, filledUrlTemplate);
return new shaka.util.FailoverUri(
networkCallback, mediaUrl, startByte, endByte);
};
/**
* Fills a SegmentTemplate URL template.
*
* @see ISO/IEC 23009-1:2014 section 5.3.9.4.4
*
* @param {string} urlTemplate
* @param {?string} representationId
* @param {?number} number
* @param {?number} bandwidth
* @param {?number} time
* @return {goog.Uri} A URL on success; otherwise, return null.
*/
shaka.dash.MpdUtils.fillUrlTemplate = function(
urlTemplate, representationId, number, bandwidth, 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 url = urlTemplate.replace(re, function(match, name, widthString) {
if (match == '$$') {
return '$';
}
var value = valueTable[name];
shaka.asserts.assert(value !== undefined);
// Note that |value| may be 0 or ''.
if (value == null) {
shaka.log.warning(
'URL template does not have an available substitution for identifier',
'"' + name + '":',
urlTemplate);
return match;
}
if (name == 'RepresentationID' && widthString) {
shaka.log.warning(
'URL template should not contain a width specifier for identifier',
'"RepresentationID":',
urlTemplate);
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;
});
// The URL might contain illegal characters (e.g., '%').
try {
return new goog.Uri(url);
} catch (exception) {
if (exception instanceof URIError) {
shaka.log.warning(
'URL template contains an illegal character:', urlTemplate);
return null;
}
throw exception;
}
};
/**
* Expands a SegmentTimeline into a simple array-based timeline.
*
* @param {shaka.dash.mpd.SegmentTimeline} segmentTimeline
* @param {number} timescale
* @param {number} durationSeconds The duration of the period (in seconds).
* @return {!Array.<{start: number, end: number}>}
*/
shaka.dash.MpdUtils.createTimeline = function(
segmentTimeline, timescale, durationSeconds) {
shaka.asserts.assert(segmentTimeline);
var lastEndTime = 0;
var duration = durationSeconds * timescale;
var timePoints = segmentTimeline.timePoints;
/** @type {!Array.<{start: number, end: number}>} */
var timeline = [];
for (var i = 0; i < timePoints.length; ++i) {
if (!timePoints[i].duration) {
shaka.log.warning(
'SegmentTimeline "S" element does not have a duration:',
'ignoring the remaining "S" elements.',
timePoints[i]);
return timeline;
}
var tpStart = timePoints[i].startTime;
var startTime = tpStart != null ? tpStart : lastEndTime;
var repeat = timePoints[i].repeat || 0;
if (repeat < 0) {
var d = timePoints[i].duration;
if (i + 1 === timePoints.length) {
var delta = timePoints[0].startTime + duration - startTime;
repeat = Math.ceil(delta / d) - 1;
} else {
var next = timePoints[i + 1].startTime;
repeat = Math.ceil((next - 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.
if ((timeline.length > 0) && (startTime != lastEndTime)) {
var delta = startTime - lastEndTime;
if (Math.abs(delta / timescale) >=
shaka.dash.MpdUtils.GAP_OVERLAP_WARN_THRESHOLD) {
shaka.log.warning(
'SegmentTimeline contains a large gap/overlap.',
'The content may have errors in it.',
timePoints[i]);
}
timeline[timeline.length - 1].end = startTime;
}
for (var j = 0; j <= repeat; ++j) {
var endTime = startTime + timePoints[i].duration;
timeline.push({start: startTime, end: endTime});
startTime = endTime;
lastEndTime = endTime;
}
}
return timeline;
};