Files
shaka-player/lib/media/segment_reference.js
T
theodab 24e32559bf feat(DASH): Handle mixed-codec variants. (#5950)
With the addition of the changeType API for MediaSource, it is theoretically possible for a variant to change between multiple codecs for a given buffer, over the course of playback.
This adds support for the DASH player to stitch together periods which have such multi-codec variants, but only as a last resort. For example, if one period only has audio in aac, and another period only has opus audio, the player will now stitch those periods together as one, but if there is a throughline that does not involve changing codecs it will go for that instead.

Closes #5961
2023-12-01 00:37:32 -08:00

642 lines
16 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.media.InitSegmentReference');
goog.provide('shaka.media.SegmentReference');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.ArrayUtils');
goog.require('shaka.util.BufferUtils');
/**
* Creates an InitSegmentReference, which provides the location to an
* initialization segment.
*
* @export
*/
shaka.media.InitSegmentReference = class {
/**
* @param {function():!Array.<string>} uris A function that creates the URIs
* of the resource containing the segment.
* @param {number} startByte The offset from the start of the resource to the
* start of the segment.
* @param {?number} endByte The offset from the start of the resource
* to the end of the segment, inclusive. A value of null indicates that the
* segment extends to the end of the resource.
* @param {null|shaka.extern.MediaQualityInfo=} mediaQuality Information about
* the quality of the media associated with this init segment.
* @param {(null|number)=} timescale
* @param {(null|BufferSource)=} segmentData
* @param {?shaka.extern.aes128Key=} aes128Key
* The segment's AES-128-CBC full segment encryption key and iv.
*/
constructor(uris, startByte, endByte, mediaQuality = null, timescale = null,
segmentData = null, aes128Key = null) {
/** @type {function():!Array.<string>} */
this.getUris = uris;
/** @const {number} */
this.startByte = startByte;
/** @const {?number} */
this.endByte = endByte;
/** @const {shaka.extern.MediaQualityInfo|null} */
this.mediaQuality = mediaQuality;
/** @type {number|null} */
this.timescale = timescale;
/** @type {BufferSource|null} */
this.segmentData = segmentData;
/** @type {?shaka.extern.aes128Key} */
this.aes128Key = aes128Key;
/** @type {?string} */
this.codecs = null;
/** @type {?string} */
this.mimeType = null;
}
/**
* Returns the offset from the start of the resource to the
* start of the segment.
*
* @return {number}
* @export
*/
getStartByte() {
return this.startByte;
}
/**
* Returns the offset from the start of the resource to the end of the
* segment, inclusive. A value of null indicates that the segment extends
* to the end of the resource.
*
* @return {?number}
* @export
*/
getEndByte() {
return this.endByte;
}
/**
* Returns the size of the init segment.
* @return {?number}
*/
getSize() {
if (this.endByte) {
return this.endByte - this.startByte;
} else {
return null;
}
}
/**
* Returns media quality information for the segments associated with
* this init segment.
*
* @return {?shaka.extern.MediaQualityInfo}
*/
getMediaQuality() {
return this.mediaQuality;
}
/**
* Return the segment data.
*
* @return {?BufferSource}
*/
getSegmentData() {
return this.segmentData;
}
/**
* Check if two initSegmentReference have all the same values.
* @param {?shaka.media.InitSegmentReference} reference1
* @param {?shaka.media.InitSegmentReference} reference2
* @return {boolean}
*/
static equal(reference1, reference2) {
const ArrayUtils = shaka.util.ArrayUtils;
const BufferUtils = shaka.util.BufferUtils;
if (!reference1 || !reference2) {
return reference1 == reference2;
} else {
return reference1.getStartByte() == reference2.getStartByte() &&
reference1.getEndByte() == reference2.getEndByte() &&
ArrayUtils.equal(
reference1.getUris().sort(), reference2.getUris().sort()) &&
BufferUtils.equal(reference1.getSegmentData(),
reference2.getSegmentData());
}
}
};
/**
* SegmentReference provides the start time, end time, and location to a media
* segment.
*
* @export
*/
shaka.media.SegmentReference = class {
/**
* @param {number} startTime The segment's start time in seconds.
* @param {number} endTime The segment's end time in seconds. The segment
* ends the instant before this time, so |endTime| must be strictly greater
* than |startTime|.
* @param {function():!Array.<string>} uris
* A function that creates the URIs of the resource containing the segment.
* @param {number} startByte The offset from the start of the resource to the
* start of the segment.
* @param {?number} endByte The offset from the start of the resource to the
* end of the segment, inclusive. A value of null indicates that the
* segment extends to the end of the resource.
* @param {shaka.media.InitSegmentReference} initSegmentReference
* The segment's initialization segment metadata, or null if the segments
* are self-initializing.
* @param {number} timestampOffset
* The amount of time, in seconds, that must be added to the segment's
* internal timestamps to align it to the presentation timeline.
* <br>
* For DASH, this value should equal the Period start time minus the first
* presentation timestamp of the first frame/sample in the Period. For
* example, for MP4 based streams, this value should equal Period start
* minus the first segment's tfdt box's 'baseMediaDecodeTime' field (after
* it has been converted to seconds).
* <br>
* For HLS, this value should be the start time of the most recent
* discontinuity, or 0 if there is no preceding discontinuity. Only used
* in segments mode.
* @param {number} appendWindowStart
* The start of the append window for this reference, relative to the
* presentation. Any content from before this time will be removed by
* MediaSource.
* @param {number} appendWindowEnd
* The end of the append window for this reference, relative to the
* presentation. Any content from after this time will be removed by
* MediaSource.
* @param {!Array.<!shaka.media.SegmentReference>=} partialReferences
* A list of SegmentReferences for the partial segments.
* @param {?string=} tilesLayout
* The value is a grid-item-dimension consisting of two positive decimal
* integers in the format: column-x-row ('4x3'). It describes the
* arrangement of Images in a Grid. The minimum valid LAYOUT is '1x1'.
* @param {?number=} tileDuration
* The explicit duration of an individual tile within the tiles grid.
* If not provided, the duration should be automatically calculated based on
* the duration of the reference.
* @param {?number=} syncTime
* A time value, expressed in seconds since 1970, which is used to
* synchronize between streams. Both produced and consumed by the HLS
* parser. Other components should not need this value.
* @param {shaka.media.SegmentReference.Status=} status
* The segment status is used to indicate that a segment does not exist or is
* not available.
* @param {?shaka.extern.aes128Key=} aes128Key
* The segment's AES-128-CBC full segment encryption key and iv.
* @param {boolean=} allPartialSegments
* Indicate if the segment has all partial segments
*/
constructor(
startTime, endTime, uris, startByte, endByte, initSegmentReference,
timestampOffset, appendWindowStart, appendWindowEnd,
partialReferences = [], tilesLayout = '', tileDuration = null,
syncTime = null, status = shaka.media.SegmentReference.Status.AVAILABLE,
aes128Key = null, allPartialSegments = false) {
// A preload hinted Partial Segment has the same startTime and endTime.
goog.asserts.assert(startTime <= endTime,
'startTime must be less than or equal to endTime');
goog.asserts.assert((endByte == null) || (startByte < endByte),
'startByte must be < endByte');
/** @type {number} */
this.startTime = startTime;
/** @type {number} */
this.endTime = endTime;
/**
* The "true" end time of the segment, without considering the period end
* time. This is necessary for thumbnail segments, where timing requires us
* to know the original segment duration as described in the manifest.
* @type {number}
*/
this.trueEndTime = endTime;
/** @type {function():!Array.<string>} */
this.getUrisInner = uris;
/** @const {number} */
this.startByte = startByte;
/** @const {?number} */
this.endByte = endByte;
/** @type {shaka.media.InitSegmentReference} */
this.initSegmentReference = initSegmentReference;
/** @type {number} */
this.timestampOffset = timestampOffset;
/** @type {number} */
this.appendWindowStart = appendWindowStart;
/** @type {number} */
this.appendWindowEnd = appendWindowEnd;
/** @type {!Array.<!shaka.media.SegmentReference>} */
this.partialReferences = partialReferences;
/** @type {?string} */
this.tilesLayout = tilesLayout;
/** @type {?number} */
this.tileDuration = tileDuration;
/**
* A time value, expressed in seconds since 1970, which is used to
* synchronize between streams. Both produced and consumed by the HLS
* parser. Other components should not need this value.
*
* @type {?number}
*/
this.syncTime = syncTime;
/** @type {shaka.media.SegmentReference.Status} */
this.status = status;
/** @type {boolean} */
this.preload = false;
/** @type {boolean} */
this.independent = true;
/** @type {boolean} */
this.byterangeOptimization = false;
/** @type {?shaka.extern.aes128Key} */
this.aes128Key = aes128Key;
/** @type {?shaka.media.SegmentReference.ThumbnailSprite} */
this.thumbnailSprite = null;
/** @type {number} */
this.discontinuitySequence = 0;
/** @type {boolean} */
this.allPartialSegments = allPartialSegments;
/** @type {boolean} */
this.partial = false;
/** @type {boolean} */
this.lastPartial = false;
for (const partial of this.partialReferences) {
partial.markAsPartial();
}
if (this.allPartialSegments && this.partialReferences.length) {
const lastPartial =
this.partialReferences[this.partialReferences.length - 1];
lastPartial.markAsLastPartial();
}
/** @type {?string} */
this.codecs = null;
/** @type {?string} */
this.mimeType = null;
}
/**
* Creates and returns the URIs of the resource containing the segment.
*
* @return {!Array.<string>}
* @export
*/
getUris() {
return this.getUrisInner();
}
/**
* Returns the segment's start time in seconds.
*
* @return {number}
* @export
*/
getStartTime() {
return this.startTime;
}
/**
* Returns the segment's end time in seconds.
*
* @return {number}
* @export
*/
getEndTime() {
return this.endTime;
}
/**
* Returns the offset from the start of the resource to the
* start of the segment.
*
* @return {number}
* @export
*/
getStartByte() {
return this.startByte;
}
/**
* Returns the offset from the start of the resource to the end of the
* segment, inclusive. A value of null indicates that the segment extends to
* the end of the resource.
*
* @return {?number}
* @export
*/
getEndByte() {
return this.endByte;
}
/**
* Returns the size of the segment.
* @return {?number}
*/
getSize() {
if (this.endByte) {
return this.endByte - this.startByte;
} else {
return null;
}
}
/**
* Returns true if it contains partial SegmentReferences.
* @return {boolean}
*/
hasPartialSegments() {
return this.partialReferences.length > 0;
}
/**
* Returns true if it contains all partial SegmentReferences.
* @return {boolean}
*/
hasAllPartialSegments() {
return this.allPartialSegments;
}
/**
* Returns the segment's tiles layout. Only defined in image segments.
*
* @return {?string}
* @export
*/
getTilesLayout() {
return this.tilesLayout;
}
/**
* Returns the segment's explicit tile duration.
* Only defined in image segments.
*
* @return {?number}
* @export
*/
getTileDuration() {
return this.tileDuration;
}
/**
* Returns the segment's status.
*
* @return {shaka.media.SegmentReference.Status}
* @export
*/
getStatus() {
return this.status;
}
/**
* Mark the reference as unavailable.
*
* @export
*/
markAsUnavailable() {
this.status = shaka.media.SegmentReference.Status.UNAVAILABLE;
}
/**
* Mark the reference as preload.
*
* @export
*/
markAsPreload() {
this.preload = true;
}
/**
* Returns true if the segment is preloaded.
*
* @return {boolean}
* @export
*/
isPreload() {
return this.preload;
}
/**
* Mark the reference as non-independent.
*
* @export
*/
markAsNonIndependent() {
this.independent = false;
}
/**
* Returns true if the segment is independent.
*
* @return {boolean}
* @export
*/
isIndependent() {
return this.independent;
}
/**
* Mark the reference as partial.
*
* @export
*/
markAsPartial() {
this.partial = true;
}
/**
* Returns true if the segment is partial.
*
* @return {boolean}
* @export
*/
isPartial() {
return this.partial;
}
/**
* Mark the reference as being the last part of the full segment
*
* @export
*/
markAsLastPartial() {
this.lastPartial = true;
}
/**
* Returns true if reference as being the last part of the full segment.
*
* @return {boolean}
* @export
*/
isLastPartial() {
return this.lastPartial;
}
/**
* Mark the reference as byterange optimization.
*
* The "byterange optimization" means that it is playable using MP4 low
* latency streaming with chunked data.
*
* @export
*/
markAsByterangeOptimization() {
this.byterangeOptimization = true;
}
/**
* Returns true if the segment has a byterange optimization.
*
* @return {boolean}
* @export
*/
hasByterangeOptimization() {
return this.byterangeOptimization;
}
/**
* Set the segment's thumbnail sprite.
*
* @param {shaka.media.SegmentReference.ThumbnailSprite} thumbnailSprite
* @export
*/
setThumbnailSprite(thumbnailSprite) {
this.thumbnailSprite = thumbnailSprite;
}
/**
* Returns the segment's thumbnail sprite.
*
* @return {?shaka.media.SegmentReference.ThumbnailSprite}
* @export
*/
getThumbnailSprite() {
return this.thumbnailSprite;
}
/**
* Offset the segment reference by a fixed amount.
*
* @param {number} offset The amount to add to the segment's start and end
* times.
* @export
*/
offset(offset) {
this.startTime += offset;
this.endTime += offset;
this.trueEndTime += offset;
for (const partial of this.partialReferences) {
partial.startTime += offset;
partial.endTime += offset;
partial.trueEndTime += offset;
}
}
/**
* Sync this segment against a particular sync time that will serve as "0" in
* the presentation timeline.
*
* @param {number} lowestSyncTime
* @export
*/
syncAgainst(lowestSyncTime) {
if (this.syncTime == null) {
shaka.log.alwaysError('Sync attempted without sync time!');
return;
}
const desiredStart = this.syncTime - lowestSyncTime;
const offset = desiredStart - this.startTime;
if (Math.abs(offset) >= 0.001) {
this.offset(offset);
}
}
};
/**
* Rather than using booleans to communicate what the state of the reference,
* we have this enum.
*
* @enum {number}
* @export
*/
shaka.media.SegmentReference.Status = {
AVAILABLE: 0,
UNAVAILABLE: 1,
MISSING: 2,
};
/**
* A convenient typedef for when either type of reference is acceptable.
*
* @typedef {shaka.media.InitSegmentReference|shaka.media.SegmentReference}
*/
shaka.media.AnySegmentReference;
/**
* @typedef {{
* height: number,
* positionX: number,
* positionY: number,
* width: number
* }}
*
* @property {number} height
* The thumbnail height in px.
* @property {number} positionX
* The thumbnail left position in px.
* @property {number} positionY
* The thumbnail top position in px.
* @property {number} width
* The thumbnail width in px.
* @export
*/
shaka.media.SegmentReference.ThumbnailSprite;