UNPKG

shaka-player

Version:
459 lines (400 loc) 16.4 kB
/** * @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'); const SegmentTemplate = shaka.dash.SegmentTemplate; let init = SegmentTemplate.createInitSegment_(context); let info = SegmentTemplate.parseSegmentTemplateInfo_(context); SegmentTemplate.checkSegmentTemplateInfo_(context, info); /** @type {?shaka.dash.DashParser.SegmentIndexFunctions} */ let segmentIndexFunctions = null; if (info.indexTemplate) { segmentIndexFunctions = SegmentTemplate.createFromIndexTemplate_( context, requestInitSegment, init, info); } else if (info.segmentDuration) { if (!isUpdate) { context.presentationTimeline.notifyMaxSegmentDuration( info.segmentDuration); context.presentationTimeline.notifyMinSegmentStartTime( context.periodInfo.start); } segmentIndexFunctions = SegmentTemplate.createFromDuration_(context, info); } else { /** @type {shaka.media.SegmentIndex} */ let segmentIndex = null; let 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]; } let references = SegmentTemplate.createFromTimeline_(context, info); // Don't fit live content, since it might receive more segments. // Unless that live content is multi-period; it's safe to fit every period // but the last one, since only the last period might receive new segments. let shouldFit = !context.dynamic || !context.periodInfo.isLastPeriod; if (segmentIndex) { if (shouldFit) { // Fit the new references before merging them, so that the merge // algorithm has a more accurate view of their start and end times. let wrapper = new shaka.media.SegmentIndex(references); wrapper.fit(context.periodInfo.duration); } segmentIndex.merge(references); let start = context.presentationTimeline.getSegmentAvailabilityStart(); segmentIndex.evict(start - context.periodInfo.start); } else { context.presentationTimeline.notifySegments( references, context.periodInfo.start); segmentIndex = new shaka.media.SegmentIndex(references); if (id && context.dynamic) { segmentIndexMap[id] = segmentIndex; } } if (shouldFit) { 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) { const SegmentTemplate = shaka.dash.SegmentTemplate; const MpdUtils = shaka.dash.MpdUtils; let segmentInfo = MpdUtils.parseSegmentInfo(context, SegmentTemplate.fromInheritance_); let media = MpdUtils.inheritAttribute( context, SegmentTemplate.fromInheritance_, 'media'); let 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) { let 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) { const MpdUtils = shaka.dash.MpdUtils; const ManifestParserUtils = shaka.util.ManifestParserUtils; // Determine the container type. let 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'); let filledTemplate = MpdUtils.fillUriTemplate( info.indexTemplate, context.representation.id, null, context.bandwidth || null, null); let 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'); const MpdUtils = shaka.dash.MpdUtils; const ManifestParserUtils = shaka.util.ManifestParserUtils; let periodDuration = context.periodInfo.duration; let segmentDuration = info.segmentDuration; let startNumber = info.startNumber; let timescale = info.timescale; let template = info.mediaTemplate; let bandwidth = context.bandwidth || null; let id = context.representation.id; let baseUris = context.representation.baseUris; let find = function(periodTime) { if (periodTime < 0) { return null; } else if (periodDuration && periodTime >= periodDuration) { return null; } return Math.floor(periodTime / segmentDuration); }; let get = function(position) { let segmentStart = position * segmentDuration; // Cap the segment end at the period end, to avoid period transition issues // in StreamingEngine. let segmentEnd = segmentStart + segmentDuration; if (periodDuration) segmentEnd = Math.min(segmentEnd, periodDuration); // Do not construct segments references that should not exist. if (segmentEnd < 0) { return null; } else if (periodDuration && segmentStart >= periodDuration) { return null; } let getUris = function() { let 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'); const MpdUtils = shaka.dash.MpdUtils; const ManifestParserUtils = shaka.util.ManifestParserUtils; /** @type {!Array.<!shaka.media.SegmentReference>} */ let references = []; for (let i = 0; i < info.timeline.length; i++) { let start = info.timeline[i].start; let unscaledStart = info.timeline[i].unscaledStart; let 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.) let segmentReplacement = i + info.startNumber; // Consider the presentation time offset in segment uri computation let timeReplacement = unscaledStart + info.unscaledPresentationTimeOffset; let createUris = (function( template, repId, bandwidth, baseUris, segmentId, time) { let 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) { const MpdUtils = shaka.dash.MpdUtils; const ManifestParserUtils = shaka.util.ManifestParserUtils; const SegmentTemplate = shaka.dash.SegmentTemplate; let initialization = MpdUtils.inheritAttribute( context, SegmentTemplate.fromInheritance_, 'initialization'); if (!initialization) { return null; } let repId = context.representation.id; let bandwidth = context.bandwidth || null; let baseUris = context.representation.baseUris; let getUris = function() { goog.asserts.assert(initialization, 'Should have returned earler'); let filledTemplate = MpdUtils.fillUriTemplate( initialization, repId, null, bandwidth, null); let resolvedUris = ManifestParserUtils.resolveUris( baseUris, [filledTemplate]); return resolvedUris; }; return new shaka.media.InitSegmentReference(getUris, 0, null); };