mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-15 16:06:41 +03:00
e5ce395f37
The bulk of the logic for gap jumping is handled in Playhead. It
tracks the current buffered ranges and jumps over any gaps that appear.
It listens for a special browser event ('waiting') for when the video
element runs out of playable frames.
This change also removes the logic for jumping gaps at the beginning
of the timeline. This is handled by the more general gap jumping
logic and works cross-browser.
Finally, this updates the buffering logic to only count the amount of
content buffered (i.e. ignoring the gaps). This fixes some bugs where
gaps in the content can result in StreamingEngine buffering forever
since it thinks only a little is buffered.
This includes full implementation of the logic, but this doesn't close
the issue since there aren't any integration tests yet. Those will
be added next.
Issue #555
Change-Id: Id99eb9fe469e8cf2c7464a3d70c3733791e806e0
437 lines
15 KiB
JavaScript
437 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,
|
|
presentationTimeOffset: info.presentationTimeOffset
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* timescale: number,
|
|
* segmentDuration: ?number,
|
|
* startNumber: number,
|
|
* presentationTimeOffset: 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} presentationTimeOffset
|
|
* The presentationTimeOffset of the representation, in seconds.
|
|
* @property {number} unscaledPresentationTimeOffset
|
|
* The presentationTimeOffset 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,
|
|
presentationTimeOffset: segmentInfo.presentationTimeOffset,
|
|
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.presentationTimeOffset);
|
|
};
|
|
|
|
|
|
/**
|
|
* 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 template = info.mediaTemplate;
|
|
var bandwidth = context.bandwidth || null;
|
|
var id = context.representation.id;
|
|
var baseUris = context.representation.baseUris;
|
|
|
|
var find = function(periodTime) {
|
|
if (periodTime < 0)
|
|
return null;
|
|
else if (periodDuration && periodTime >= periodDuration)
|
|
return null;
|
|
|
|
return Math.floor(periodTime / segmentDuration);
|
|
};
|
|
var get = function(position) {
|
|
var segmentStart = position * segmentDuration;
|
|
|
|
// Do not construct segments references that should not exist.
|
|
if (segmentStart < 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, segmentStart + segmentDuration, 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);
|
|
};
|
|
|