Files
shaka-player/lib/dash/mpd_utils.js
T
Jacob Trimble f610cbf4c9 Added support for SegmentTimeline inside a SegmentList.
A SegmentTimeline can appear inside a SegmentList and describes the
duration of the items in the list.  A SegmentTimeline can also be
inherited from the Period or AdaptationSet.  These have all been
added.  Also added SegmentList support for dynamic MPDs.

Closes #88

Change-Id: I849491b1c6a5808a58c597eccfd439154e2e6ecd
2015-08-03 16:54:28 +00:00

283 lines
8.6 KiB
JavaScript

/**
* 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.
*
* @fileoverview MPD processing utility functions.
*/
goog.provide('shaka.dash.MpdUtils');
goog.require('goog.Uri');
goog.require('shaka.asserts');
goog.require('shaka.dash.mpd');
goog.require('shaka.log');
/**
* @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.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(
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. Note
// that |segmentReplacement| may be zero.
var segmentReplacement = (segmentNumber - 1) + segmentTemplate.startNumber;
var timeReplacement = (segmentNumber - 1) * segmentTemplate.segmentDuration;
// Generate the media URL.
var mediaUrl = shaka.dash.MpdUtils.fillMediaUrlTemplate(
representation, segmentReplacement, timeReplacement);
if (!mediaUrl) {
// An error has already been logged.
return null;
}
references.push(
new shaka.media.SegmentReference(
scaledStartTime,
scaledEndTime,
0 /* startByte */,
null /* endByte */,
new goog.Uri(mediaUrl)));
}
return references;
};
/**
* Fills a media URL template. Falls-back to the Representation's BaseURL if
* the SegmentTemplate's media URL template is missing.
*
* @param {!shaka.dash.mpd.Representation} representation
* @param {number} number
* @param {number} time
* @return {goog.Uri} A URL on success; otherwise, return null.
*/
shaka.dash.MpdUtils.fillMediaUrlTemplate = function(
representation, number, time) {
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 goog.Uri(representation.baseUrl) :
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 = representation.baseUrl ?
representation.baseUrl.resolve(filledUrlTemplate) :
filledUrlTemplate;
return mediaUrl;
};
/**
* 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
* @return {!Array.<{start: number, end: number}>}
*/
shaka.dash.MpdUtils.createTimeline = function(segmentTimeline, timescale) {
shaka.asserts.assert(segmentTimeline);
var lastEndTime = 0;
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;
for (var j = 0; j <= repeat; ++j) {
var endTime = startTime + timePoints[i].duration;
// 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;
}
timeline.push({start: startTime, end: endTime});
startTime = endTime;
lastEndTime = endTime;
} // for j
}
return timeline;
};