UNPKG

shaka-player

Version:
582 lines (512 loc) 21.8 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ 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.Iterables'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.ObjectUtils'); goog.requireType('shaka.dash.DashParser'); /** * @summary A set of functions for parsing SegmentTemplate elements. */ shaka.dash.SegmentTemplate = class { /** * Creates a new StreamInfo object. * Updates the existing SegmentIndex, if any. * * @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. * @param {number} segmentLimit The maximum number of segments to generate for * a SegmentTemplate with fixed duration. * @return {shaka.dash.DashParser.StreamInfo} */ static createStreamInfo( context, requestInitSegment, segmentIndexMap, isUpdate, segmentLimit) { goog.asserts.assert(context.representation.segmentTemplate, 'Should only be called with SegmentTemplate'); const SegmentTemplate = shaka.dash.SegmentTemplate; const initSegmentReference = SegmentTemplate.createInitSegment_(context); const info = SegmentTemplate.parseSegmentTemplateInfo_(context); SegmentTemplate.checkSegmentTemplateInfo_(context, info); // Direct fields of context will be reassigned by the parser before // generateSegmentIndex is called. So we must make a shallow copy first, // and use that in the generateSegmentIndex callbacks. const shallowCopyOfContext = shaka.util.ObjectUtils.shallowCloneObject(context); if (info.indexTemplate) { shaka.dash.SegmentBase.checkSegmentIndexSupport( context, initSegmentReference); return { generateSegmentIndex: () => { return SegmentTemplate.generateSegmentIndexFromIndexTemplate_( shallowCopyOfContext, requestInitSegment, initSegmentReference, info); }, }; } else if (info.segmentDuration) { if (!isUpdate) { context.presentationTimeline.notifyMaxSegmentDuration( info.segmentDuration); context.presentationTimeline.notifyMinSegmentStartTime( context.periodInfo.start); } return { generateSegmentIndex: () => { return SegmentTemplate.generateSegmentIndexFromDuration_( shallowCopyOfContext, info, segmentLimit, initSegmentReference); }, }; } 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]; } const references = SegmentTemplate.createFromTimeline_( shallowCopyOfContext, info, initSegmentReference); const periodStart = context.periodInfo.start; const periodEnd = context.periodInfo.duration ? context.periodInfo.start + context.periodInfo.duration : Infinity; // 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. const shouldFit = periodEnd != Infinity; 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. const wrapper = new shaka.media.SegmentIndex(references); wrapper.fit(periodStart, periodEnd); } segmentIndex.merge(references); segmentIndex.evict( context.presentationTimeline.getSegmentAvailabilityStart()); } else { context.presentationTimeline.notifySegments(references); segmentIndex = new shaka.media.SegmentIndex(references); if (id && context.dynamic) { segmentIndexMap[id] = segmentIndex; } } if (shouldFit) { segmentIndex.fit(periodStart, periodEnd); } return { generateSegmentIndex: () => Promise.resolve(segmentIndex), }; } } /** * @param {?shaka.dash.DashParser.InheritanceFrame} frame * @return {Element} * @private */ static fromInheritance_(frame) { return frame.segmentTemplate; } /** * Parses a SegmentTemplate element into an info object. * * @param {shaka.dash.DashParser.Context} context * @return {shaka.dash.SegmentTemplate.SegmentTemplateInfo} * @private */ static parseSegmentTemplateInfo_(context) { const SegmentTemplate = shaka.dash.SegmentTemplate; const MpdUtils = shaka.dash.MpdUtils; const segmentInfo = MpdUtils.parseSegmentInfo(context, SegmentTemplate.fromInheritance_); const media = MpdUtils.inheritAttribute( context, SegmentTemplate.fromInheritance_, 'media'); const 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 * @private */ static checkSegmentTemplateInfo_(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); } } /** * Generates a SegmentIndex from an 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 * @return {!Promise.<shaka.media.SegmentIndex>} * @private */ static generateSegmentIndexFromIndexTemplate_( context, requestInitSegment, init, info) { const MpdUtils = shaka.dash.MpdUtils; const ManifestParserUtils = shaka.util.ManifestParserUtils; goog.asserts.assert(info.indexTemplate, 'must be using index template'); const filledTemplate = MpdUtils.fillUriTemplate( info.indexTemplate, context.representation.id, null, context.bandwidth || null, null); const resolvedUris = ManifestParserUtils.resolveUris( context.representation.baseUris, [filledTemplate]); return shaka.dash.SegmentBase.generateSegmentIndexFromUris( context, requestInitSegment, init, resolvedUris, 0, null, info.scaledPresentationTimeOffset); } /** * Generates a SegmentIndex from fixed-duration segments. * * @param {shaka.dash.DashParser.Context} context * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info * @param {number} segmentLimit The maximum number of segments to generate. * @param {shaka.media.InitSegmentReference} initSegmentReference * @return {!Promise.<shaka.media.SegmentIndex>} * @private */ static generateSegmentIndexFromDuration_( context, info, segmentLimit, initSegmentReference) { goog.asserts.assert(info.mediaTemplate, 'There should be a media template with duration'); const MpdUtils = shaka.dash.MpdUtils; const ManifestParserUtils = shaka.util.ManifestParserUtils; const presentationTimeline = context.presentationTimeline; // Capture values that could change as the parsing context moves on to // other parts of the manifest. const periodStart = context.periodInfo.start; const periodDuration = context.periodInfo.duration; const periodEnd = periodDuration ? periodStart + periodDuration : Infinity; const segmentDuration = info.segmentDuration; goog.asserts.assert( segmentDuration != null, 'Segment duration must not be null!'); const startNumber = info.startNumber; const timescale = info.timescale; const template = info.mediaTemplate; const bandwidth = context.bandwidth || null; const id = context.representation.id; const baseUris = context.representation.baseUris; const timestampOffset = periodStart - info.scaledPresentationTimeOffset; // Computes the range of presentation timestamps both within the period and // available. This is an intersection of the period range and the // availability window. const computeAvailablePeriodRange = () => { return [ Math.max( presentationTimeline.getSegmentAvailabilityStart(), periodStart), Math.min( presentationTimeline.getSegmentAvailabilityEnd(), periodEnd), ]; }; // Computes the range of absolute positions both within the period and // available. The range is inclusive. These are the positions for which we // will generate segment references. const computeAvailablePositionRange = () => { // In presentation timestamps. const availablePresentationTimes = computeAvailablePeriodRange(); goog.asserts.assert(availablePresentationTimes.every(isFinite), 'Available presentation times must be finite!'); goog.asserts.assert(availablePresentationTimes.every((x) => x >= 0), 'Available presentation times must be positive!'); goog.asserts.assert(segmentDuration != null, 'Segment duration must not be null!'); // In period-relative timestamps. const availablePeriodTimes = availablePresentationTimes.map((x) => x - periodStart); // These may sometimes be reversed ([1] <= [0]) if the period is // completely unavailable. The logic will still work if this happens, // because we will simply generate no references. // In period-relative positions (0-based). const availablePeriodPositions = [ Math.ceil(availablePeriodTimes[0] / segmentDuration), Math.ceil(availablePeriodTimes[1] / segmentDuration) - 1, ]; // In absolute positions. const availablePresentationPositions = availablePeriodPositions.map((x) => x + startNumber); return availablePresentationPositions; }; // For Live, we must limit the initial SegmentIndex in size, to avoid // consuming too much CPU or memory for content with gigantic // timeShiftBufferDepth (which can have values up to and including // Infinity). const range = computeAvailablePositionRange(); const minPosition = context.dynamic ? Math.max(range[0], range[1] - segmentLimit + 1) : range[0]; const maxPosition = range[1]; const references = []; const createReference = (position) => { // These inner variables are all scoped to the inner loop, and can be used // safely in the callback below. goog.asserts.assert(segmentDuration != null, 'Segment duration must not be null!'); // Relative to the period start. const positionWithinPeriod = position - startNumber; const segmentPeriodTime = positionWithinPeriod * segmentDuration; // What will appear in the actual segment files. The media timestamp is // what is expected in the $Time$ template. const segmentMediaTime = segmentPeriodTime + info.scaledPresentationTimeOffset; const getUris = () => { const mediaUri = MpdUtils.fillUriTemplate( template, id, position, bandwidth, segmentMediaTime * timescale); return ManifestParserUtils.resolveUris(baseUris, [mediaUri]); }; // Relative to the presentation. const segmentStart = segmentPeriodTime + periodStart; // Cap the segment end at the period end so that references from the // next period will fit neatly after it. const segmentEnd = Math.min(segmentStart + segmentDuration, periodEnd); // This condition will be true unless the segmentStart was >= periodEnd. // If we've done the position calculations correctly, this won't happen. goog.asserts.assert(segmentStart < segmentEnd, 'Generated a segment outside of the period!'); return new shaka.media.SegmentReference( segmentStart, segmentEnd, getUris, /* startByte= */ 0, /* endByte= */ null, initSegmentReference, timestampOffset, /* appendWindowStart= */ periodStart, /* appendWindowEnd= */ periodEnd); }; for (let position = minPosition; position <= maxPosition; ++position) { const reference = createReference(position); references.push(reference); } /** @type {shaka.media.SegmentIndex} */ const segmentIndex = new shaka.media.SegmentIndex(references); // If the availability timeline currently ends before the period, we will // need to add references over time. if (presentationTimeline.getSegmentAvailabilityEnd() < periodEnd) { // The period continues to get longer over time, so check for new // references once every |segmentDuration| seconds. // We clamp to |minPosition| in case the initial range was reversed and no // references were generated. Otherwise, the update would start creating // negative positions for segments in periods which begin in the future. let nextPosition = Math.max(minPosition, maxPosition + 1); segmentIndex.updateEvery(segmentDuration, () => { // Evict any references outside the window. segmentIndex.evict(presentationTimeline.getSegmentAvailabilityStart()); // Compute any new references that need to be added. const [_, maxPosition] = computeAvailablePositionRange(); const references = []; while (nextPosition <= maxPosition) { const reference = createReference(nextPosition); references.push(reference); nextPosition++; } if (presentationTimeline.getSegmentAvailabilityEnd() >= periodEnd && !references.length) { // Signal stop. return null; } return references; }); } return Promise.resolve(segmentIndex); } /** * Creates segment references from a timeline. * * @param {shaka.dash.DashParser.Context} context * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info * @param {shaka.media.InitSegmentReference} initSegmentReference * @return {!Array.<!shaka.media.SegmentReference>} * @private */ static createFromTimeline_(context, info, initSegmentReference) { const MpdUtils = shaka.dash.MpdUtils; const ManifestParserUtils = shaka.util.ManifestParserUtils; const periodStart = context.periodInfo.start; const periodDuration = context.periodInfo.duration; const timestampOffset = periodStart - info.scaledPresentationTimeOffset; const appendWindowStart = periodStart; const appendWindowEnd = periodDuration ? periodStart + periodDuration : Infinity; /** @type {!Array.<!shaka.media.SegmentReference>} */ const references = []; const enum_ = (it) => shaka.util.Iterables.enumerate(it); for (const {i, item: {start, unscaledStart, end}} of enum_(info.timeline)) { // 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.) const segmentReplacement = i + info.startNumber; // Consider the presentation time offset in segment uri computation const timeReplacement = unscaledStart + info.unscaledPresentationTimeOffset; const repId = context.representation.id; const bandwidth = context.bandwidth || null; const mediaTemplate = info.mediaTemplate; const baseUris = context.representation.baseUris; // This callback must not capture any non-local // variables, such as info, context, etc. Make // sure any values you reference here have // been assigned to local variables within the // loop, or else we will end up with a leak. const createUris = () => { goog.asserts.assert( mediaTemplate, 'There should be a media template with a timeline'); const mediaUri = MpdUtils.fillUriTemplate( mediaTemplate, repId, segmentReplacement, bandwidth || null, timeReplacement); return ManifestParserUtils .resolveUris(baseUris, [mediaUri]) .map((g) => { return g.toString(); }); }; references.push(new shaka.media.SegmentReference( periodStart + start, periodStart + end, createUris, /* startByte= */ 0, /* endByte= */ null, initSegmentReference, timestampOffset, appendWindowStart, appendWindowEnd)); } return references; } /** * Creates an init segment reference from a context object. * * @param {shaka.dash.DashParser.Context} context * @return {shaka.media.InitSegmentReference} * @private */ static createInitSegment_(context) { const MpdUtils = shaka.dash.MpdUtils; const ManifestParserUtils = shaka.util.ManifestParserUtils; const SegmentTemplate = shaka.dash.SegmentTemplate; const initialization = MpdUtils.inheritAttribute( context, SegmentTemplate.fromInheritance_, 'initialization'); if (!initialization) { return null; } const repId = context.representation.id; const bandwidth = context.bandwidth || null; const baseUris = context.representation.baseUris; const getUris = () => { goog.asserts.assert(initialization, 'Should have returned earler'); const filledTemplate = MpdUtils.fillUriTemplate( initialization, repId, null, bandwidth, null); const resolvedUris = ManifestParserUtils.resolveUris( baseUris, [filledTemplate]); return resolvedUris; }; return new shaka.media.InitSegmentReference(getUris, 0, null); } }; /** * @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;