mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-18 16:36:56 +03:00
dc008cc6cf
Issue #67 Change-Id: I7df48dcad94823b2937b3bc88a0d5d9c6087edc5
1104 lines
36 KiB
JavaScript
1104 lines
36 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.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.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(
|
|
null, mpd.updateLocation || mpd.url);
|
|
}
|
|
|
|
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 = 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];
|
|
shaka.asserts.assert(adaptationSet.group != null);
|
|
|
|
var streamSetInfo = new shaka.media.StreamSetInfo();
|
|
streamSetInfo.id = adaptationSet.id;
|
|
streamSetInfo.group = /** @type {number} */(adaptationSet.group);
|
|
streamSetInfo.lang = adaptationSet.lang || '';
|
|
streamSetInfo.contentType = adaptationSet.contentType || '';
|
|
streamSetInfo.main = adaptationSet.main;
|
|
|
|
for (var k = 0; k < adaptationSet.representations.length; ++k) {
|
|
var representation = adaptationSet.representations[k];
|
|
|
|
// Get common DRM infos.
|
|
var commonDrmInfos = streamSetInfo.drmInfos.slice(0);
|
|
this.updateCommonDrmInfos_(representation, 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.',
|
|
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;
|
|
} // for k
|
|
|
|
periodInfo.streamSetInfos.push(streamSetInfo);
|
|
} // for j
|
|
|
|
manifestInfo.periodInfos.push(periodInfo);
|
|
} // for i
|
|
|
|
return manifestInfo;
|
|
};
|
|
|
|
|
|
/**
|
|
* Updates |commonDrmInfos|.
|
|
*
|
|
* If |commonDrmInfos| is empty then after this function is called
|
|
* |commonDrmInfos| will equal |representation|'s application provided
|
|
* DrmInfos.
|
|
*
|
|
* Otherwise, if |commonDrmInfos| is non-empty then after this function is
|
|
* called |commonDrmInfos| will equal the intersection between
|
|
* |representation|'s application provided DrmInfos and |commonDrmInfos| at the
|
|
* time this function was called.
|
|
*
|
|
* @param {!shaka.dash.mpd.Representation} representation
|
|
* @param {!Array.<!shaka.player.DrmInfo>} commonDrmInfos
|
|
*
|
|
* @private
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.updateCommonDrmInfos_ = function(
|
|
representation, commonDrmInfos) {
|
|
var drmInfos = this.getDrmInfos_(representation);
|
|
|
|
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;
|
|
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
|
|
* @suppress {accessControls} to use deprecated shaka.player.DrmSchemeInfo
|
|
* without an error.
|
|
*/
|
|
shaka.dash.MpdProcessor.prototype.createDrmInfos_ = function(
|
|
contentProtection) {
|
|
var drmInfos = [];
|
|
|
|
var newStyle = this.interpretContentProtection_.length == 2;
|
|
if (newStyle) {
|
|
var configs = this.interpretContentProtection_(
|
|
contentProtection.schemeIdUri, contentProtection.element);
|
|
if (!configs) {
|
|
return [];
|
|
}
|
|
|
|
if (!(configs instanceof Array)) {
|
|
shaka.log.error(
|
|
'New-style 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]);
|
|
}
|
|
|
|
if (contentProtection.defaultKeyId) {
|
|
drmInfo.addKeyIds([contentProtection.defaultKeyId]);
|
|
}
|
|
|
|
drmInfos.push(drmInfo);
|
|
}
|
|
} else {
|
|
// TODO: Remove when DrmSchemeInfo is removed.
|
|
shaka.log.warning(
|
|
'Please use the new-style ContentProtection interpretation API.',
|
|
'See shaka.player.DashVideoSource.ContentProtectionCallback.');
|
|
|
|
var drmSchemeInfo = this.interpretContentProtection_(contentProtection);
|
|
if (!drmSchemeInfo) {
|
|
return [];
|
|
}
|
|
|
|
if (!(drmSchemeInfo instanceof shaka.player.DrmSchemeInfo)) {
|
|
shaka.log.error(
|
|
'Old-style ContentProtection interpretation callback must return',
|
|
'an instance of shaka.player.DrmSchemeInfo.');
|
|
return [];
|
|
}
|
|
|
|
var drmInfo = shaka.player.DrmInfo.createFromDrmSchemeInfo(drmSchemeInfo);
|
|
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 || '';
|
|
|
|
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);
|
|
|
|
// 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);
|
|
|
|
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);
|
|
|
|
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 a SegmentIndexSource 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();
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates a SegmentIndexSource 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);
|
|
|
|
// 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);
|
|
};
|
|
|