Files
shaka-player/lib/media/time_ranges_utils.js
T
Gary Katsevman d6fcf66f5e fix: ignore buffered content less than 1e-4s (#6802)
On Firefox, in some cases after a period ends, seeking shortly after
will cause playback to fail or stall.
This occurs when the next period has small gaps and we're seeking to
after the gap. Seeking to before the gap succeeds. Even though the seek
requests the soure buffers to be fully cleared, Firefox actually keeps
around less than 1e-4s of content and won't let us forcibly remove this
content. Trying to call flush causes in infinite loop.
This leftover content makes shaka think that the buffer end in where we
used to be even though the presentation time reflects where we seeked
to. This means that playback doesn't continue. The buffer contitues
getting filled and playback will either fail when the SourcBuffer is
filled and triggers a QuotaExceededError or contiue when the buffer will
reach the presentationTime.
2024-06-12 12:06:36 -07:00

213 lines
5.4 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.media.TimeRangesUtils');
/**
* @summary A set of utility functions for dealing with TimeRanges objects.
*/
shaka.media.TimeRangesUtils = class {
/**
* Returns whether the buffer is small enough to be ignored.
*
* @param {TimeRanges} b
* @return {boolean}
* @private
*/
static isBufferNegligible_(b) {
// Workaround Safari bug: https://bit.ly/2trx6O8
// Firefox may leave <1e-4s of data in buffer after clearing all content
return b.length == 1 && b.end(0) - b.start(0) < 1e-4;
}
/**
* Gets the first timestamp in the buffer.
*
* @param {TimeRanges} b
* @return {?number} The first buffered timestamp, in seconds, if |buffered|
* is non-empty; otherwise, return null.
*/
static bufferStart(b) {
if (!b) {
return null;
}
if (shaka.media.TimeRangesUtils.isBufferNegligible_(b)) {
return null;
}
// Workaround Edge bug: https://bit.ly/2JYLPeB
if (b.length == 1 && b.start(0) < 0) {
return 0;
}
return b.length ? b.start(0) : null;
}
/**
* Gets the last timestamp in the buffer.
*
* @param {TimeRanges} b
* @return {?number} The last buffered timestamp, in seconds, if |buffered|
* is non-empty; otherwise, return null.
*/
static bufferEnd(b) {
if (!b) {
return null;
}
if (shaka.media.TimeRangesUtils.isBufferNegligible_(b)) {
return null;
}
return b.length ? b.end(b.length - 1) : null;
}
/**
* Determines if the given time is inside a buffered range.
*
* @param {TimeRanges} b
* @param {number} time Playhead time
* @return {boolean}
*/
static isBuffered(b, time) {
if (!b || !b.length) {
return false;
}
if (shaka.media.TimeRangesUtils.isBufferNegligible_(b)) {
return false;
}
if (time > b.end(b.length - 1)) {
return false;
}
return time >= b.start(0);
}
/**
* Computes how far ahead of the given timestamp is buffered. To provide
* smooth playback while jumping gaps, we don't include the gaps when
* calculating this.
* This only includes the amount of content that is buffered.
*
* @param {TimeRanges} b
* @param {number} time
* @return {number} The number of seconds buffered, in seconds, ahead of the
* given time.
*/
static bufferedAheadOf(b, time) {
if (!b || !b.length) {
return 0;
}
if (shaka.media.TimeRangesUtils.isBufferNegligible_(b)) {
return 0;
}
// We calculate the buffered amount by ONLY accounting for the content
// buffered (i.e. we ignore the times of the gaps). We also buffer through
// all gaps.
// Therefore, we start at the end and add up all buffers until |time|.
let result = 0;
for (const {start, end} of shaka.media.TimeRangesUtils.getBufferedInfo(b)) {
if (end > time) {
result += end - Math.max(start, time);
}
}
return result;
}
/**
* Determines if the given time is inside a gap between buffered ranges. If
* it is, this returns the index of the buffer that is *ahead* of the gap.
*
* @param {TimeRanges} b
* @param {number} time
* @param {number} threshold
* @return {?number} The index of the buffer after the gap, or null if not in
* a gap.
*/
static getGapIndex(b, time, threshold) {
const TimeRangesUtils = shaka.media.TimeRangesUtils;
if (!b || !b.length) {
return null;
}
if (shaka.media.TimeRangesUtils.isBufferNegligible_(b)) {
return null;
}
const idx = TimeRangesUtils.getBufferedInfo(b).findIndex((item, i, arr) => {
return item.start > time &&
(i == 0 || arr[i - 1].end - time <= threshold);
});
return idx >= 0 ? idx : null;
}
/**
* @param {TimeRanges} b
* @return {!Array.<shaka.extern.BufferedRange>}
*/
static getBufferedInfo(b) {
if (!b) {
return [];
}
const ret = [];
for (let i = 0; i < b.length; i++) {
ret.push({start: b.start(i), end: b.end(i)});
}
return ret;
}
/**
* This operation can be potentially EXPENSIVE and should only be done in
* debug builds for debugging purposes.
*
* @param {TimeRanges} oldRanges
* @param {TimeRanges} newRanges
* @return {?shaka.extern.BufferedRange} The last added range,
* chronologically by presentation time.
*/
static computeAddedRange(oldRanges, newRanges) {
const TimeRangesUtils = shaka.media.TimeRangesUtils;
if (!oldRanges || !oldRanges.length) {
return null;
}
if (!newRanges || !newRanges.length) {
return TimeRangesUtils.getBufferedInfo(newRanges).pop();
}
const newRangesReversed =
TimeRangesUtils.getBufferedInfo(newRanges).reverse();
const oldRangesReversed =
TimeRangesUtils.getBufferedInfo(oldRanges).reverse();
for (const newRange of newRangesReversed) {
let foundOverlap = false;
for (const oldRange of oldRangesReversed) {
if (oldRange.end >= newRange.start && oldRange.end <= newRange.end) {
foundOverlap = true;
// If the new range goes beyond the corresponding old one, the
// difference is newly-added.
if (newRange.end > oldRange.end) {
return {start: oldRange.end, end: newRange.end};
}
}
}
if (!foundOverlap) {
return newRange;
}
}
return null;
}
};