mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-17 16:26:39 +03:00
b8b3ad862b
* Rename SegmentIndex functions to shorter names. * Add first() and last() functions for convenience. * Throw exceptions for out of range errors. * Handle more corner-cases in merge(). * Add more unit tests. Change-Id: If875cd766e7091504c028413694326fcdf2aae5b
1577 lines
53 KiB
JavaScript
1577 lines
53 KiB
JavaScript
/**
|
|
* Copyright 2014 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 Implements MpdProcessor.
|
|
*/
|
|
|
|
goog.provide('shaka.dash.MpdProcessor');
|
|
|
|
goog.require('goog.Uri');
|
|
goog.require('shaka.asserts');
|
|
goog.require('shaka.dash.mpd');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.media.PeriodInfo');
|
|
goog.require('shaka.media.SegmentIndex');
|
|
goog.require('shaka.media.SegmentMetadataInfo');
|
|
goog.require('shaka.media.SegmentReference');
|
|
goog.require('shaka.media.StreamInfo');
|
|
goog.require('shaka.media.StreamSetInfo');
|
|
goog.require('shaka.util.Clock');
|
|
|
|
|
|
|
|
/**
|
|
* Creates an MpdProcessor, which validates MPDs, calculates start/duration
|
|
* attributes, removes invalid Representations, and ultimately generates a
|
|
* ManifestInfo.
|
|
*
|
|
* @param {?shaka.player.DashVideoSource.ContentProtectionCallback}
|
|
* interpretContentProtection
|
|
*
|
|
* @constructor
|
|
* @struct
|
|
*/
|
|
shaka.dash.MpdProcessor = function(interpretContentProtection) {
|
|
/** @private {?shaka.player.DashVideoSource.ContentProtectionCallback} */
|
|
this.interpretContentProtection_ = interpretContentProtection;
|
|
|
|
/** @type {!shaka.media.ManifestInfo} */
|
|
this.manifestInfo = new shaka.media.ManifestInfo();
|
|
};
|
|
|
|
|
|
/**
|
|
* 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.MpdProcessor.GAP_OVERLAP_WARNING_THRESHOLD = 1.0 / 32.0;
|
|
|
|
|
|
/**
|
|
* The maximum span, in seconds, that a SegmentIndex must account for when that
|
|
* SegmentIndex is being generated via a segment duration.
|
|
* @const {number}
|
|
*/
|
|
shaka.dash.MpdProcessor.MAX_SEGMENT_INDEX_SPAN = 2 * 60;
|
|
|
|
|
|
/**
|
|
* Processes the given MPD. Sets |this.periodInfos|.
|
|
*
|
|
* @param {!shaka.dash.mpd.Mpd} mpd
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.process = function(mpd) {
|
|
this.manifestInfo = new shaka.media.ManifestInfo();
|
|
this.validateSegmentInfo_(mpd);
|
|
this.calculateDurations_(mpd);
|
|
this.filterPeriods_(mpd);
|
|
this.createManifestInfo_(mpd);
|
|
};
|
|
|
|
|
|
/**
|
|
* Attempts to calculate each Period's start attribute and duration attribute,
|
|
* and attempts to calcuate the MPD's mediaPresentationDuration attribute.
|
|
*
|
|
* @see ISO/IEC 23009-1:2014 section 5.3.2.1
|
|
*
|
|
* @param {!shaka.dash.mpd.Mpd} mpd
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.calculateDurations_ = function(mpd) {
|
|
if (!mpd.periods.length) {
|
|
return;
|
|
}
|
|
|
|
if (mpd.periods[0].start == null) {
|
|
mpd.periods[0].start = 0;
|
|
}
|
|
|
|
// If it's zero or truthy, it's set. This means null and NaN are not set.
|
|
var isSet = function(x) { return x == 0 || !!x; };
|
|
|
|
// @mediaPresentationDuration should only be used if the MPD is static.
|
|
if (isSet(mpd.minUpdatePeriod)) {
|
|
mpd.mediaPresentationDuration = null;
|
|
}
|
|
|
|
// If there is only one Period then infer its duration.
|
|
if (isSet(mpd.mediaPresentationDuration) &&
|
|
(mpd.periods.length == 1) &&
|
|
!isSet(mpd.periods[0].duration)) {
|
|
mpd.periods[0].duration = mpd.mediaPresentationDuration;
|
|
}
|
|
|
|
var totalDuration = 0;
|
|
|
|
// True if |totalDuration| includes all periods, false if it only includes up
|
|
// to the last Period in which a start time and duration could be
|
|
// ascertained.
|
|
var totalDurationIncludesAllPeriods = true;
|
|
|
|
for (var i = 0; i < mpd.periods.length; ++i) {
|
|
var previousPeriod = mpd.periods[i - 1];
|
|
var period = mpd.periods[i];
|
|
|
|
// "The Period extends until the Period.start of the next Period, or until
|
|
// the end of the Media Presentation in the case of the last Period."
|
|
var nextPeriod = mpd.periods[i + 1] ||
|
|
{ start: mpd.mediaPresentationDuration };
|
|
|
|
// "If the 'start' attribute is absent, but the previous period contains a
|
|
// 'duration' attribute, the start time of the new Period is the sum of the
|
|
// start time of the previous period Period.start and the value of the
|
|
// attribute 'duration' of the previous Period."
|
|
if (!isSet(period.start) &&
|
|
previousPeriod &&
|
|
isSet(previousPeriod.start) &&
|
|
isSet(previousPeriod.duration)) {
|
|
period.start = previousPeriod.start + previousPeriod.duration;
|
|
}
|
|
|
|
// "The difference between the start time of a Period and the start time
|
|
// of the following Period is the duration of the media content represented
|
|
// by this Period."
|
|
if (!isSet(period.duration) && isSet(nextPeriod.start)) {
|
|
period.duration = nextPeriod.start - period.start;
|
|
}
|
|
|
|
if ((period.start != null) && (period.duration != null)) {
|
|
totalDuration += period.duration;
|
|
} else {
|
|
totalDurationIncludesAllPeriods = false;
|
|
}
|
|
}
|
|
|
|
// "The Media Presentation Duration is provided either as the value of MPD
|
|
// 'mediaPresentationDuration' attribute if present, or as the sum of
|
|
// Period.start + Period.duration of the last Period."
|
|
if (isSet(mpd.mediaPresentationDuration)) {
|
|
if (mpd.mediaPresentationDuration != totalDuration) {
|
|
shaka.log.warning('@mediaPresentationDuration does not match the total ' +
|
|
'duration of all periods.');
|
|
// Assume mpd.mediaPresentationDuration is correct;
|
|
// |totalDurationIncludesAllPeriods| may be false.
|
|
}
|
|
} else {
|
|
var finalPeriod = mpd.periods[mpd.periods.length - 1];
|
|
if (totalDurationIncludesAllPeriods) {
|
|
shaka.asserts.assert(isSet(finalPeriod.start) &&
|
|
isSet(finalPeriod.duration));
|
|
shaka.asserts.assert(totalDuration ==
|
|
finalPeriod.start + finalPeriod.duration);
|
|
mpd.mediaPresentationDuration = totalDuration;
|
|
} else {
|
|
if (isSet(finalPeriod.start) && isSet(finalPeriod.duration)) {
|
|
shaka.log.warning('Some Periods may not have valid start times ' +
|
|
'or durations.');
|
|
mpd.mediaPresentationDuration =
|
|
finalPeriod.start + finalPeriod.duration;
|
|
} else {
|
|
// Fallback to what we were able to compute.
|
|
if (mpd.type == 'static') {
|
|
shaka.log.warning('Some Periods may not have valid start times ' +
|
|
'or durations; @mediaPresentationDuration may ' +
|
|
'not include the duration of all periods.');
|
|
mpd.mediaPresentationDuration = totalDuration;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Ensures that each Representation has either a SegmentBase, SegmentList, or
|
|
* SegmentTemplate.
|
|
*
|
|
* @param {!shaka.dash.mpd.Mpd} mpd
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.validateSegmentInfo_ = function(mpd) {
|
|
for (var i = 0; i < mpd.periods.length; ++i) {
|
|
var period = mpd.periods[i];
|
|
for (var j = 0; j < period.adaptationSets.length; ++j) {
|
|
var adaptationSet = period.adaptationSets[j];
|
|
if (adaptationSet.contentType == 'text') continue;
|
|
|
|
for (var k = 0; k < adaptationSet.representations.length; ++k) {
|
|
var representation = adaptationSet.representations[k];
|
|
|
|
var n = 0;
|
|
n += representation.segmentBase ? 1 : 0;
|
|
n += representation.segmentList ? 1 : 0;
|
|
n += representation.segmentTemplate ? 1 : 0;
|
|
|
|
if (n == 0) {
|
|
shaka.log.warning(
|
|
'Representation does not contain any segment information. ' +
|
|
'A Representation must contain one of SegmentBase, ' +
|
|
'SegmentList, or SegmentTemplate.',
|
|
representation);
|
|
adaptationSet.representations.splice(k, 1);
|
|
--k;
|
|
} else if (n != 1) {
|
|
shaka.log.warning(
|
|
'Representation contains multiple segment information sources. ' +
|
|
'A Representation should only contain one of SegmentBase, ' +
|
|
'SegmenstList, or SegmentTemplate.',
|
|
representation);
|
|
if (representation.segmentBase) {
|
|
shaka.log.info('Using SegmentBase by default.');
|
|
representation.segmentList = null;
|
|
representation.segmentTemplate = null;
|
|
} else if (representation.segmentList) {
|
|
shaka.log.info('Using SegmentList by default.');
|
|
representation.segmentTemplate = null;
|
|
} else {
|
|
shaka.asserts.unreachable();
|
|
}
|
|
}
|
|
} // for k
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Builds a StreamInfo from a SegmentTemplate with an index URL template.
|
|
*
|
|
* @param {!shaka.dash.mpd.Representation} representation
|
|
* @param {!shaka.media.StreamInfo} streamInfo
|
|
* @return {boolean} True on success.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.buildStreamInfoFromIndexUrlTemplate_ =
|
|
function(representation, streamInfo) {
|
|
shaka.asserts.assert(representation.segmentTemplate);
|
|
shaka.asserts.assert(representation.segmentTemplate.indexUrlTemplate);
|
|
shaka.asserts.assert(representation.segmentTemplate.timescale > 0);
|
|
|
|
var segmentTemplate = representation.segmentTemplate;
|
|
|
|
// Generate the media URL. Since there is no SegmentTimeline there is only
|
|
// one media URL, so just map $Number$ to 1 and $Time$ to 0.
|
|
var mediaUrl;
|
|
if (segmentTemplate.mediaUrlTemplate) {
|
|
var filledUrlTemplate = this.fillUrlTemplate_(
|
|
segmentTemplate.mediaUrlTemplate,
|
|
representation.id,
|
|
1,
|
|
representation.bandwidth,
|
|
0);
|
|
|
|
if (!filledUrlTemplate) {
|
|
// An error has already been logged.
|
|
return false;
|
|
}
|
|
|
|
mediaUrl = representation.baseUrl ?
|
|
representation.baseUrl.resolve(filledUrlTemplate) :
|
|
filledUrlTemplate;
|
|
} else {
|
|
// Fallback to the Representation's URL.
|
|
mediaUrl = new goog.Uri(representation.baseUrl);
|
|
}
|
|
|
|
// Generate a RepresentationIndex.
|
|
var representationIndex = this.generateRepresentationIndex_(representation);
|
|
if (!representationIndex) {
|
|
// An error has already been logged.
|
|
return false;
|
|
}
|
|
|
|
// Generate an Initialization.
|
|
var initialization = null;
|
|
if (segmentTemplate.initializationUrlTemplate) {
|
|
initialization = this.generateInitialization_(representation);
|
|
if (!initialization) {
|
|
// An error has already been logged.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Set StreamInfo properties.
|
|
streamInfo.mediaUrl = new goog.Uri(mediaUrl);
|
|
|
|
if (segmentTemplate.presentationTimeOffset) {
|
|
streamInfo.timestampOffset =
|
|
-1 * segmentTemplate.presentationTimeOffset / segmentTemplate.timescale;
|
|
}
|
|
|
|
streamInfo.segmentIndexInfo =
|
|
this.createSegmentMetadataInfo_(representationIndex);
|
|
|
|
streamInfo.segmentInitializationInfo =
|
|
this.createSegmentMetadataInfo_(initialization);
|
|
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Builds a StreamInfo from a SegmentTemplate with a SegmentTimeline.
|
|
*
|
|
* @param {!shaka.dash.mpd.Mpd} mpd
|
|
* @param {!shaka.dash.mpd.Period} period
|
|
* @param {!shaka.dash.mpd.Representation} representation
|
|
* @param {!shaka.media.StreamInfo} streamInfo
|
|
* @return {boolean} True on success.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.buildStreamInfoFromSegmentTimeline_ =
|
|
function(mpd, period, representation, streamInfo) {
|
|
shaka.asserts.assert(representation.segmentTemplate);
|
|
shaka.asserts.assert(representation.segmentTemplate.timeline);
|
|
shaka.asserts.assert(representation.segmentTemplate.timescale > 0);
|
|
|
|
if (period.start == null) {
|
|
shaka.log.error(
|
|
'Cannot instantiate SegmentTemplate: the period\'s start time is ' +
|
|
'unknown.',
|
|
representation);
|
|
return false;
|
|
}
|
|
|
|
var segmentTemplate = representation.segmentTemplate;
|
|
if (!segmentTemplate.mediaUrlTemplate) {
|
|
shaka.log.error(
|
|
'Cannot instantiate SegmentTemplate: SegmentTemplate does not have a ' +
|
|
'media URL template.',
|
|
representation);
|
|
return false;
|
|
}
|
|
|
|
var timeline = this.createTimeline_(segmentTemplate);
|
|
if (!timeline) {
|
|
// An error has already been logged.
|
|
return false;
|
|
}
|
|
|
|
// Compute the earliest available timestamp. Assume the MPD only contains
|
|
// segments that are available. This simplifies the calculation below by
|
|
// allowing us to ignore @availabilityStartTime. If we did use
|
|
// @availabilityStartTime then the calculation below would be more
|
|
// complicated than the calculations in computeAvailableSegmentRange_() since
|
|
// the duration of each segment is variable here.
|
|
var earliestAvailableTimestamp = 0;
|
|
if (mpd.minUpdatePeriod && (timeline.length >= 2)) {
|
|
var timeShiftBufferDepth = mpd.timeShiftBufferDepth || 0;
|
|
earliestAvailableTimestamp =
|
|
(timeline[timeline.length - 2].start / segmentTemplate.timescale) -
|
|
timeShiftBufferDepth;
|
|
}
|
|
|
|
// Generate a SegmentIndex.
|
|
var references = [];
|
|
|
|
for (var i = 0; i < timeline.length; ++i) {
|
|
var startTime = timeline[i].start;
|
|
var endTime = timeline[i].end;
|
|
|
|
// Compute the segment's scaled start time and scaled end time.
|
|
var scaledStartTime = startTime / segmentTemplate.timescale;
|
|
var scaledEndTime = endTime / segmentTemplate.timescale;
|
|
|
|
if (scaledStartTime < earliestAvailableTimestamp) {
|
|
// Skip unavailable segments.
|
|
continue;
|
|
}
|
|
|
|
var absoluteSegmentNumber = i + segmentTemplate.startNumber;
|
|
|
|
// Compute the media URL template placeholder replacements.
|
|
var segmentReplacement = absoluteSegmentNumber;
|
|
var timeReplacement = startTime;
|
|
|
|
// Generate the media URL.
|
|
shaka.asserts.assert(segmentTemplate.mediaUrlTemplate);
|
|
var filledUrlTemplate = this.fillUrlTemplate_(
|
|
segmentTemplate.mediaUrlTemplate,
|
|
representation.id,
|
|
segmentReplacement,
|
|
representation.bandwidth,
|
|
timeReplacement);
|
|
|
|
if (!filledUrlTemplate) {
|
|
// An error has already been logged.
|
|
return false;
|
|
}
|
|
|
|
var mediaUrl = representation.baseUrl ?
|
|
representation.baseUrl.resolve(filledUrlTemplate) :
|
|
filledUrlTemplate;
|
|
|
|
references.push(
|
|
new shaka.media.SegmentReference(
|
|
absoluteSegmentNumber,
|
|
scaledStartTime,
|
|
scaledEndTime,
|
|
0 /* startByte */,
|
|
null /* endByte */,
|
|
new goog.Uri(mediaUrl)));
|
|
}
|
|
|
|
// Generate an Initialization. If there are no references then assume that
|
|
// the intialization segment is not available.
|
|
var initialization = null;
|
|
if (segmentTemplate.initializationUrlTemplate && (references.length > 0)) {
|
|
initialization = this.generateInitialization_(representation);
|
|
if (!initialization) {
|
|
// An error has already been logged.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Set StreamInfo properties.
|
|
if (mpd.minUpdatePeriod && (references.length > 0)) {
|
|
// Compute the start time of the current segment.
|
|
var minBufferTime = mpd.minBufferTime || 0;
|
|
var bestAvailableTimestamp =
|
|
references[references.length - 1].startTime - minBufferTime;
|
|
for (var i = 0; i < references.length; ++i) {
|
|
if (references[i].endTime >= bestAvailableTimestamp) {
|
|
streamInfo.currentSegmentStartTime = references[i].startTime;
|
|
break;
|
|
}
|
|
}
|
|
shaka.asserts.assert(streamInfo.currentSegmentStartTime != null);
|
|
}
|
|
|
|
streamInfo.segmentInitializationInfo =
|
|
this.createSegmentMetadataInfo_(initialization);
|
|
|
|
streamInfo.segmentIndex = new shaka.media.SegmentIndex(references);
|
|
shaka.log.debug('Generated SegmentIndex from SegmentTimeline',
|
|
streamInfo.segmentIndex);
|
|
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Expands a SegmentTimeline into a simple array-based timeline.
|
|
*
|
|
* @return {Array.<{start: number, end: number}>}
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.createTimeline_ = function(
|
|
segmentTemplate) {
|
|
shaka.asserts.assert(segmentTemplate.timeline);
|
|
|
|
var timePoints = segmentTemplate.timeline.timePoints;
|
|
var lastEndTime = 0;
|
|
|
|
/** @type {!Array.<{start: number, end: number}>} */
|
|
var timeline = [];
|
|
|
|
for (var i = 0; i < timePoints.length; ++i) {
|
|
var repeat = timePoints[i].repeat || 0;
|
|
for (var j = 0; j <= repeat; ++j) {
|
|
if (!timePoints[i].duration) {
|
|
shaka.log.warning(
|
|
'SegmentTimeline "S" element does not have a duration.',
|
|
timePoints[i]);
|
|
return null;
|
|
}
|
|
|
|
// Compute the segment's unscaled start time and unscaled end time.
|
|
var startTime;
|
|
if (timePoints[i].startTime && j == 0) {
|
|
startTime = timePoints[i].startTime;
|
|
} else {
|
|
if (i == 0 && j == 0) {
|
|
startTime = 0;
|
|
} else {
|
|
startTime = lastEndTime;
|
|
}
|
|
}
|
|
shaka.asserts.assert(startTime >= 0);
|
|
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 complicates the computation of the
|
|
// $Time$ placeholder.
|
|
if ((timeline.length > 0) && (startTime != lastEndTime)) {
|
|
var delta = startTime - lastEndTime;
|
|
|
|
if (Math.abs(delta / segmentTemplate.timescale) >=
|
|
shaka.dash.MpdProcessor.GAP_OVERLAP_WARNING_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;
|
|
}
|
|
|
|
lastEndTime = endTime;
|
|
|
|
timeline.push({start: startTime, end: endTime});
|
|
} // for j
|
|
}
|
|
|
|
return timeline;
|
|
};
|
|
|
|
|
|
/**
|
|
* Builds a StreamInfo from a SegmentTemplate with a segment duration.
|
|
*
|
|
* @param {!shaka.dash.mpd.Mpd} mpd
|
|
* @param {!shaka.dash.mpd.Period} period
|
|
* @param {!shaka.dash.mpd.Representation} representation
|
|
* @param {!shaka.media.StreamInfo} streamInfo
|
|
* @return {boolean} True on success.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.buildStreamInfoFromSegmentDuration_ =
|
|
function(mpd, period, representation, streamInfo) {
|
|
shaka.asserts.assert(representation.segmentTemplate);
|
|
shaka.asserts.assert(representation.segmentTemplate.segmentDuration);
|
|
shaka.asserts.assert(representation.segmentTemplate.timescale > 0);
|
|
|
|
if (period.start == null) {
|
|
shaka.log.error(
|
|
'Cannot instantiate SegmentTemplate: the period\'s start time is ' +
|
|
'unknown.',
|
|
representation);
|
|
return false;
|
|
}
|
|
|
|
var segmentTemplate = representation.segmentTemplate;
|
|
if (!segmentTemplate.mediaUrlTemplate) {
|
|
shaka.log.error(
|
|
'Cannot instantiate SegmentTemplate: SegmentTemplate does not have a ' +
|
|
'media URL template.',
|
|
representation);
|
|
return false;
|
|
}
|
|
|
|
// The number of segment references to generate starting from the earliest
|
|
// available segment to the current segment, but not counting the current
|
|
// segment.
|
|
var numSegmentsBeforeCurrentSegment = 0;
|
|
|
|
// Find the earliest available segment and the current segment. All segment
|
|
// numbers are relative to the start of |period| unless marked otherwise.
|
|
var earliestSegmentNumber;
|
|
var currentSegmentNumber;
|
|
if (mpd.minUpdatePeriod) {
|
|
var pair = this.computeAvailableSegmentRange_(mpd, period, segmentTemplate);
|
|
if (pair) {
|
|
// Build the SegmentIndex starting from the earliest available segment.
|
|
earliestSegmentNumber = pair.earliest;
|
|
currentSegmentNumber = pair.current;
|
|
numSegmentsBeforeCurrentSegment =
|
|
currentSegmentNumber - earliestSegmentNumber;
|
|
shaka.asserts.assert(numSegmentsBeforeCurrentSegment >= 0);
|
|
}
|
|
} else {
|
|
earliestSegmentNumber = 1;
|
|
}
|
|
shaka.asserts.assert(earliestSegmentNumber === undefined ||
|
|
earliestSegmentNumber >= 0);
|
|
|
|
// The optimal number of segment references to generate starting from, and
|
|
// including, the current segment
|
|
var numSegmentsFromCurrentSegment = 0;
|
|
|
|
// Note that if |earliestSegmentNumber| is undefined then the current segment
|
|
// is not available.
|
|
if (earliestSegmentNumber >= 0) {
|
|
numSegmentsFromCurrentSegment =
|
|
this.computeOptimalSegmentIndexSize_(mpd, period, segmentTemplate);
|
|
if (!numSegmentsFromCurrentSegment) {
|
|
// An error has already been logged.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var totalNumSegments =
|
|
numSegmentsBeforeCurrentSegment + numSegmentsFromCurrentSegment;
|
|
var references = [];
|
|
|
|
for (var i = 0; i < totalNumSegments; ++i) {
|
|
var segmentNumber = i + earliestSegmentNumber;
|
|
|
|
var startTime = (segmentNumber - 1) * segmentTemplate.segmentDuration;
|
|
var endTime = startTime + segmentTemplate.segmentDuration;
|
|
|
|
var scaledStartTime = startTime / segmentTemplate.timescale;
|
|
var scaledEndTime = endTime / segmentTemplate.timescale;
|
|
|
|
var absoluteSegmentNumber =
|
|
(segmentNumber - 1) + segmentTemplate.startNumber;
|
|
|
|
// Compute the media URL template placeholder replacements.
|
|
var segmentReplacement = absoluteSegmentNumber;
|
|
var timeReplacement =
|
|
((segmentNumber - 1) + (segmentTemplate.startNumber - 1)) *
|
|
segmentTemplate.segmentDuration;
|
|
|
|
// Generate the media URL.
|
|
shaka.asserts.assert(segmentTemplate.mediaUrlTemplate);
|
|
var filledUrlTemplate = this.fillUrlTemplate_(
|
|
segmentTemplate.mediaUrlTemplate,
|
|
representation.id,
|
|
segmentReplacement,
|
|
representation.bandwidth,
|
|
timeReplacement);
|
|
|
|
if (!filledUrlTemplate) {
|
|
// An error has already been logged.
|
|
return false;
|
|
}
|
|
|
|
var mediaUrl = representation.baseUrl ?
|
|
representation.baseUrl.resolve(filledUrlTemplate) :
|
|
filledUrlTemplate;
|
|
|
|
references.push(
|
|
new shaka.media.SegmentReference(
|
|
absoluteSegmentNumber,
|
|
scaledStartTime,
|
|
scaledEndTime,
|
|
0 /* startByte */,
|
|
null /* endByte */,
|
|
new goog.Uri(mediaUrl)));
|
|
}
|
|
|
|
// Generate an Initialization. If there are no references then assume that
|
|
// the intialization segment is not available.
|
|
var initialization = null;
|
|
if (segmentTemplate.initializationUrlTemplate && (references.length > 0)) {
|
|
initialization = this.generateInitialization_(representation);
|
|
if (!initialization) {
|
|
// An error has already been logged.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Set StreamInfo properties.
|
|
if (segmentTemplate.presentationTimeOffset != null) {
|
|
if (mpd.minUpdatePeriod) {
|
|
// Align @presentationTimeOffset to a segment boundary if possible.
|
|
// Round-up to ensure that we have the first segment.
|
|
var ptoInNumSegments =
|
|
Math.ceil(segmentTemplate.presentationTimeOffset /
|
|
segmentTemplate.segmentDuration);
|
|
var pto = ptoInNumSegments * segmentTemplate.segmentDuration;
|
|
streamInfo.timestampOffset = -1 * pto / segmentTemplate.timescale;
|
|
} else {
|
|
streamInfo.timestampOffset = -1 * segmentTemplate.presentationTimeOffset;
|
|
}
|
|
}
|
|
|
|
if (mpd.minUpdatePeriod && (references.length > 0)) {
|
|
shaka.asserts.assert(currentSegmentNumber);
|
|
var scaledSegmentDuration =
|
|
segmentTemplate.segmentDuration / segmentTemplate.timescale;
|
|
streamInfo.currentSegmentStartTime =
|
|
(currentSegmentNumber - 1) * scaledSegmentDuration;
|
|
}
|
|
|
|
streamInfo.segmentInitializationInfo =
|
|
this.createSegmentMetadataInfo_(initialization);
|
|
|
|
streamInfo.segmentIndex = new shaka.media.SegmentIndex(references);
|
|
shaka.log.debug('Generated SegmentIndex from segment duration',
|
|
streamInfo.segmentIndex);
|
|
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Computes the optimal number of segment references, N, for |period|. If the
|
|
* MPD is static then N * segmentDuration is the smallest multiple of
|
|
* segmentDuration >= |period|'s duration; if the MPD is dynamic then N *
|
|
* segmentDuration is the smallest multiple of segmentDuration >= the minimum
|
|
* of |period|'s duration, minimumUpdatePeriod, and MAX_SEGMENT_INDEX_SPAN.
|
|
*
|
|
* If the MPD is dynamic, and at least one segment is available, then N can be
|
|
* regarded as the number of segment references that we can generate right now,
|
|
* such that the generated segment references will all be valid when it's time
|
|
* to actually fetch the corresponding segments.
|
|
*
|
|
* @param {!shaka.dash.mpd.Mpd} mpd
|
|
* @param {!shaka.dash.mpd.Period} period
|
|
* @param {!shaka.dash.mpd.SegmentTemplate} segmentTemplate
|
|
* @return {?number}
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.computeOptimalSegmentIndexSize_ = function(
|
|
mpd, period, segmentTemplate) {
|
|
shaka.asserts.assert(segmentTemplate.segmentDuration);
|
|
shaka.asserts.assert(segmentTemplate.timescale > 0);
|
|
|
|
var duration;
|
|
if (mpd.type == 'static') {
|
|
if (period.duration != null) {
|
|
duration = period.duration;
|
|
} else {
|
|
shaka.log.error(
|
|
'Cannot instantiate SegmentTemplate: the Period\'s duration ' +
|
|
'is unknown.',
|
|
period);
|
|
return null;
|
|
}
|
|
} else {
|
|
// Note that |period|'s duration and @minimumUpdatePeriod may be very
|
|
// large, so fallback to a default value if necessary. The VideoSource is
|
|
// responsible for generating new SegmentIndexes when it needs them.
|
|
duration = Math.min(period.duration || Number.POSITIVE_INFINITY,
|
|
mpd.minUpdatePeriod || Number.POSITIVE_INFINITY,
|
|
shaka.dash.MpdProcessor.MAX_SEGMENT_INDEX_SPAN);
|
|
}
|
|
shaka.asserts.assert(
|
|
duration && (duration != Number.POSITIVE_INFINITY),
|
|
'duration should not be zero or infinity!');
|
|
|
|
var scaledSegmentDuration =
|
|
segmentTemplate.segmentDuration / segmentTemplate.timescale;
|
|
var n = Math.ceil(duration / scaledSegmentDuration);
|
|
|
|
shaka.log.v1('SegmentIndex span', duration);
|
|
shaka.log.v1('Optimal SegmentIndex size', n);
|
|
|
|
shaka.asserts.assert(n >= 1);
|
|
return n;
|
|
};
|
|
|
|
|
|
/**
|
|
* Computes the segment numbers of the earliest segment and the current
|
|
* segment, both relative to the start of |period|. Assumes the MPD is dynamic.
|
|
* |segmentTemplate| must have a segment duration.
|
|
*
|
|
* The earliest segment is the segment with the smallest start time that is
|
|
* still available from the media server. The current segment is the segment
|
|
* with the largest start time that is available from the media server and that
|
|
* also respects the suggestedPresentationDelay attribute and the minBufferTime
|
|
* attribute.
|
|
*
|
|
* @param {!shaka.dash.mpd.Mpd} mpd
|
|
* @param {!shaka.dash.mpd.Period} period
|
|
* @param {!shaka.dash.mpd.SegmentTemplate} segmentTemplate
|
|
* @return {?{earliest: number, current: number}} Two segment numbers, or null
|
|
* if the stream is not available yet.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.computeAvailableSegmentRange_ = function(
|
|
mpd, period, segmentTemplate) {
|
|
var currentTime = shaka.util.Clock.now() / 1000.0;
|
|
|
|
var availabilityStartTime = mpd.availabilityStartTime != null ?
|
|
mpd.availabilityStartTime :
|
|
currentTime;
|
|
|
|
if (availabilityStartTime > currentTime) {
|
|
shaka.log.warning('The stream is not available yet!', period);
|
|
return null;
|
|
}
|
|
|
|
var minBufferTime = mpd.minBufferTime || 0;
|
|
var suggestedPresentationDelay = mpd.suggestedPresentationDelay || 0;
|
|
|
|
// The following diagram shows the relationship between the values we use to
|
|
// compute the current segment number; descriptions of each value are given
|
|
// within the code. The diagram depicts the media presentation timeline. 0
|
|
// corresponds to availabilityStartTime + period.start in wall-clock time,
|
|
// and currentPresentationTime corresponds to currentTime in wall-clock time.
|
|
//
|
|
// Legend:
|
|
// CPT: currentPresentationTime
|
|
// EAT: earliestAvailableSegmentStartTime
|
|
// LAT: latestAvailableSegmentStartTime
|
|
// BAT: bestAvailableSegmentStartTime
|
|
// SD: scaledSegmentDuration.
|
|
// SPD: suggestedPresentationDelay
|
|
// MBT: minBufferTime
|
|
// TSB: timeShiftBufferDepth
|
|
//
|
|
// Time:
|
|
// <---|-----------------+--------+-----------------+----------|--------->
|
|
// 0 EAT BAT LAT CPT
|
|
// |---SD---|
|
|
// |-MBT-|--SPD--|
|
|
// |---SD---|---SD---|<--------TSB--------->|
|
|
// Segments:
|
|
// <---1--------2--------3--------4--------5--------6--------7--------8-->
|
|
// |---SD---|---SD---| ...
|
|
|
|
shaka.asserts.assert(segmentTemplate.segmentDuration);
|
|
shaka.asserts.assert(segmentTemplate.timescale > 0);
|
|
var scaledSegmentDuration =
|
|
segmentTemplate.segmentDuration / segmentTemplate.timescale;
|
|
|
|
// The current presentation time, which is the amount of time since the start
|
|
// of the Period.
|
|
var currentPresentationTime =
|
|
currentTime - (availabilityStartTime + period.start);
|
|
if (currentPresentationTime < 0) {
|
|
shaka.log.warning('The Period is not available yet!', period);
|
|
return null;
|
|
}
|
|
|
|
// Compute the segment start time of the earliest available segment, i.e.,
|
|
// the segment that starts furthest from the present but is still available).
|
|
// The MPD spec. indicates that
|
|
//
|
|
// SegmentAvailabilityStartTime =
|
|
// MpdAvailabilityStartTime + PeriodStart + SegmentStart + SegmentDuration
|
|
//
|
|
// SegmentAvailabilityEndTime =
|
|
// SegmentAvailabilityStartTime + SegmentDuration + TimeShiftBufferDepth
|
|
//
|
|
// So let SegmentAvailabilityEndTime equal the current time and compute
|
|
// SegmentStart, which yields the start time that a segment would need to
|
|
// have to have an availability end time equal to the current time.
|
|
//
|
|
// TODO: Use availabilityTimeOffset
|
|
var earliestAvailableTimestamp = currentPresentationTime -
|
|
(2 * scaledSegmentDuration) -
|
|
mpd.timeShiftBufferDepth;
|
|
if (earliestAvailableTimestamp < 0) {
|
|
earliestAvailableTimestamp = 0;
|
|
}
|
|
|
|
// Now round up to the nearest segment boundary, since the segment
|
|
// corresponding to |earliestAvailableTimestamp| is not available.
|
|
var earliestAvailableSegmentStartTime =
|
|
Math.ceil(earliestAvailableTimestamp / scaledSegmentDuration) *
|
|
scaledSegmentDuration;
|
|
|
|
// Compute the segment start time of the latest available segment, i.e., the
|
|
// segment that starts closest to the present but is available.
|
|
//
|
|
// Using the above formulas, let SegmentAvailabilityStartTime equal the
|
|
// current time and compute SegmentStart, which yields the start time that
|
|
// a segment would need to have to have an availability start time
|
|
// equal to the current time.
|
|
var latestAvailableTimestamp = currentPresentationTime -
|
|
scaledSegmentDuration;
|
|
if (latestAvailableTimestamp < 0) {
|
|
shaka.log.warning('The first segment is not available yet!', period);
|
|
return null;
|
|
}
|
|
|
|
// Now round down to the nearest segment boundary, since the segment
|
|
// corresponding to |latestAvailableTimestamp| may not yet be available.
|
|
var latestAvailableSegmentStartTime =
|
|
Math.floor(latestAvailableTimestamp / scaledSegmentDuration) *
|
|
scaledSegmentDuration;
|
|
|
|
// Now compute the start time of the "best" available segment, by offsetting
|
|
// by @suggestedPresentationDelay and @minBufferTime. Note that we subtract
|
|
// by @minBufferTime to ensure that after playback begins we can buffer at
|
|
// least @minBufferTime seconds worth of media content.
|
|
var bestAvailableTimestamp = latestAvailableSegmentStartTime -
|
|
suggestedPresentationDelay -
|
|
minBufferTime;
|
|
if (bestAvailableTimestamp < 0) {
|
|
shaka.log.warning('The first segment may not be available yet.');
|
|
bestAvailableTimestamp = 0;
|
|
// Don't return; taking into account @suggestedPresentationDelay is only a
|
|
// reccomendation. The first segment /might/ be available.
|
|
}
|
|
|
|
var bestAvailableSegmentStartTime =
|
|
Math.floor(bestAvailableTimestamp / scaledSegmentDuration) *
|
|
scaledSegmentDuration;
|
|
|
|
// Now take the larger of |bestAvailableSegmentStartTime| and
|
|
// |earliestAvailableSegmentStartTime|.
|
|
var currentSegmentStartTime;
|
|
if (bestAvailableSegmentStartTime >= earliestAvailableSegmentStartTime) {
|
|
currentSegmentStartTime = bestAvailableSegmentStartTime;
|
|
shaka.log.v1('The best available segment is still available!');
|
|
} else {
|
|
// NOTE: @suggestedPresentationDelay + @minBufferTime is large compared to
|
|
// @timeShiftBufferDepth, so we can't go back as far or buffer as much as
|
|
// we'd like.
|
|
currentSegmentStartTime = earliestAvailableSegmentStartTime;
|
|
shaka.log.v1('The best available segment is no longer available.');
|
|
}
|
|
|
|
var earliestSegmentNumber =
|
|
(earliestAvailableSegmentStartTime / scaledSegmentDuration) + 1;
|
|
shaka.asserts.assert(
|
|
earliestSegmentNumber == Math.round(earliestSegmentNumber),
|
|
'earliestSegmentNumber should be an integer.');
|
|
|
|
var currentSegmentNumber =
|
|
(currentSegmentStartTime / scaledSegmentDuration) + 1;
|
|
shaka.asserts.assert(
|
|
currentSegmentNumber == Math.round(currentSegmentNumber),
|
|
'currentSegmentNumber should be an integer.');
|
|
|
|
shaka.log.v1('earliestSegmentNumber', earliestSegmentNumber);
|
|
shaka.log.v1('currentSegmentNumber', currentSegmentNumber);
|
|
|
|
return { earliest: earliestSegmentNumber, current: currentSegmentNumber };
|
|
};
|
|
|
|
|
|
/**
|
|
* Generates a RepresentationIndex from a SegmentTemplate.
|
|
*
|
|
* @param {!shaka.dash.mpd.Representation} representation
|
|
* @return {shaka.dash.mpd.RepresentationIndex} A RepresentationIndex on
|
|
* success, null if no index URL template exists or an error occurred.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.generateRepresentationIndex_ = function(
|
|
representation) {
|
|
shaka.asserts.assert(representation.segmentTemplate);
|
|
|
|
var segmentTemplate = representation.segmentTemplate;
|
|
shaka.asserts.assert(segmentTemplate.indexUrlTemplate);
|
|
if (!segmentTemplate.indexUrlTemplate) {
|
|
return null;
|
|
}
|
|
|
|
var representationIndex = new shaka.dash.mpd.RepresentationIndex();
|
|
|
|
// $Number$ and $Time$ cannot be present in an index URL template.
|
|
var filledUrlTemplate = this.fillUrlTemplate_(
|
|
segmentTemplate.indexUrlTemplate,
|
|
representation.id,
|
|
null,
|
|
representation.bandwidth,
|
|
null);
|
|
|
|
if (!filledUrlTemplate) {
|
|
// An error has already been logged.
|
|
return null;
|
|
}
|
|
|
|
if (representation.baseUrl && filledUrlTemplate) {
|
|
representationIndex.url = representation.baseUrl.resolve(filledUrlTemplate);
|
|
} else {
|
|
representationIndex.url = filledUrlTemplate;
|
|
}
|
|
|
|
return representationIndex;
|
|
};
|
|
|
|
|
|
/**
|
|
* Generates an Initialization from a SegmentTemplate.
|
|
*
|
|
* @param {!shaka.dash.mpd.Representation} representation
|
|
* @return {shaka.dash.mpd.Initialization} An Initialization on success, null
|
|
* if no initialization URL template exists or an error occurred.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.generateInitialization_ = function(
|
|
representation) {
|
|
shaka.asserts.assert(representation.segmentTemplate);
|
|
|
|
var segmentTemplate = representation.segmentTemplate;
|
|
shaka.asserts.assert(segmentTemplate.initializationUrlTemplate);
|
|
if (!segmentTemplate.initializationUrlTemplate) {
|
|
return null;
|
|
}
|
|
|
|
var initialization = new shaka.dash.mpd.Initialization();
|
|
|
|
// $Number$ and $Time$ cannot be present in an initialization URL template.
|
|
var filledUrlTemplate = this.fillUrlTemplate_(
|
|
segmentTemplate.initializationUrlTemplate,
|
|
representation.id,
|
|
null,
|
|
representation.bandwidth,
|
|
null);
|
|
|
|
if (!filledUrlTemplate) {
|
|
// An error has already been logged.
|
|
return null;
|
|
}
|
|
|
|
if (representation.baseUrl && filledUrlTemplate) {
|
|
initialization.url = representation.baseUrl.resolve(filledUrlTemplate);
|
|
} else {
|
|
initialization.url = filledUrlTemplate;
|
|
}
|
|
|
|
return initialization;
|
|
};
|
|
|
|
|
|
/**
|
|
* 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; null if the resulting URL contains
|
|
* illegal characters.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.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 + '".');
|
|
return match;
|
|
}
|
|
|
|
if (name == 'RepresentationID' && widthString) {
|
|
shaka.log.warning(
|
|
'URL template should not contain a width specifier for identifier ' +
|
|
'"RepresentationID".');
|
|
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.');
|
|
return null;
|
|
}
|
|
throw exception;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Removes invalid Representations from |mpd|.
|
|
*
|
|
* @param {!shaka.dash.mpd.Mpd} mpd
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.filterPeriods_ = function(mpd) {
|
|
for (var i = 0; i < mpd.periods.length; ++i) {
|
|
var period = mpd.periods[i];
|
|
for (var j = 0; j < period.adaptationSets.length; ++j) {
|
|
var adaptationSet = period.adaptationSets[j];
|
|
this.filterAdaptationSet_(adaptationSet);
|
|
if (adaptationSet.representations.length == 0) {
|
|
// Drop any AdaptationSet that is empty.
|
|
// An error has already been logged.
|
|
period.adaptationSets.splice(j, 1);
|
|
--j;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Removes any Representation from the given AdaptationSet that has a different
|
|
* MIME type than the MIME type of the first Representation of the
|
|
* AdaptationSet.
|
|
*
|
|
* @param {!shaka.dash.mpd.AdaptationSet} adaptationSet
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.filterAdaptationSet_ = function(
|
|
adaptationSet) {
|
|
var desiredMimeType = null;
|
|
|
|
for (var i = 0; i < adaptationSet.representations.length; ++i) {
|
|
var representation = adaptationSet.representations[i];
|
|
var mimeType = representation.mimeType || '';
|
|
|
|
if (!desiredMimeType) {
|
|
desiredMimeType = mimeType;
|
|
} else if (mimeType != desiredMimeType) {
|
|
shaka.log.warning(
|
|
'Representation has an inconsistent mime type.',
|
|
adaptationSet.representations[i]);
|
|
adaptationSet.representations.splice(i, 1);
|
|
--i;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates a ManifestInfo from |mpd|.
|
|
*
|
|
* @param {!shaka.dash.mpd.Mpd} mpd
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.createManifestInfo_ = function(mpd) {
|
|
this.manifestInfo.live = mpd.minUpdatePeriod != null;
|
|
this.manifestInfo.minBufferTime = mpd.minBufferTime || 0;
|
|
|
|
for (var i = 0; i < mpd.periods.length; ++i) {
|
|
var period = mpd.periods[i];
|
|
|
|
var periodInfo = new shaka.media.PeriodInfo();
|
|
periodInfo.id = period.id;
|
|
|
|
shaka.asserts.assert(period.start != null);
|
|
periodInfo.start = period.start || 0;
|
|
periodInfo.duration = period.duration;
|
|
|
|
for (var j = 0; j < period.adaptationSets.length; ++j) {
|
|
var adaptationSet = period.adaptationSets[j];
|
|
|
|
var streamSetInfo = new shaka.media.StreamSetInfo();
|
|
streamSetInfo.id = adaptationSet.id;
|
|
streamSetInfo.main = adaptationSet.main;
|
|
streamSetInfo.contentType = adaptationSet.contentType || '';
|
|
streamSetInfo.lang = adaptationSet.lang || '';
|
|
|
|
// Keep track of the largest end time of all segment references so that
|
|
// we can set a Period duration if one was not explicitly set in the MPD
|
|
// or calculated from calculateDurations_().
|
|
var maxLastEndTime = 0;
|
|
|
|
for (var k = 0; k < adaptationSet.representations.length; ++k) {
|
|
var representation = adaptationSet.representations[k];
|
|
|
|
// Get common DRM schemes.
|
|
var commonDrmSchemes = streamSetInfo.drmSchemes.slice(0);
|
|
this.updateCommonDrmSchemes_(representation, commonDrmSchemes);
|
|
if (commonDrmSchemes.length == 0 &&
|
|
streamSetInfo.drmSchemes.length > 0) {
|
|
shaka.log.warning(
|
|
'Representation does not contain any DRM schemes that are in ' +
|
|
'common with other Representations within its AdaptationSet.',
|
|
representation);
|
|
continue;
|
|
}
|
|
|
|
var streamInfo = this.createStreamInfo_(mpd, period, representation);
|
|
if (!streamInfo) {
|
|
// An error has already been logged.
|
|
continue;
|
|
}
|
|
|
|
streamSetInfo.streamInfos.push(streamInfo);
|
|
streamSetInfo.drmSchemes = commonDrmSchemes;
|
|
|
|
if (streamInfo.segmentIndex && streamInfo.segmentIndex.length() > 0) {
|
|
maxLastEndTime =
|
|
Math.max(maxLastEndTime, streamInfo.segmentIndex.last().endTime);
|
|
}
|
|
}
|
|
|
|
periodInfo.streamSetInfos.push(streamSetInfo);
|
|
|
|
if (!periodInfo.duration) {
|
|
periodInfo.duration = maxLastEndTime;
|
|
|
|
// If the MPD is dynamic then the Period's duration will likely change
|
|
// after we re-process/update the MPD. When the Period's duration
|
|
// changes we must update the MediaSource object that is presenting the
|
|
// MPD's content, so we can append new media segments to the
|
|
// MediaSource's SourceBuffers. However, changing the MediaSource's
|
|
// duration is challenging as it requires synchronizing the states of
|
|
// multiple SourceBuffers. We can leave the Period's duration as
|
|
// undefined, but then we cannot seek (even programmatically).
|
|
//
|
|
// So, if the MPD is dynamic just set the Period's duration to a
|
|
// "large" value. This ensures that we can seek and that we can append
|
|
// new media segments. This does cause a poor UX if we use the video
|
|
// element's default controls, but we shouldn't use the default
|
|
// controls for live anyways.
|
|
//
|
|
// TODO: Remove this hack once SourceBuffer synchronization is
|
|
// implemented.
|
|
if (mpd.minUpdatePeriod) {
|
|
periodInfo.duration += 60 * 60 * 24;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.manifestInfo.periodInfos.push(periodInfo);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates a StreamInfo from the given Representation.
|
|
*
|
|
* @param {!shaka.dash.mpd.Mpd} mpd
|
|
* @param {!shaka.dash.mpd.Period} period
|
|
* @param {!shaka.dash.mpd.Representation} representation
|
|
* @return {shaka.media.StreamInfo} The new StreamInfo on success; otherwise,
|
|
* return null.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.createStreamInfo_ = function(
|
|
mpd, period, representation) {
|
|
var streamInfo = new shaka.media.StreamInfo();
|
|
|
|
streamInfo.id = representation.id;
|
|
streamInfo.minBufferTime = representation.minBufferTime || 0;
|
|
streamInfo.bandwidth = representation.bandwidth;
|
|
streamInfo.width = representation.width;
|
|
streamInfo.height = representation.height;
|
|
streamInfo.mimeType = representation.mimeType || '';
|
|
streamInfo.codecs = representation.codecs || '';
|
|
|
|
var ok;
|
|
|
|
if (representation.segmentBase) {
|
|
ok = this.buildStreamInfoFromSegmentBase_(
|
|
representation.segmentBase, streamInfo);
|
|
} else if (representation.segmentList) {
|
|
ok = this.buildStreamInfoFromSegmentList_(
|
|
representation.segmentList, streamInfo);
|
|
} else if (representation.segmentTemplate) {
|
|
ok = this.buildStreamInfoFromSegmentTemplate_(
|
|
mpd, period, representation, streamInfo);
|
|
} else if (representation.mimeType.split('/')[0] == 'text') {
|
|
// All we need is a URL for subtitles.
|
|
streamInfo.mediaUrl = new goog.Uri(representation.baseUrl);
|
|
ok = true;
|
|
} else {
|
|
shaka.asserts.unreachable();
|
|
}
|
|
|
|
return ok ? streamInfo : null;
|
|
};
|
|
|
|
|
|
/**
|
|
* Builds a StreamInfo from a SegmentBase.
|
|
*
|
|
* @param {!shaka.dash.mpd.SegmentBase} segmentBase
|
|
* @param {!shaka.media.StreamInfo} streamInfo
|
|
* @return {boolean} True on success.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.buildStreamInfoFromSegmentBase_ =
|
|
function(segmentBase, streamInfo) {
|
|
shaka.asserts.assert(segmentBase.timescale > 0);
|
|
|
|
var hasSegmentIndexMetadata =
|
|
segmentBase.indexRange ||
|
|
(segmentBase.representationIndex &&
|
|
segmentBase.representationIndex.range);
|
|
if (!hasSegmentIndexMetadata || !segmentBase.baseUrl) {
|
|
shaka.log.warning(
|
|
'A SegmentBase must have a segment index URL and a base URL.',
|
|
segmentBase);
|
|
return false;
|
|
}
|
|
|
|
if (segmentBase.presentationTimeOffset) {
|
|
// Each timestamp within each media segment is relative to the start of the
|
|
// Period minus @presentationTimeOffset. So to align the start of the first
|
|
// segment to the start of the Period we must apply an offset of -1 *
|
|
// @presentationTimeOffset seconds to each timestamp within each media
|
|
// segment.
|
|
streamInfo.timestampOffset =
|
|
-1 * segmentBase.presentationTimeOffset / segmentBase.timescale;
|
|
}
|
|
|
|
// If a RepresentationIndex does not exist then fallback to the indexRange
|
|
// attribute.
|
|
var representationIndex = segmentBase.representationIndex;
|
|
if (!representationIndex) {
|
|
representationIndex = new shaka.dash.mpd.RepresentationIndex();
|
|
representationIndex.url = new goog.Uri(segmentBase.baseUrl);
|
|
representationIndex.range = segmentBase.indexRange ?
|
|
segmentBase.indexRange.clone() :
|
|
null;
|
|
}
|
|
|
|
// Set StreamInfo properties.
|
|
streamInfo.mediaUrl = new goog.Uri(segmentBase.baseUrl);
|
|
|
|
streamInfo.segmentIndexInfo =
|
|
this.createSegmentMetadataInfo_(representationIndex);
|
|
|
|
streamInfo.segmentInitializationInfo =
|
|
this.createSegmentMetadataInfo_(segmentBase.initialization);
|
|
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Builds a StreamInfo from a SegmentList.
|
|
*
|
|
* @param {!shaka.dash.mpd.SegmentList} segmentList
|
|
* @param {!shaka.media.StreamInfo} streamInfo
|
|
* @return {boolean} True on success.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.buildStreamInfoFromSegmentList_ =
|
|
function(segmentList, streamInfo) {
|
|
shaka.asserts.assert(segmentList.timescale > 0);
|
|
|
|
if (!segmentList.segmentDuration && (segmentList.segmentUrls.length > 1)) {
|
|
shaka.log.warning(
|
|
'A SegmentList without a segment duration can only have one segment.',
|
|
segmentList);
|
|
return false;
|
|
}
|
|
|
|
streamInfo.segmentInitializationInfo =
|
|
this.createSegmentMetadataInfo_(segmentList.initialization);
|
|
|
|
var lastEndTime = 0;
|
|
var references = [];
|
|
|
|
for (var i = 0; i < segmentList.segmentUrls.length; ++i) {
|
|
var segmentUrl = segmentList.segmentUrls[i];
|
|
|
|
var absoluteSegmentNumber = i + segmentList.startNumber;
|
|
|
|
// Compute the segment's unscaled start time.
|
|
var startTime;
|
|
if (i == 0) {
|
|
startTime = 0;
|
|
} else {
|
|
startTime = lastEndTime;
|
|
}
|
|
shaka.asserts.assert(startTime >= 0);
|
|
|
|
var endTime = null;
|
|
var scaledEndTime = null;
|
|
|
|
var scaledStartTime = startTime / segmentList.timescale;
|
|
|
|
// If segmentList.segmentDuration is null then there must only be one
|
|
// segment.
|
|
if (segmentList.segmentDuration) {
|
|
endTime = startTime + segmentList.segmentDuration;
|
|
scaledEndTime = endTime / segmentList.timescale;
|
|
}
|
|
|
|
lastEndTime = endTime;
|
|
|
|
var startByte = 0;
|
|
var endByte = null;
|
|
if (segmentUrl.mediaRange) {
|
|
startByte = segmentUrl.mediaRange.begin;
|
|
endByte = segmentUrl.mediaRange.end;
|
|
}
|
|
|
|
references.push(
|
|
new shaka.media.SegmentReference(
|
|
absoluteSegmentNumber,
|
|
scaledStartTime,
|
|
scaledEndTime,
|
|
startByte,
|
|
endByte,
|
|
new goog.Uri(segmentUrl.mediaUrl)));
|
|
}
|
|
|
|
// Set StreamInfo properties.
|
|
streamInfo.segmentIndex = new shaka.media.SegmentIndex(references);
|
|
shaka.log.debug('Generated SegmentIndex from SegmentList',
|
|
streamInfo.segmentIndex);
|
|
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Builds a StreamInfo from a SegmentTemplate.
|
|
*
|
|
* @param {!shaka.dash.mpd.Mpd} mpd
|
|
* @param {!shaka.dash.mpd.Period} period
|
|
* @param {!shaka.dash.mpd.Representation} representation
|
|
* @param {!shaka.media.StreamInfo} streamInfo
|
|
* @return {boolean} True on success.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.buildStreamInfoFromSegmentTemplate_ =
|
|
function(mpd, period, representation, streamInfo) {
|
|
shaka.asserts.assert(representation.segmentTemplate);
|
|
var segmentTemplate = representation.segmentTemplate;
|
|
|
|
var ok;
|
|
|
|
// Prefer an explicit segment index URL, then a SegmentTimeline, and then a
|
|
// segment duration.
|
|
if (segmentTemplate.indexUrlTemplate) {
|
|
if (segmentTemplate.timeline) {
|
|
shaka.log.warning(
|
|
'Ignoring SegmentTimeline because an explicit segment index ' +
|
|
'URL was provided for the SegmentTemplate.',
|
|
representation);
|
|
}
|
|
if (segmentTemplate.segmentDuration) {
|
|
shaka.log.warning(
|
|
'Ignoring segment duration because an explicit segment index ' +
|
|
'URL was provided for the SegmentTemplate.',
|
|
representation);
|
|
}
|
|
ok = this.buildStreamInfoFromIndexUrlTemplate_(representation, streamInfo);
|
|
} else if (segmentTemplate.timeline) {
|
|
if (segmentTemplate.segmentDuration) {
|
|
shaka.log.warning(
|
|
'Ignoring segment duration because a SegmentTimeline was ' +
|
|
'provided for the SegmentTemplate.',
|
|
representation);
|
|
}
|
|
ok = this.buildStreamInfoFromSegmentTimeline_(
|
|
mpd, period, representation, streamInfo);
|
|
} else if (segmentTemplate.segmentDuration) {
|
|
ok = this.buildStreamInfoFromSegmentDuration_(
|
|
mpd, period, representation, streamInfo);
|
|
} else {
|
|
shaka.log.error(
|
|
'SegmentTemplate does not provide an explicit segment index URL, ' +
|
|
'a SegmentTimeline, or a segment duration.',
|
|
representation);
|
|
ok = false;
|
|
}
|
|
|
|
return ok;
|
|
};
|
|
|
|
|
|
/**
|
|
* Updates |commonDrmSchemes|.
|
|
*
|
|
* If |commonDrmSchemes| is empty then after this function is called
|
|
* |commonDrmSchemes| will equal |representation|'s application provided DRM
|
|
* schemes.
|
|
*
|
|
* Otherwise, if |commonDrmSchemes| is non-empty then after this function is
|
|
* called |commonDrmSchemes| will equal the intersection between
|
|
* |representation|'s application provided DRM schemes and |commonDrmSchemes|
|
|
* at the time this function was called.
|
|
*
|
|
* @param {!shaka.dash.mpd.Representation} representation
|
|
* @param {!Array.<!shaka.player.DrmSchemeInfo>} commonDrmSchemes
|
|
*
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.updateCommonDrmSchemes_ = function(
|
|
representation, commonDrmSchemes) {
|
|
var drmSchemes = this.getDrmSchemeInfos_(representation);
|
|
|
|
if (commonDrmSchemes.length == 0) {
|
|
Array.prototype.push.apply(commonDrmSchemes, drmSchemes);
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < commonDrmSchemes.length; ++i) {
|
|
var found = false;
|
|
for (var j = 0; j < drmSchemes.length; ++j) {
|
|
if (commonDrmSchemes[i].key() == drmSchemes[j].key()) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
commonDrmSchemes.splice(i, 1);
|
|
--i;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates a SegmentMetadataInfo from either a RepresentationIndex or an
|
|
* Initialization.
|
|
*
|
|
* @param {shaka.dash.mpd.RepresentationIndex|
|
|
* shaka.dash.mpd.Initialization} urlTypeObject
|
|
* @return {shaka.media.SegmentMetadataInfo}
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.createSegmentMetadataInfo_ = function(
|
|
urlTypeObject) {
|
|
if (!urlTypeObject) {
|
|
return null;
|
|
}
|
|
|
|
var segmentMetadataInfo = new shaka.media.SegmentMetadataInfo();
|
|
|
|
segmentMetadataInfo.url = urlTypeObject.url;
|
|
|
|
if (urlTypeObject.range) {
|
|
segmentMetadataInfo.startByte = urlTypeObject.range.begin;
|
|
segmentMetadataInfo.endByte = urlTypeObject.range.end;
|
|
}
|
|
|
|
return segmentMetadataInfo;
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets the application provided DrmSchemeInfos for the given Representation.
|
|
*
|
|
* @param {!shaka.dash.mpd.Representation} representation
|
|
* @return {!Array.<!shaka.player.DrmSchemeInfo>} The application provided
|
|
* DrmSchemeInfos. A dummy scheme, which has an empty |keySystem| string,
|
|
* is used for unencrypted content.
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.getDrmSchemeInfos_ =
|
|
function(representation) {
|
|
var drmSchemes = [];
|
|
if (representation.contentProtections.length == 0) {
|
|
// Return a single item which indicates that the content is unencrypted.
|
|
drmSchemes.push(shaka.player.DrmSchemeInfo.createUnencrypted());
|
|
} else if (this.interpretContentProtection_) {
|
|
for (var i = 0; i < representation.contentProtections.length; ++i) {
|
|
var contentProtection = representation.contentProtections[i];
|
|
var drmSchemeInfo = this.interpretContentProtection_(contentProtection);
|
|
if (drmSchemeInfo) {
|
|
drmSchemes.push(drmSchemeInfo);
|
|
}
|
|
}
|
|
}
|
|
return drmSchemes;
|
|
};
|
|
|