Files
shaka-player/lib/dash/segment_template.js
T
Joey Parrish 20750504fe Fix presentationTimeOffset in SegmentTemplate
We were not previously applying presentationTimeOffset in
SegmentTemplate w/ the duration attribute.  This fixes the DASH parser
to correctly apply the offset in this case.

This also fixes an issue in which segment times could stretch past the
end of the period or even past the end of the presentation, causing
StreamingEngine to fail period transitions afterward.

Closes #1164
Closes #1163 (PR to address 1164)

Change-Id: Ib16fc2b65117557a4ad9a05adc4a07f8bc90fd3c
2017-12-13 18:46:28 +00:00

443 lines
15 KiB
JavaScript

/**
* @license
* Copyright 2016 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.SegmentTemplate');
goog.require('goog.asserts');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.dash.SegmentBase');
goog.require('shaka.log');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.Error');
goog.require('shaka.util.ManifestParserUtils');
/**
* @namespace shaka.dash.SegmentTemplate
* @summary A set of functions for parsing SegmentTemplate elements.
*/
/**
* Creates a new Stream object or updates the Stream in the manifest.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment
* @param {!Object.<string, !shaka.media.SegmentIndex>} segmentIndexMap
* @param {boolean} isUpdate True if the manifest is being updated.
* @throws shaka.util.Error When there is a parsing error.
* @return {shaka.dash.DashParser.StreamInfo}
*/
shaka.dash.SegmentTemplate.createStream = function(
context, requestInitSegment, segmentIndexMap, isUpdate) {
goog.asserts.assert(context.representation.segmentTemplate,
'Should only be called with SegmentTemplate');
var SegmentTemplate = shaka.dash.SegmentTemplate;
var init = SegmentTemplate.createInitSegment_(context);
var info = SegmentTemplate.parseSegmentTemplateInfo_(context);
SegmentTemplate.checkSegmentTemplateInfo_(context, info);
/** @type {?shaka.dash.DashParser.SegmentIndexFunctions} */
var segmentIndexFunctions = null;
if (info.indexTemplate) {
segmentIndexFunctions = SegmentTemplate.createFromIndexTemplate_(
context, requestInitSegment, init, info);
} else if (info.segmentDuration) {
if (!isUpdate) {
context.presentationTimeline.notifyMaxSegmentDuration(
info.segmentDuration);
}
segmentIndexFunctions = SegmentTemplate.createFromDuration_(context, info);
} else {
/** @type {shaka.media.SegmentIndex} */
var segmentIndex = null;
var id = null;
if (context.period.id && context.representation.id) {
// Only check/store the index if period and representation IDs are set.
id = context.period.id + ',' + context.representation.id;
segmentIndex = segmentIndexMap[id];
}
var references = SegmentTemplate.createFromTimeline_(context, info);
if (segmentIndex) {
segmentIndex.merge(references);
var start = context.presentationTimeline.getSegmentAvailabilityStart();
segmentIndex.evict(start - context.periodInfo.start);
} else {
context.presentationTimeline.notifySegments(
context.periodInfo.start, references);
segmentIndex = new shaka.media.SegmentIndex(references);
if (id && context.dynamic)
segmentIndexMap[id] = segmentIndex;
}
if (!context.dynamic || !context.periodInfo.isLastPeriod)
segmentIndex.fit(context.periodInfo.duration);
segmentIndexFunctions = {
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: segmentIndex.find.bind(segmentIndex),
getSegmentReference: segmentIndex.get.bind(segmentIndex)
};
}
return {
createSegmentIndex: segmentIndexFunctions.createSegmentIndex,
findSegmentPosition: segmentIndexFunctions.findSegmentPosition,
getSegmentReference: segmentIndexFunctions.getSegmentReference,
initSegmentReference: init,
scaledPresentationTimeOffset: info.scaledPresentationTimeOffset
};
};
/**
* @typedef {{
* timescale: number,
* segmentDuration: ?number,
* startNumber: number,
* scaledPresentationTimeOffset: number,
* unscaledPresentationTimeOffset: number,
* timeline: Array.<shaka.dash.MpdUtils.TimeRange>,
* mediaTemplate: ?string,
* indexTemplate: ?string
* }}
* @private
*
* @description
* Contains information about a SegmentTemplate.
*
* @property {number} timescale
* The time-scale of the representation.
* @property {?number} segmentDuration
* The duration of the segments in seconds, if given.
* @property {number} startNumber
* The start number of the segments; 1 or greater.
* @property {number} scaledPresentationTimeOffset
* The presentation time offset of the representation, in seconds.
* @property {number} unscaledPresentationTimeOffset
* The presentation time offset of the representation, in timescale units.
* @property {Array.<shaka.dash.MpdUtils.TimeRange>} timeline
* The timeline of the representation, if given. Times in seconds.
* @property {?string} mediaTemplate
* The media URI template, if given.
* @property {?string} indexTemplate
* The index URI template, if given.
*/
shaka.dash.SegmentTemplate.SegmentTemplateInfo;
/**
* @param {?shaka.dash.DashParser.InheritanceFrame} frame
* @return {Element}
* @private
*/
shaka.dash.SegmentTemplate.fromInheritance_ = function(frame) {
return frame.segmentTemplate;
};
/**
* Parses a SegmentTemplate element into an info object.
*
* @param {shaka.dash.DashParser.Context} context
* @return {shaka.dash.SegmentTemplate.SegmentTemplateInfo}
* @private
*/
shaka.dash.SegmentTemplate.parseSegmentTemplateInfo_ = function(context) {
var SegmentTemplate = shaka.dash.SegmentTemplate;
var MpdUtils = shaka.dash.MpdUtils;
var segmentInfo =
MpdUtils.parseSegmentInfo(context, SegmentTemplate.fromInheritance_);
var media = MpdUtils.inheritAttribute(
context, SegmentTemplate.fromInheritance_, 'media');
var index = MpdUtils.inheritAttribute(
context, SegmentTemplate.fromInheritance_, 'index');
return {
segmentDuration: segmentInfo.segmentDuration,
timescale: segmentInfo.timescale,
startNumber: segmentInfo.startNumber,
scaledPresentationTimeOffset: segmentInfo.scaledPresentationTimeOffset,
unscaledPresentationTimeOffset: segmentInfo.unscaledPresentationTimeOffset,
timeline: segmentInfo.timeline,
mediaTemplate: media,
indexTemplate: index
};
};
/**
* Verifies a SegmentTemplate info object.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
* @throws shaka.util.Error When there is a parsing error.
* @private
*/
shaka.dash.SegmentTemplate.checkSegmentTemplateInfo_ = function(context, info) {
var n = 0;
n += info.indexTemplate ? 1 : 0;
n += info.timeline ? 1 : 0;
n += info.segmentDuration ? 1 : 0;
if (n == 0) {
shaka.log.error(
'SegmentTemplate does not contain any segment information:',
'the SegmentTemplate must contain either an index URL template',
'a SegmentTimeline, or a segment duration.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
} 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.',
context.representation);
if (info.indexTemplate) {
shaka.log.info('Using the index URL template by default.');
info.timeline = null;
info.segmentDuration = null;
} else {
goog.asserts.assert(info.timeline, 'There should be a timeline');
shaka.log.info('Using the SegmentTimeline by default.');
info.segmentDuration = null;
}
}
if (!info.indexTemplate && !info.mediaTemplate) {
shaka.log.error(
'SegmentTemplate does not contain sufficient segment information:',
'the SegmentTemplate\'s media URL template is missing.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
}
};
/**
* Creates segment index functions from a index URL template.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment
* @param {shaka.media.InitSegmentReference} init
* @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
* @throws shaka.util.Error When there is a parsing error.
* @return {shaka.dash.DashParser.SegmentIndexFunctions}
* @private
*/
shaka.dash.SegmentTemplate.createFromIndexTemplate_ = function(
context, requestInitSegment, init, info) {
var MpdUtils = shaka.dash.MpdUtils;
var ManifestParserUtils = shaka.util.ManifestParserUtils;
// Determine the container type.
var containerType = context.representation.mimeType.split('/')[1];
if ((containerType != 'mp4') && (containerType != 'webm')) {
shaka.log.error(
'SegmentTemplate specifies an unsupported container type.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_UNSUPPORTED_CONTAINER);
}
if ((containerType == 'webm') && !init) {
shaka.log.error(
'SegmentTemplate does not contain sufficient segment information:',
'the SegmentTemplate uses a WebM container,',
'but does not contain an initialization URL template.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_WEBM_MISSING_INIT);
}
goog.asserts.assert(info.indexTemplate, 'must be using index template');
var filledTemplate = MpdUtils.fillUriTemplate(
info.indexTemplate, context.representation.id,
null, context.bandwidth || null, null);
var resolvedUris = ManifestParserUtils.resolveUris(
context.representation.baseUris, [filledTemplate]);
return shaka.dash.SegmentBase.createSegmentIndexFromUris(
context, requestInitSegment, init, resolvedUris, 0, null, containerType,
info.scaledPresentationTimeOffset);
};
/**
* Creates segment index functions from a segment duration.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
* @return {shaka.dash.DashParser.SegmentIndexFunctions}
* @private
*/
shaka.dash.SegmentTemplate.createFromDuration_ = function(context, info) {
goog.asserts.assert(info.mediaTemplate,
'There should be a media template with duration');
var MpdUtils = shaka.dash.MpdUtils;
var ManifestParserUtils = shaka.util.ManifestParserUtils;
var periodDuration = context.periodInfo.duration;
var segmentDuration = info.segmentDuration;
var startNumber = info.startNumber;
var timescale = info.timescale;
var scaledPresentationTimeOffset = info.scaledPresentationTimeOffset;
var template = info.mediaTemplate;
var bandwidth = context.bandwidth || null;
var id = context.representation.id;
var baseUris = context.representation.baseUris;
var find = function(periodTime) {
var segmentTime = periodTime + scaledPresentationTimeOffset;
if (periodTime < 0)
return null;
else if (periodDuration && periodTime >= periodDuration)
return null;
return Math.floor(segmentTime / segmentDuration);
};
var get = function(position) {
var segmentStart =
position * segmentDuration - scaledPresentationTimeOffset;
// Cap the segment end at the period end, to avoid period transition issues
// in StreamingEngine.
var segmentEnd = Math.min(segmentStart + segmentDuration, periodDuration);
// Do not construct segments references that should not exist.
if (segmentEnd < 0)
return null;
else if (periodDuration && segmentStart >= periodDuration)
return null;
var getUris = function() {
var mediaUri = MpdUtils.fillUriTemplate(
template, id, position + startNumber, bandwidth,
segmentStart * timescale);
return ManifestParserUtils.resolveUris(baseUris, [mediaUri]);
};
return new shaka.media.SegmentReference(
position, segmentStart, segmentEnd, getUris, 0, null);
};
return {
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: find,
getSegmentReference: get
};
};
/**
* Creates segment references from a timeline.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
* @return {!Array.<!shaka.media.SegmentReference>}
* @private
*/
shaka.dash.SegmentTemplate.createFromTimeline_ = function(context, info) {
goog.asserts.assert(info.mediaTemplate,
'There should be a media template with a timeline');
var MpdUtils = shaka.dash.MpdUtils;
var ManifestParserUtils = shaka.util.ManifestParserUtils;
/** @type {!Array.<!shaka.media.SegmentReference>} */
var references = [];
for (var i = 0; i < info.timeline.length; i++) {
var start = info.timeline[i].start;
var unscaledStart = info.timeline[i].unscaledStart;
var end = info.timeline[i].end;
// Note: i = k - 1, where k indicates the k'th segment listed in the MPD.
// (See section 5.3.9.5.3 of the DASH spec.)
var segmentReplacement = i + info.startNumber;
// Consider the presentation time offset in segment uri computation
var timeReplacement = unscaledStart +
info.unscaledPresentationTimeOffset;
var createUris = (function(
template, repId, bandwidth, baseUris, segmentId, time) {
var mediaUri = MpdUtils.fillUriTemplate(
template, repId, segmentId, bandwidth, time);
return ManifestParserUtils.resolveUris(baseUris, [mediaUri])
.map(function(g) { return g.toString(); });
}.bind(null, info.mediaTemplate, context.representation.id,
context.bandwidth || null, context.representation.baseUris,
segmentReplacement, timeReplacement));
references.push(new shaka.media.SegmentReference(
segmentReplacement, start, end, createUris, 0, null));
}
return references;
};
/**
* Creates an init segment reference from a context object.
*
* @param {shaka.dash.DashParser.Context} context
* @return {shaka.media.InitSegmentReference}
* @private
*/
shaka.dash.SegmentTemplate.createInitSegment_ = function(context) {
var MpdUtils = shaka.dash.MpdUtils;
var ManifestParserUtils = shaka.util.ManifestParserUtils;
var SegmentTemplate = shaka.dash.SegmentTemplate;
var initialization = MpdUtils.inheritAttribute(
context, SegmentTemplate.fromInheritance_, 'initialization');
if (!initialization)
return null;
var repId = context.representation.id;
var bandwidth = context.bandwidth || null;
var baseUris = context.representation.baseUris;
var getUris = function() {
goog.asserts.assert(initialization, 'Should have returned earler');
var filledTemplate = MpdUtils.fillUriTemplate(
initialization, repId, null, bandwidth, null);
var resolvedUris = ManifestParserUtils.resolveUris(
baseUris, [filledTemplate]);
return resolvedUris;
};
return new shaka.media.InitSegmentReference(getUris, 0, null);
};