/**
* @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.MpdProcessor');
goog.require('goog.Uri');
goog.require('shaka.asserts');
goog.require('shaka.dash.ContainerSegmentIndexSource');
goog.require('shaka.dash.DurationSegmentIndexSource');
goog.require('shaka.dash.ListSegmentIndexSource');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.dash.TimelineSegmentIndexSource');
goog.require('shaka.dash.mpd');
goog.require('shaka.features');
goog.require('shaka.log');
goog.require('shaka.media.PeriodInfo');
goog.require('shaka.media.SegmentInitSource');
goog.require('shaka.media.StreamInfo');
goog.require('shaka.media.StreamSetInfo');
goog.require('shaka.media.TextSegmentIndexSource');
goog.require('shaka.util.FailoverUri');
/**
* Creates an MpdProcessor, which validates MPDs, calculates start, duration,
* and other missing attributes, removes invalid Periods, AdaptationSets, and
* 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;
};
/**
* The default value, in seconds, for MPD@minBufferTime if this attribute is
* missing.
* @const {number}
*/
shaka.dash.MpdProcessor.DEFAULT_MIN_BUFFER_TIME = 5.0;
/**
* Processes the given MPD.
* This function modifies |mpd| but does not take ownership of it.
*
* @param {!shaka.dash.mpd.Mpd} mpd
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @return {!shaka.media.ManifestInfo}
*/
shaka.dash.MpdProcessor.prototype.process = function(mpd, networkCallback) {
var manifestCreationTime = shaka.util.Clock.now() / 1000.0;
this.validateSegmentInfo_(mpd);
this.calculateDurations_(mpd);
this.filterPeriods_(mpd);
if ((mpd.type == 'dynamic') && (mpd.availabilityStartTime == null)) {
// Assume broadcasting just started.
shaka.log.warning(
'The MPD is \'dynamic\' but @availabilityStartTime is not specified:',
'treating @availabilityStartTime as if it were the current time.');
mpd.availabilityStartTime = manifestCreationTime;
}
return this.createManifestInfo_(mpd, manifestCreationTime, networkCallback);
};
/**
* 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:',
'the 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:',
'the Representation should only contain one of SegmentBase,',
'SegmentList, 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
}
}
};
/**
* 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; };
// Ignore @mediaPresentationDuration if the MPD is dynamic.
// TODO: Consider using @mediaPresentationDuration or other duration
// attributes for signalling the end of a live stream.
if (mpd.type == 'dynamic') {
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 != 'dynamic') {
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;
}
}
}
}
};
/**
* 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 does not have the same MIME type as other',
'Representations within its AdaptationSet.',
adaptationSet.representations[i]);
adaptationSet.representations.splice(i, 1);
--i;
}
}
};
/**
* Creates a ManifestInfo from |mpd|.
*
* @param {!shaka.dash.mpd.Mpd} mpd
* @param {number} manifestCreationTime The time, in seconds, when the manifest
* was created.
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @return {!shaka.media.ManifestInfo}
* @private
*/
shaka.dash.MpdProcessor.prototype.createManifestInfo_ = function(
mpd, manifestCreationTime, networkCallback) {
var manifestInfo = new shaka.media.ManifestInfo();
if (mpd.type == 'dynamic') {
manifestInfo.live = true;
manifestInfo.updatePeriod = mpd.minUpdatePeriod;
// Prefer the URL specified by the Location element.
manifestInfo.updateUrl = new shaka.util.FailoverUri(
networkCallback, mpd.updateLocation || mpd.url);
manifestInfo.availabilityStartTime = mpd.availabilityStartTime;
}
manifestInfo.minBufferTime = mpd.minBufferTime ||
shaka.dash.MpdProcessor.DEFAULT_MIN_BUFFER_TIME;
for (var i = 0; i < mpd.periods.length; ++i) {
var period = mpd.periods[i];
if (period.start == null) {
shaka.log.warning(
'Skipping Period', i + 1, 'and any subsequent Periods:',
'Period', i + 1, 'does not have a valid start time.',
period);
break;
}
var periodInfo = this.createPeriodInfo_(
mpd, period, manifestCreationTime, networkCallback);
manifestInfo.periodInfos.push(periodInfo);
}
return manifestInfo;
};
/**
* Creates a PeriodInfo.
*
* @param {!shaka.dash.mpd.Mpd} mpd
* @param {!shaka.dash.mpd.Period} period
* @param {number} manifestCreationTime The time, in seconds, when the manifest
* was created.
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @return {!shaka.media.PeriodInfo}
* @private
*/
shaka.dash.MpdProcessor.prototype.createPeriodInfo_ = function(
mpd, period, manifestCreationTime, networkCallback) {
shaka.asserts.assert(period.start != null);
var periodInfo = new shaka.media.PeriodInfo();
periodInfo.id = period.id;
periodInfo.start = /** @type {number} */(period.start);
periodInfo.duration = period.duration;
// First group AdaptationSets by type.
var setsByType = new shaka.util.MultiMap();
period.adaptationSets.forEach(
function(set) { setsByType.push(set.contentType || '', set); });
var setTypes = setsByType.keys();
for (var typeIdx = 0; typeIdx < setTypes.length; ++typeIdx) {
var type = setTypes[typeIdx];
// Then group AdaptationSets of the same type by group.
var setsByGroup = new shaka.util.MultiMap();
setsByType.get(type).forEach(
function(set) { setsByGroup.push(set.group, set); });
var setGroups = setsByGroup.keys();
for (var groupIdx = 0; groupIdx < setGroups.length; ++groupIdx) {
var group = setGroups[groupIdx];
shaka.asserts.assert(group != null);
// Finally group AdaptationSets of the same type and group by language,
// then squash them into the same StreamSetInfo.
var setsByLang = new shaka.util.MultiMap();
setsByGroup.get(group).forEach(
function(set) { setsByLang.push(set.lang, set); });
var setLangs = setsByLang.keys();
for (var langIdx = 0; langIdx < setLangs.length; ++langIdx) {
var lang = setLangs[langIdx];
var sets = /** @type {!Array.<shaka.dash.mpd.AdaptationSet>} */(
setsByLang.get(lang));
var streamSetInfo = this.createStreamSetInfo_(
mpd, period, sets, manifestCreationTime, networkCallback);
periodInfo.streamSetInfos.push(streamSetInfo);
} // for langIdx
} // for groupIdx
} // for typeIdx
return periodInfo;
};
/**
* Creates a StreamSetInfo from AdaptationSets of the same type, group, and
* language.
*
* @param {!shaka.dash.mpd.Mpd} mpd
* @param {!shaka.dash.mpd.Period} period
* @param {Array.<!shaka.dash.mpd.AdaptationSet>} adaptationSets
* AdaptationSets from the same group.
* @param {number} manifestCreationTime The time, in seconds, when the manifest
* was created.
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @return {!shaka.media.StreamSetInfo}
* @private
*/
shaka.dash.MpdProcessor.prototype.createStreamSetInfo_ = function(
mpd, period, adaptationSets, manifestCreationTime, networkCallback) {
shaka.asserts.assert(adaptationSets.length > 0);
shaka.asserts.assert(adaptationSets.every(function(set) {
return (set.group == adaptationSets[0].group) &&
((set.lang || '') == (adaptationSets[0].lang || '')) &&
((set.contentType || '') == (adaptationSets[0].contentType || ''));
}));
var streamSetInfo = new shaka.media.StreamSetInfo();
// Construct a StreamSetInfo ID from the AdaptationSet IDs. Consider the
// entire StreamSetInfo unidentifiable if an AdaptationSet (in the group)
// does not have an ID. StreamSetInfo IDs must remain identical between
// manifest updates, see shaka.media.ManifestUpdater.
var identifiable = adaptationSets
.filter(function(set) { return set.id != null; });
if (identifiable.length == adaptationSets.length) {
streamSetInfo.id = identifiable
.map(function(set) { return set.id; })
.sort()
.reduce(function(all, part) { return all + ',' + part; });
}
streamSetInfo.lang = adaptationSets[0].lang || '';
streamSetInfo.contentType = adaptationSets[0].contentType || '';
streamSetInfo.main = adaptationSets.some(
function(set) { return set.main; });
// Maps each StreamInfo to its AdaptationSet, which we use below to enable
// certain StreamInfos.
var streamInfoUidToAdaptationSet = {};
for (var i = 0; i < adaptationSets.length; ++i) {
var adaptationSet = adaptationSets[i];
for (var j = 0; j < adaptationSet.representations.length; ++j) {
var representation = adaptationSet.representations[j];
var drmInfos = this.getDrmInfos_(representation);
// Update the common DrmInfos.
var commonDrmInfos = streamSetInfo.drmInfos.slice(0);
this.updateCommonDrmInfos_(drmInfos, commonDrmInfos);
if (commonDrmInfos.length == 0 && streamSetInfo.drmInfos.length > 0) {
shaka.log.warning(
'Representation does not contain any ContentProtection elements',
'that are compatible with other Representations within its',
'AdaptationSet or AdaptationSet group.',
representation);
continue;
}
var streamInfo = this.createStreamInfo_(
mpd, period, representation, manifestCreationTime, networkCallback);
if (!streamInfo) {
// An error has already been logged.
continue;
}
streamSetInfo.streamInfos.push(streamInfo);
streamSetInfo.drmInfos = commonDrmInfos;
streamInfoUidToAdaptationSet[streamInfo.uniqueId] = adaptationSet;
} // for j
} // for i
// If the streams are unencrypted then we don't require a key system.
var unencrypted = streamSetInfo.drmInfos.some(
function(drmInfo) { return drmInfo.keySystem == ''; });
if (unencrypted) {
streamSetInfo.streamInfos.forEach(
function(streamInfo) { streamInfo.allowedByKeySystem = true; });
return streamSetInfo;
}
// If the streams are encrypted then assume the key system can probably
// decrypt the lowest quality streams. This enables the VideoSource to
// select at least one stream for stream startup without having to query the
// key system first.
//
// The lowest quality streams are the streams that are contained within
// the AdaptationSet which contains THE lowest quality stream.
//
// For example, given an MPD that looks like
// <AdaptationSet id="A1" group="1">
// <Representation id="R1" width="360" height="240" />
// <Representation id="R2" width="640" height="480" />
// </AdaptationSet>
// <AdaptationSet id="A2' group="1">
// <ContentProtection cenc:default_KID="01234567890ABCDEF" />
// <Representation id="R3" width="1280" height="720" />
// <Representation id="R4" width="1920" height="1080" />
// </AdaptationSet>
// the lowest quality streams are R1 and R2.
var lowestQualitySet = this.findLowestQualityAdaptationSet_(adaptationSets);
for (var i = 0; i < streamSetInfo.streamInfos.length; ++i) {
var streamInfo = streamSetInfo.streamInfos[i];
var adaptationSet = streamInfoUidToAdaptationSet[streamInfo.uniqueId];
if (adaptationSet == lowestQualitySet) {
shaka.log.v1(
'Assuming the key system can decrypt stream', streamInfo.id + '.');
streamInfo.allowedByKeySystem = true;
}
}
return streamSetInfo;
};
/**
* Returns the AdaptationSet that contains the lowest quality Representation.
*
* @param {!Array.<!shaka.dash.mpd.AdaptationSet>} adaptationSets
* @return {shaka.dash.mpd.AdaptationSet}
* @private
*/
shaka.dash.MpdProcessor.prototype.findLowestQualityAdaptationSet_ = function(
adaptationSets) {
var lowestQuality = null;
var lowestQualitySet = null;
for (var i = 0; i < adaptationSets.length; ++i) {
var adaptationSet = adaptationSets[i];
for (var j = 0; j < adaptationSet.representations.length; ++j) {
var representation = adaptationSet.representations[j];
var quality = (representation.width || 1) *
(representation.height || 1) *
(representation.bandwidth || 1);
if ((lowestQuality == null) || (quality < lowestQuality)) {
lowestQuality = quality;
lowestQualitySet = adaptationSet;
}
}
}
return lowestQualitySet;
};
/**
* Sets |commonDrmInfos| to |drmInfos| if |commonDrmInfos| is empty; otherwise,
* sets |commonDrmInfos| to the intersection between |commonDrmInfos| and
* |drmInfos|.
*
* @param {!Array.<!shaka.player.DrmInfo>} drmInfos
* @param {!Array.<!shaka.player.DrmInfo>} commonDrmInfos
*
* @private
*/
shaka.dash.MpdProcessor.prototype.updateCommonDrmInfos_ = function(
drmInfos, commonDrmInfos) {
if (commonDrmInfos.length == 0) {
Array.prototype.push.apply(commonDrmInfos, drmInfos);
return;
}
for (var i = 0; i < commonDrmInfos.length; ++i) {
var found = false;
for (var j = 0; j < drmInfos.length; ++j) {
if (commonDrmInfos[i].isCompatible(drmInfos[j])) {
found = true;
commonDrmInfos[i].addInitDatas(drmInfos[j].initDatas);
break;
}
}
if (!found) {
commonDrmInfos.splice(i, 1);
--i;
}
}
};
/**
* Gets the application provided DrmInfos for the given Representation.
*
* @param {!shaka.dash.mpd.Representation} representation
* @return {!Array.<!shaka.player.DrmInfo>} The application provided
* DrmInfos. A dummy DrmInfo, which has an empty |keySystem| string,
* is used for unencrypted content.
* @private
*/
shaka.dash.MpdProcessor.prototype.getDrmInfos_ = function(representation) {
var drmInfos = [];
if (representation.contentProtections.length == 0) {
// Return a single item which indicates that the content is unencrypted.
drmInfos.push(new shaka.player.DrmInfo());
} else if (this.interpretContentProtection_) {
for (var i = 0; i < representation.contentProtections.length; ++i) {
var contentProtection = representation.contentProtections[i];
drmInfos.push.apply(drmInfos, this.createDrmInfos_(contentProtection));
}
}
return drmInfos;
};
/**
* @param {!shaka.dash.mpd.ContentProtection} contentProtection
* @return {!Array.<!shaka.player.DrmInfo>}
* @private
*/
shaka.dash.MpdProcessor.prototype.createDrmInfos_ = function(
contentProtection) {
var drmInfos = [];
var newStyle = this.interpretContentProtection_.length == 2;
if (!newStyle) {
shaka.log.error(
'You must use the new-style ContentProtection interpretation API.',
'See shaka.player.DashVideoSource.ContentProtectionCallback.');
return [];
}
var schemeId = contentProtection.schemeIdUri || '';
var element = /** @type {!Node} */ (contentProtection.element);
var configs = this.interpretContentProtection_(schemeId, element);
if (!configs) {
return [];
}
if (!(configs instanceof Array)) {
shaka.log.error(
'ContentProtection interpretation callback must return',
'an array of shaka.player.DrmInfo.Config objects.');
return [];
}
for (var i = 0; i < configs.length; ++i) {
var drmInfo = shaka.player.DrmInfo.createFromConfig(configs[i]);
if (drmInfo.initDatas.length == 0 &&
contentProtection.pssh &&
contentProtection.pssh.psshBox) {
var initData = {
'initData': contentProtection.pssh.psshBox,
'initDataType': 'cenc'
};
drmInfo.addInitDatas([initData]);
}
drmInfos.push(drmInfo);
}
return drmInfos;
};
/**
* 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
* @param {number} manifestCreationTime The time, in seconds, when the manifest
* was created.
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @return {shaka.media.StreamInfo} The new StreamInfo on success; otherwise,
* return null.
* @private
*/
shaka.dash.MpdProcessor.prototype.createStreamInfo_ = function(
mpd, period, representation, manifestCreationTime, networkCallback) {
if (!representation.baseUrl || representation.baseUrl.length === 0) {
shaka.log.warning(
'Representation does not contain sufficient segment information:',
'the Representation must contain a BaseURL.',
representation);
return null;
}
var streamInfo = null;
var timescale = 1;
var presentationTimeOffset = 0;
if (representation.segmentBase) {
streamInfo = this.createStreamInfoFromSegmentBase_(
mpd, period, representation, manifestCreationTime, networkCallback);
timescale = representation.segmentBase.timescale;
presentationTimeOffset = representation.segmentBase.presentationTimeOffset;
} else if (representation.segmentList) {
streamInfo = this.createStreamInfoFromSegmentList_(
mpd, period, representation, manifestCreationTime, networkCallback);
timescale = representation.segmentList.timescale;
presentationTimeOffset = representation.segmentList.presentationTimeOffset;
} else if (representation.segmentTemplate) {
streamInfo = this.createStreamInfoFromSegmentTemplate_(
mpd, period, representation, manifestCreationTime, networkCallback);
timescale = representation.segmentTemplate.timescale;
presentationTimeOffset =
representation.segmentTemplate.presentationTimeOffset;
} else if (representation.mimeType.split('/')[0] == 'text') {
streamInfo = new shaka.media.StreamInfo();
streamInfo.segmentIndexSource = new shaka.media.TextSegmentIndexSource(
new shaka.util.FailoverUri(networkCallback, representation.baseUrl));
} else {
shaka.asserts.unreachable();
}
if (!streamInfo) {
// An error has already been logged.
return null;
}
streamInfo.id = representation.id;
if (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 * presentationTimeOffset / timescale;
}
streamInfo.bandwidth = representation.bandwidth;
streamInfo.width = representation.width;
streamInfo.height = representation.height;
streamInfo.mimeType = representation.mimeType || '';
streamInfo.codecs = representation.codecs || '';
for (var i = 0; i < representation.contentProtections.length; ++i) {
// Since we don't know which key system we will use yet, map each
// cenc:default_KID that the manifest specifies.
var contentProtection = representation.contentProtections[i];
if (contentProtection.defaultKeyId) {
streamInfo.keyIds.push(contentProtection.defaultKeyId);
}
}
return streamInfo;
};
/**
* Creates a StreamInfo from a SegmentBase.
*
* @param {!shaka.dash.mpd.Mpd} mpd
* @param {!shaka.dash.mpd.Period} period
* @param {!shaka.dash.mpd.Representation} representation
* @param {number} manifestCreationTime The time, in seconds, when the manifest
* was created.
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @return {shaka.media.StreamInfo} A streamInfo on success; otherwise,
* return null.
* @private
*/
shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentBase_ = function(
mpd, period, representation, manifestCreationTime, networkCallback) {
shaka.asserts.assert(representation.segmentBase);
shaka.asserts.assert(representation.segmentBase.timescale > 0);
if (!shaka.features.Containers) {
shaka.log.error('Parsing of containers not supported in this build.');
return null;
}
// Determine the container type.
var containerType = representation.mimeType.split('/')[1];
if ((containerType != 'mp4') && (containerType != 'webm')) {
shaka.log.warning(
'SegmentBase specifies an unsupported container type.',
representation);
return null;
}
var segmentBase = representation.segmentBase;
if ((containerType == 'webm') && !segmentBase.initialization) {
shaka.log.warning(
'SegmentBase does not contain sufficient segment information:',
'the SegmentBase uses a WebM container,',
'but does not contain an Initialization element.',
segmentBase);
return null;
}
var hasSegmentIndexMetadata =
segmentBase.indexRange ||
(segmentBase.representationIndex &&
segmentBase.representationIndex.range);
if (!hasSegmentIndexMetadata) {
shaka.log.warning(
'SegmentBase does not contain sufficient segment information:',
'the SegmentBase does not contain @indexRange',
'or a RepresentationIndex element.',
segmentBase);
return null;
}
// If a RepresentationIndex does not exist then fallback to @indexRange.
var representationIndex = segmentBase.representationIndex;
if (!representationIndex) {
representationIndex = new shaka.dash.mpd.RepresentationIndex();
representationIndex.url = representation.baseUrl;
representationIndex.range = segmentBase.indexRange ?
segmentBase.indexRange.clone() :
null;
}
var indexMetadata = this.createSegmentMetadata_(
representationIndex, networkCallback);
var initMetadata =
segmentBase.initialization ?
this.createSegmentMetadata_(segmentBase.initialization, networkCallback) :
null;
var segmentIndexSource =
new shaka.dash.ContainerSegmentIndexSource(
mpd,
period,
containerType,
indexMetadata,
initMetadata,
manifestCreationTime,
networkCallback);
var segmentInitSource = new shaka.media.SegmentInitSource(initMetadata);
var streamInfo = new shaka.media.StreamInfo();
streamInfo.segmentIndexSource = segmentIndexSource;
streamInfo.segmentInitSource = segmentInitSource;
return streamInfo;
};
/**
* Creates a StreamInfo from a SegmentList.
*
* @param {!shaka.dash.mpd.Mpd} mpd
* @param {!shaka.dash.mpd.Period} period
* @param {!shaka.dash.mpd.Representation} representation
* @param {number} manifestCreationTime The time, in seconds, when the manifest
* was created.
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @return {shaka.media.StreamInfo} A StreamInfo on success; otherwise,
* return null.
* @private
*/
shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentList_ =
function(mpd, period, representation, manifestCreationTime,
networkCallback) {
shaka.asserts.assert(representation.segmentList);
if (!shaka.features.Live) {
shaka.log.error('SegmentList requires Live, which is disabled.');
return null;
}
var segmentList = representation.segmentList;
if (!segmentList.segmentDuration && !segmentList.timeline &&
(segmentList.segmentUrls.length > 1)) {
shaka.log.warning(
'SegmentList does not contain sufficient segment information:',
'the SegmentList specifies multiple segments,',
'but does not specify a segment duration or timeline.',
segmentList);
return null;
}
if (!segmentList.segmentDuration && !period.duration &&
!segmentList.timeline && (segmentList.segmentUrls.length == 1)) {
shaka.log.warning(
'SegmentList does not contain sufficient segment information:',
'the SegmentList specifies one segment,',
'but does not specify a segment duration, period duration,',
'or timeline.',
segmentList);
return null;
}
if (segmentList.timeline && segmentList.timeline.timePoints.length === 0) {
shaka.log.warning(
'SegmentList does not contain sufficient segment information:',
'the SegmentList has an empty timeline.',
segmentList);
return null;
}
var initMetadata =
segmentList.initialization ?
this.createSegmentMetadata_(segmentList.initialization, networkCallback) :
null;
var segmentIndexSource =
new shaka.dash.ListSegmentIndexSource(
mpd, period, representation, manifestCreationTime, networkCallback);
var segmentInitSource = new shaka.media.SegmentInitSource(initMetadata);
var streamInfo = new shaka.media.StreamInfo();
streamInfo.segmentIndexSource = segmentIndexSource;
streamInfo.segmentInitSource = segmentInitSource;
return streamInfo;
};
/**
* Creates a StreamInfo from a SegmentTemplate
*
* @param {!shaka.dash.mpd.Mpd} mpd
* @param {!shaka.dash.mpd.Period} period
* @param {!shaka.dash.mpd.Representation} representation
* @param {number} manifestCreationTime The time, in seconds, when the manifest
* was created.
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @return {shaka.media.StreamInfo} A StreamInfo on success; otherwise,
* return null.
* @private
*/
shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentTemplate_ =
function(mpd, period, representation, manifestCreationTime,
networkCallback) {
shaka.asserts.assert(representation.segmentTemplate);
if (!shaka.features.Live) {
shaka.log.error('SegmentTemplate requires Live, which is disabled.');
return null;
}
var segmentTemplate = /** @type {!shaka.dash.mpd.SegmentTemplate} */ (
representation.segmentTemplate);
if (!this.validateSegmentTemplate_(segmentTemplate)) {
// An error has already been logged.
return null;
}
// Generate an Initialization.
var initialization = null;
if (segmentTemplate.initializationUrlTemplate) {
initialization = this.generateInitialization_(representation);
if (!initialization) {
// An error has already been logged.
return null;
}
}
var initMetadata =
initialization ? this.createSegmentMetadata_(
initialization, networkCallback) : null;
var segmentIndexSource = this.makeSegmentIndexSourceViaSegmentTemplate_(
mpd, period, representation, manifestCreationTime, initMetadata,
networkCallback);
if (!segmentIndexSource) {
// An error has already been logged.
return null;
}
var segmentInitSource = new shaka.media.SegmentInitSource(initMetadata);
var streamInfo = new shaka.media.StreamInfo();
streamInfo.segmentIndexSource = segmentIndexSource;
streamInfo.segmentInitSource = segmentInitSource;
return streamInfo;
};
/**
* Ensures that |segmentTemplate| has either an index URL template, a
* SegmentTimeline, or a segment duration.
*
* @param {!shaka.dash.mpd.SegmentTemplate} segmentTemplate
* @return {boolean}
* @private
*/
shaka.dash.MpdProcessor.prototype.validateSegmentTemplate_ = function(
segmentTemplate) {
var n = 0;
n += segmentTemplate.indexUrlTemplate ? 1 : 0;
n += segmentTemplate.timeline ? 1 : 0;
n += segmentTemplate.segmentDuration ? 1 : 0;
if (n == 0) {
shaka.log.warning(
'SegmentTemplate does not contain any segment information:',
'the SegmentTemplate must contain either an index URL template',
'a SegmentTimeline, or a segment duration.',
segmentTemplate);
return false;
} else if (n != 1) {
shaka.log.warning(
'SegmentTemplate containes multiple segment information sources:',
'the SegmentTemplate should only contain an index URL template,',
'a SegmentTimeline or a segment duration.',
segmentTemplate);
if (segmentTemplate.indexUrlTemplate) {
shaka.log.info('Using the index URL template by default.');
segmentTemplate.timeline = null;
segmentTemplate.segmentDuration = null;
} else if (segmentTemplate.timeline) {
shaka.log.info('Using the SegmentTimeline by default.');
segmentTemplate.segmentDuration = null;
} else {
shaka.asserts.unreachable();
}
}
return true;
};
/**
* Creates an ISegmentIndexSource from a SegmentTemplate.
*
* @param {!shaka.dash.mpd.Mpd} mpd
* @param {!shaka.dash.mpd.Period} period
* @param {!shaka.dash.mpd.Representation} representation
* @param {number} manifestCreationTime The time, in seconds, when the manifest
* was created.
* @param {shaka.util.FailoverUri} initMetadata
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @return {shaka.media.ISegmentIndexSource} A SegmentIndexSource on success;
* otherwise, return null.
* @private
*/
shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaSegmentTemplate_ =
function(mpd, period, representation, manifestCreationTime, initMetadata,
networkCallback) {
shaka.asserts.assert(representation.segmentTemplate);
var segmentTemplate = representation.segmentTemplate;
if (segmentTemplate.indexUrlTemplate) {
return this.makeSegmentIndexSourceViaIndexUrlTemplate_(
mpd, period, representation, manifestCreationTime, initMetadata,
networkCallback);
}
if (!segmentTemplate.mediaUrlTemplate) {
shaka.log.warning(
'SegmentTemplate does not contain sufficient segment information:',
'the SegmentTemplate\'s media URL template is missing.',
representation);
return null;
}
if (segmentTemplate.timeline) {
return new shaka.dash.TimelineSegmentIndexSource(
mpd, period, representation, manifestCreationTime, networkCallback);
} else if (segmentTemplate.segmentDuration) {
if ((mpd.type != 'dynamic') && (period.duration == null)) {
shaka.log.warning(
'SegmentTemplate does not contain sufficient segment information:',
'the Period\'s duration is not known.',
representation);
return null;
}
return new shaka.dash.DurationSegmentIndexSource(
mpd, period, representation, manifestCreationTime, networkCallback);
}
shaka.asserts.unreachable();
return null;
};
/**
* Creates an ISegmentIndexSource from a SegmentTemplate with an index URL
* template.
*
* @param {!shaka.dash.mpd.Mpd} mpd
* @param {!shaka.dash.mpd.Period} period
* @param {!shaka.dash.mpd.Representation} representation
* @param {number} manifestCreationTime The time, in seconds, when the manifest
* was created.
* @param {shaka.util.FailoverUri} initMetadata
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @return {shaka.media.ISegmentIndexSource} A SegmentIndexSource on success;
* otherwise, return null.
* @private
*/
shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaIndexUrlTemplate_ =
function(mpd, period, representation, manifestCreationTime, initMetadata,
networkCallback) {
shaka.asserts.assert(representation.segmentTemplate);
shaka.asserts.assert(representation.segmentTemplate.indexUrlTemplate);
if (!shaka.features.Containers) {
shaka.log.error('Parsing of containers not supported in this build.');
return null;
}
// Determine the container type.
var containerType = representation.mimeType.split('/')[1];
if ((containerType != 'mp4') && (containerType != 'webm')) {
shaka.log.warning(
'SegmentTemplate specifies an unsupported container type.',
representation);
return null;
}
var segmentTemplate = representation.segmentTemplate;
if ((containerType == 'webm') && !initMetadata) {
shaka.log.warning(
'SegmentTemplate does not contain sufficient segment information:',
'the SegmentTemplate uses a WebM container,',
'but does not contain an initialization URL template.',
segmentTemplate);
return null;
}
// Generate the media URL.
var mediaUrl = shaka.dash.MpdUtils.createFromTemplate(
networkCallback, representation, 1, 0, 0, null);
if (!mediaUrl) {
// An error has already been logged.
return null;
}
// Generate a RepresentationIndex.
var representationIndex = this.generateRepresentationIndex_(representation);
if (!representationIndex) {
// An error has already been logged.
return null;
}
var indexMetadata = this.createSegmentMetadata_(
representationIndex, networkCallback);
var segmentIndexSource =
new shaka.dash.ContainerSegmentIndexSource(
mpd,
period,
containerType,
indexMetadata,
initMetadata,
manifestCreationTime,
networkCallback);
return segmentIndexSource;
};
/**
* Generates a RepresentationIndex from a SegmentTemplate.
*
* @param {!shaka.dash.mpd.Representation} representation
* @return {shaka.dash.mpd.RepresentationIndex} A RepresentationIndex on
* success; otherwise, return 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 urlTemplate = representation.segmentTemplate.indexUrlTemplate;
if (!urlTemplate) return null;
return this.generateUrlTypeObject_(
representation, urlTemplate, shaka.dash.mpd.RepresentationIndex);
};
/**
* Generates an Initialization from a SegmentTemplate.
*
* @param {!shaka.dash.mpd.Representation} representation
* @return {shaka.dash.mpd.Initialization} An Initialization on success;
* otherwise return 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 urlTemplate = representation.segmentTemplate.initializationUrlTemplate;
if (!urlTemplate) return null;
return this.generateUrlTypeObject_(
representation, urlTemplate, shaka.dash.mpd.Initialization);
};
/**
* Generates either an Initialization or a RepresentationIndex.
*
* @param {!shaka.dash.mpd.Representation} representation
* @param {string} urlTemplate
* @param {!function(new:T)} constructor
* @return {T}
* @template T
* @private
*/
shaka.dash.MpdProcessor.prototype.generateUrlTypeObject_ = function(
representation, urlTemplate, constructor) {
shaka.asserts.assert(representation.segmentTemplate);
var segmentTemplate = representation.segmentTemplate;
// $Number$ and $Time$ cannot be present in an initialization URL template.
var filledUrlTemplate = shaka.dash.MpdUtils.fillUrlTemplate(
urlTemplate,
representation.id,
null,
representation.bandwidth,
null);
if (!filledUrlTemplate) {
// An error has already been logged.
return null;
}
/**
* @type {!shaka.dash.mpd.RepresentationIndex|
* !shaka.dash.mpd.Initialization}
*/
var urlTypeObject = new constructor();
urlTypeObject.url = shaka.util.FailoverUri.resolve(
representation.baseUrl, filledUrlTemplate);
return urlTypeObject;
};
/**
* Creates a SegmentMetadata from either a RepresentationIndex or an
* Initialization.
*
* @param {!shaka.dash.mpd.RepresentationIndex|
* !shaka.dash.mpd.Initialization} urlTypeObject
* @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
* @return {!shaka.util.FailoverUri}
* @private
*/
shaka.dash.MpdProcessor.prototype.createSegmentMetadata_ = function(
urlTypeObject, networkCallback) {
var url = urlTypeObject.url;
var startByte = 0;
var endByte = null;
if (urlTypeObject.range) {
startByte = urlTypeObject.range.begin;
endByte = urlTypeObject.range.end;
}
return new shaka.util.FailoverUri(networkCallback, url, startByte, endByte);
};