mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-17 16:26:39 +03:00
486123ce41
When Representations have different BaseURLs, segment URLs generated for SegmentTimeline were always using the BaseURL of the last Representation because the wrong parsing context was being used. This fixes the issue and adds a regression test. This only affected v3.0.0. Closes #2650 Change-Id: I04df950b5d3205e102f9c74f52ece9773ce92282
562 lines
20 KiB
JavaScript
562 lines
20 KiB
JavaScript
/*! @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');
|
|
|
|
|
|
/**
|
|
* @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 valutes 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;
|
|
};
|
|
|
|
// 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 = Math.max(range[0], range[1] - segmentLimit);
|
|
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.
|
|
let nextPosition = 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++;
|
|
}
|
|
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 createUris =
|
|
() => {
|
|
goog.asserts.assert(
|
|
info.mediaTemplate,
|
|
'There should be a media template with a timeline');
|
|
const mediaUri = MpdUtils.fillUriTemplate(
|
|
info.mediaTemplate, repId,
|
|
segmentReplacement, bandwidth || null, timeReplacement);
|
|
return ManifestParserUtils
|
|
.resolveUris(context.representation.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;
|