/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.dash.SegmentBase');
goog.require('goog.asserts');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.log');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.Mp4SegmentIndexParser');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.WebmSegmentIndexParser');
goog.require('shaka.util.Error');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.ObjectUtils');
goog.require('shaka.util.XmlUtils');
goog.requireType('shaka.dash.DashParser');
goog.requireType('shaka.media.PresentationTimeline');
goog.requireType('shaka.media.SegmentReference');
/**
* @summary A set of functions for parsing SegmentBase elements.
*/
shaka.dash.SegmentBase = class {
/**
* Creates an init segment reference from a Context object.
*
* @param {shaka.dash.DashParser.Context} context
* @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
* @param {shaka.extern.aes128Key|undefined} aes128Key
* @return {shaka.media.InitSegmentReference}
*/
static createInitSegment(context, callback, aes128Key) {
const MpdUtils = shaka.dash.MpdUtils;
const XmlUtils = shaka.util.XmlUtils;
const ManifestParserUtils = shaka.util.ManifestParserUtils;
const initialization =
MpdUtils.inheritChild(context, callback, 'Initialization');
if (!initialization) {
return null;
}
let resolvedUris = context.representation.getBaseUris();
const uri = initialization.getAttribute('sourceURL');
if (uri) {
resolvedUris = ManifestParserUtils.resolveUris(resolvedUris, [uri]);
}
let startByte = 0;
let endByte = null;
const range =
XmlUtils.parseAttr(initialization, 'range', XmlUtils.parseRange);
if (range) {
startByte = range.start;
endByte = range.end;
}
const getUris = () => resolvedUris;
const qualityInfo = shaka.dash.SegmentBase.createQualityInfo(context);
return new shaka.media.InitSegmentReference(
getUris,
startByte,
endByte,
qualityInfo,
/* timescale= */ null,
/* segmentData= */ null,
aes128Key);
}
/**
* Creates a new StreamInfo object.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.DashParser.RequestSegmentCallback} requestSegment
* @param {shaka.extern.aes128Key|undefined} aes128Key
* @return {shaka.dash.DashParser.StreamInfo}
*/
static createStreamInfo(context, requestSegment, aes128Key) {
goog.asserts.assert(context.representation.segmentBase,
'Should only be called with SegmentBase');
// Since SegmentBase does not need updates, simply treat any call as
// the initial parse.
const MpdUtils = shaka.dash.MpdUtils;
const SegmentBase = shaka.dash.SegmentBase;
const XmlUtils = shaka.util.XmlUtils;
const unscaledPresentationTimeOffset = Number(MpdUtils.inheritAttribute(
context, SegmentBase.fromInheritance_, 'presentationTimeOffset')) || 0;
const timescaleStr = MpdUtils.inheritAttribute(
context, SegmentBase.fromInheritance_, 'timescale');
let timescale = 1;
if (timescaleStr) {
timescale = XmlUtils.parsePositiveInt(timescaleStr) || 1;
}
const scaledPresentationTimeOffset =
(unscaledPresentationTimeOffset / timescale) || 0;
const initSegmentReference = SegmentBase.createInitSegment(
context, SegmentBase.fromInheritance_, aes128Key);
// Throws an immediate error if the format is unsupported.
SegmentBase.checkSegmentIndexRangeSupport_(context, initSegmentReference);
// 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);
return {
generateSegmentIndex: () => {
return SegmentBase.generateSegmentIndex_(
shallowCopyOfContext, requestSegment, initSegmentReference,
scaledPresentationTimeOffset);
},
};
}
/**
* Creates a SegmentIndex for the given URIs and context.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.DashParser.RequestSegmentCallback} requestSegment
* @param {shaka.media.InitSegmentReference} initSegmentReference
* @param {!Array.<string>} uris
* @param {number} startByte
* @param {?number} endByte
* @param {number} scaledPresentationTimeOffset
* @return {!Promise.<shaka.media.SegmentIndex>}
*/
static async generateSegmentIndexFromUris(
context, requestSegment, initSegmentReference, uris, startByte,
endByte, scaledPresentationTimeOffset) {
// Unpack context right away, before we start an async process.
// This immunizes us against changes to the context object later.
/** @type {shaka.media.PresentationTimeline} */
const presentationTimeline = context.presentationTimeline;
const fitLast = !context.dynamic || !context.periodInfo.isLastPeriod;
const periodStart = context.periodInfo.start;
const periodDuration = context.periodInfo.duration;
const containerType = context.representation.mimeType.split('/')[1];
// Create a local variable to bind to so we can set to null to help the GC.
let localRequest = requestSegment;
let segmentIndex = null;
const responses = [
localRequest(uris, startByte, endByte, /* isInit= */ false),
containerType == 'webm' ?
localRequest(
initSegmentReference.getUris(),
initSegmentReference.startByte,
initSegmentReference.endByte,
/* isInit= */ true) :
null,
];
localRequest = null;
const results = await Promise.all(responses);
const indexData = results[0];
const initData = results[1] || null;
/** @type {Array.<!shaka.media.SegmentReference>} */
let references = null;
const timestampOffset = periodStart - scaledPresentationTimeOffset;
const appendWindowStart = periodStart;
const appendWindowEnd = periodDuration ?
periodStart + periodDuration : Infinity;
if (containerType == 'mp4') {
references = shaka.media.Mp4SegmentIndexParser.parse(
indexData, startByte, uris, initSegmentReference, timestampOffset,
appendWindowStart, appendWindowEnd);
} else {
goog.asserts.assert(initData, 'WebM requires init data');
references = shaka.media.WebmSegmentIndexParser.parse(
indexData, initData, uris, initSegmentReference, timestampOffset,
appendWindowStart, appendWindowEnd);
}
presentationTimeline.notifySegments(references);
// Since containers are never updated, we don't need to store the
// segmentIndex in the map.
goog.asserts.assert(!segmentIndex,
'Should not call generateSegmentIndex twice');
segmentIndex = new shaka.media.SegmentIndex(references);
if (fitLast) {
segmentIndex.fit(appendWindowStart, appendWindowEnd, /* isNew= */ true);
}
return segmentIndex;
}
/**
* @param {?shaka.dash.DashParser.InheritanceFrame} frame
* @return {Element}
* @private
*/
static fromInheritance_(frame) {
return frame.segmentBase;
}
/**
* Compute the byte range of the segment index from the container.
*
* @param {shaka.dash.DashParser.Context} context
* @return {?{start: number, end: number}}
* @private
*/
static computeIndexRange_(context) {
const MpdUtils = shaka.dash.MpdUtils;
const SegmentBase = shaka.dash.SegmentBase;
const XmlUtils = shaka.util.XmlUtils;
const representationIndex = MpdUtils.inheritChild(
context, SegmentBase.fromInheritance_, 'RepresentationIndex');
const indexRangeElem = MpdUtils.inheritAttribute(
context, SegmentBase.fromInheritance_, 'indexRange');
let indexRange = XmlUtils.parseRange(indexRangeElem || '');
if (representationIndex) {
indexRange = XmlUtils.parseAttr(
representationIndex, 'range', XmlUtils.parseRange, indexRange);
}
return indexRange;
}
/**
* Compute the URIs of the segment index from the container.
*
* @param {shaka.dash.DashParser.Context} context
* @return {!Array.<string>}
* @private
*/
static computeIndexUris_(context) {
const ManifestParserUtils = shaka.util.ManifestParserUtils;
const MpdUtils = shaka.dash.MpdUtils;
const SegmentBase = shaka.dash.SegmentBase;
const representationIndex = MpdUtils.inheritChild(
context, SegmentBase.fromInheritance_, 'RepresentationIndex');
let indexUris = context.representation.getBaseUris();
if (representationIndex) {
const representationUri = representationIndex.getAttribute('sourceURL');
if (representationUri) {
indexUris = ManifestParserUtils.resolveUris(
indexUris, [representationUri]);
}
}
return indexUris;
}
/**
* Check if this type of segment index is supported. This allows for
* immediate errors during parsing, as opposed to an async error from
* createSegmentIndex().
*
* Also checks for a valid byte range, which is not required for callers from
* SegmentTemplate.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.media.InitSegmentReference} initSegmentReference
* @private
*/
static checkSegmentIndexRangeSupport_(context, initSegmentReference) {
const SegmentBase = shaka.dash.SegmentBase;
SegmentBase.checkSegmentIndexSupport(context, initSegmentReference);
const indexRange = SegmentBase.computeIndexRange_(context);
if (!indexRange) {
shaka.log.error(
'SegmentBase does not contain sufficient segment information:',
'the SegmentBase does not contain @indexRange',
'or a RepresentationIndex element.',
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);
}
}
/**
* Check if this type of segment index is supported. This allows for
* immediate errors during parsing, as opposed to an async error from
* createSegmentIndex().
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.media.InitSegmentReference} initSegmentReference
*/
static checkSegmentIndexSupport(context, initSegmentReference) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const contentType = context.representation.contentType;
const containerType = context.representation.mimeType.split('/')[1];
if (contentType != ContentType.TEXT && containerType != 'mp4' &&
containerType != 'webm') {
shaka.log.error(
'SegmentBase 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') && !initSegmentReference) {
shaka.log.error(
'SegmentBase does not contain sufficient segment information:',
'the SegmentBase uses a WebM container,',
'but does not contain an Initialization element.',
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);
}
}
/**
* Generate a SegmentIndex from a Context object.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.DashParser.RequestSegmentCallback} requestSegment
* @param {shaka.media.InitSegmentReference} initSegmentReference
* @param {number} scaledPresentationTimeOffset
* @return {!Promise.<shaka.media.SegmentIndex>}
* @private
*/
static generateSegmentIndex_(
context, requestSegment, initSegmentReference,
scaledPresentationTimeOffset) {
const SegmentBase = shaka.dash.SegmentBase;
const indexUris = SegmentBase.computeIndexUris_(context);
const indexRange = SegmentBase.computeIndexRange_(context);
goog.asserts.assert(indexRange, 'Index range should not be null!');
return shaka.dash.SegmentBase.generateSegmentIndexFromUris(
context, requestSegment, initSegmentReference, indexUris,
indexRange.start, indexRange.end,
scaledPresentationTimeOffset);
}
/**
* Create a MediaQualityInfo object from a Context object.
*
* @param {!shaka.dash.DashParser.Context} context
* @return {!shaka.extern.MediaQualityInfo}
*/
static createQualityInfo(context) {
const representation = context.representation;
return {
bandwidth: context.bandwidth,
audioSamplingRate: representation.audioSamplingRate,
codecs: representation.codecs,
contentType: representation.contentType,
frameRate: representation.frameRate || null,
height: representation.height || null,
mimeType: representation.mimeType,
channelsCount: representation.numChannels,
pixelAspectRatio: representation.pixelAspectRatio || null,
width: representation.width || null,
};
}
};