rx-player
Version:
Canal+ HTML5 Video Player
271 lines (270 loc) • 12.2 kB
JavaScript
/**
* Copyright 2015 CANAL+ Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { onSourceOpen, onSourceEnded, onSourceClose } from "../../compat/event_listeners";
import hasIssuesWithHighMediaSourceDuration from "../../compat/has_issues_with_high_media_source_duration";
import log from "../../log";
import SharedReference from "../../utils/reference";
import TaskCanceller from "../../utils/task_canceller";
/** Number of seconds in a regular year. */
const YEAR_IN_SECONDS = 365 * 24 * 3600;
/**
* Keep the MediaSource's `duration` attribute up-to-date with the duration of
* the content played on it.
* @class MediaSourceDurationUpdater
*/
export default class MediaSourceDurationUpdater {
/**
* Create a new `MediaSourceDurationUpdater`,
* @param {MediaSource} mediaSource - The MediaSource on which the content is
* played.
*/
constructor(mediaSource) {
this._mediaSource = mediaSource;
this._currentMediaSourceDurationUpdateCanceller = null;
}
/**
* Indicate to the `MediaSourceDurationUpdater` the currently known duration
* of the content.
*
* The `MediaSourceDurationUpdater` will then use that value to determine
* which `duration` attribute should be set on the `MediaSource` associated
*
* @param {number} newDuration
* @param {boolean} isRealEndKnown - If set to `false`, the current content is
* a dynamic content (it might evolve in the future) and the `newDuration`
* communicated might be greater still. In effect the
* `MediaSourceDurationUpdater` will actually set a much higher value to the
* `MediaSource`'s duration to prevent being annoyed by the HTML-related
* side-effects of having a too low duration (such as the impossibility to
* seek over that value).
*/
updateDuration(newDuration, isRealEndKnown) {
if (this._currentMediaSourceDurationUpdateCanceller !== null) {
this._currentMediaSourceDurationUpdateCanceller.cancel("manual duration update");
}
this._currentMediaSourceDurationUpdateCanceller = new TaskCanceller("MediaSource Duration Update");
const mediaSource = this._mediaSource;
const currentSignal = this._currentMediaSourceDurationUpdateCanceller.signal;
const isMediaSourceOpened = createMediaSourceOpenReference(mediaSource, currentSignal);
/** TaskCanceller triggered each time the MediaSource switches to and from "open". */
let msOpenStatusCanceller = new TaskCanceller(undefined /* we do not care for logs here */);
msOpenStatusCanceller.linkToSignal(currentSignal);
isMediaSourceOpened.onUpdate(onMediaSourceOpenedStatusChanged, {
emitCurrentValue: true,
clearSignal: currentSignal,
});
function onMediaSourceOpenedStatusChanged() {
msOpenStatusCanceller.cancel("MediaSource open status changed");
if (!isMediaSourceOpened.getValue()) {
return;
}
msOpenStatusCanceller = new TaskCanceller(undefined /* we do not care for logs here */);
msOpenStatusCanceller.linkToSignal(currentSignal);
const areSourceBuffersUpdating = createSourceBuffersUpdatingReference(mediaSource.sourceBuffers, msOpenStatusCanceller.signal);
/** TaskCanceller triggered each time SourceBuffers' updating status changes */
let sourceBuffersUpdatingCanceller = new TaskCanceller(undefined);
sourceBuffersUpdatingCanceller.linkToSignal(msOpenStatusCanceller.signal);
return areSourceBuffersUpdating.onUpdate((areUpdating) => {
sourceBuffersUpdatingCanceller.cancel("SourceBuffer status update");
sourceBuffersUpdatingCanceller = new TaskCanceller(undefined);
sourceBuffersUpdatingCanceller.linkToSignal(msOpenStatusCanceller.signal);
if (areUpdating) {
return;
}
recursivelyForceDurationUpdate(mediaSource, newDuration, isRealEndKnown, sourceBuffersUpdatingCanceller.signal);
}, { clearSignal: msOpenStatusCanceller.signal, emitCurrentValue: true });
}
}
/**
* Abort the last duration-setting operation and free its resources.
* @param {string | undefined} reason - Human-inspectable reason behind the
* stop. Used for debugging matters, especially for debug log
* inspection.
*/
stopUpdating(reason) {
if (this._currentMediaSourceDurationUpdateCanceller !== null) {
this._currentMediaSourceDurationUpdateCanceller.cancel(reason !== null && reason !== void 0 ? reason : "stop MSDU");
this._currentMediaSourceDurationUpdateCanceller = null;
}
}
}
/**
* Checks that duration can be updated on the MediaSource, and then
* sets it.
*
* Returns either:
* - the new duration it has been updated to if it has
* - `null` if it hasn'nt been updated
*
* @param {MediaSource} mediaSource
* @param {number} duration
* @param {boolean} isRealEndKnown
* @returns {string}
*/
function setMediaSourceDuration(mediaSource, duration, isRealEndKnown) {
let newDuration = duration;
if (!isRealEndKnown) {
newDuration = hasIssuesWithHighMediaSourceDuration()
? Infinity
: getMaximumLiveSeekablePosition(duration);
}
let maxBufferedEnd = 0;
for (let i = 0; i < mediaSource.sourceBuffers.length; i++) {
const sourceBuffer = mediaSource.sourceBuffers[i];
const sbBufferedLen = sourceBuffer.buffered.length;
if (sbBufferedLen > 0) {
maxBufferedEnd = Math.max(maxBufferedEnd, sourceBuffer.buffered.end(sbBufferedLen - 1));
}
}
if (newDuration === mediaSource.duration) {
return "success" /* MediaSourceDurationUpdateStatus.Success */;
}
else if (maxBufferedEnd > newDuration) {
// We already buffered further than the duration we want to set.
// Keep the duration that was set at that time as a security.
if (maxBufferedEnd < mediaSource.duration) {
try {
log.info("mse", "Updating duration to what is currently buffered", {
maxBufferedEnd,
});
mediaSource.duration = maxBufferedEnd;
}
catch (err) {
log.warn("mse", "Can't update duration on the MediaSource.", err instanceof Error ? err : "");
return "failed" /* MediaSourceDurationUpdateStatus.Failed */;
}
}
return "partial" /* MediaSourceDurationUpdateStatus.Partial */;
}
else {
const oldDuration = mediaSource.duration;
try {
log.info("mse", "Updating duration", { newDuration });
mediaSource.duration = newDuration;
if (mediaSource.readyState === "open" && !isFinite(newDuration)) {
const maxSeekable = getMaximumLiveSeekablePosition(duration);
log.info("mse", "calling `mediaSource.setLiveSeekableRange`", { maxSeekable });
mediaSource.setLiveSeekableRange(0, maxSeekable);
}
}
catch (err) {
log.warn("mse", "Can't update duration on the MediaSource.", err instanceof Error ? err : "");
return "failed" /* MediaSourceDurationUpdateStatus.Failed */;
}
const deltaToExpected = Math.abs(mediaSource.duration - newDuration);
if (deltaToExpected >= 0.1) {
const deltaToBefore = Math.abs(mediaSource.duration - oldDuration);
return deltaToExpected < deltaToBefore
? "partial" /* MediaSourceDurationUpdateStatus.Partial */
: "failed" /* MediaSourceDurationUpdateStatus.Failed */;
}
return "success" /* MediaSourceDurationUpdateStatus.Success */;
}
}
/**
* Returns a `SharedReference` wrapping a boolean that tells if all the
* SourceBuffers ended all pending updates.
* @param {SourceBufferList} sourceBuffers
* @param {Object} cancelSignal
* @returns {Object}
*/
function createSourceBuffersUpdatingReference(sourceBuffers, cancelSignal) {
if (sourceBuffers.length === 0) {
const notOpenedRef = new SharedReference(false);
notOpenedRef.finish();
return notOpenedRef;
}
const areUpdatingRef = new SharedReference(false, cancelSignal);
reCheck();
for (let i = 0; i < sourceBuffers.length; i++) {
const sourceBuffer = sourceBuffers[i];
sourceBuffer.addEventListener("updatestart", reCheck);
sourceBuffer.addEventListener("update", reCheck);
cancelSignal.register(() => {
sourceBuffer.removeEventListener("updatestart", reCheck);
sourceBuffer.removeEventListener("update", reCheck);
});
}
return areUpdatingRef;
function reCheck() {
for (let i = 0; i < sourceBuffers.length; i++) {
const sourceBuffer = sourceBuffers[i];
if (sourceBuffer.updating) {
areUpdatingRef.setValueIfChanged(true);
return;
}
}
areUpdatingRef.setValueIfChanged(false);
}
}
/**
* Returns a `SharedReference` wrapping a boolean that tells if the media
* source is opened or not.
* @param {MediaSource} mediaSource
* @param {Object} cancelSignal
* @returns {Object}
*/
function createMediaSourceOpenReference(mediaSource, cancelSignal) {
const isMediaSourceOpen = new SharedReference(mediaSource.readyState === "open", cancelSignal);
onSourceOpen(mediaSource, () => {
log.debug("mse", "Reacting to MediaSource open in duration updater");
isMediaSourceOpen.setValueIfChanged(true);
}, cancelSignal);
onSourceEnded(mediaSource, () => {
log.debug("mse", "Reacting to MediaSource ended in duration updater");
isMediaSourceOpen.setValueIfChanged(false);
}, cancelSignal);
onSourceClose(mediaSource, () => {
log.debug("mse", "Reacting to MediaSource close in duration updater");
isMediaSourceOpen.setValueIfChanged(false);
}, cancelSignal);
return isMediaSourceOpen;
}
/**
* Immediately tries to set the MediaSource's duration to the most appropriate
* one.
*
* If it fails, wait 2 seconds and retries.
*
* @param {MediaSource} mediaSource
* @param {number} duration
* @param {boolean} isRealEndKnown
* @param {Object} cancelSignal
*/
function recursivelyForceDurationUpdate(mediaSource, duration, isRealEndKnown, cancelSignal) {
const res = setMediaSourceDuration(mediaSource, duration, isRealEndKnown);
if (res === "success" /* MediaSourceDurationUpdateStatus.Success */) {
return;
}
const timeoutId = setTimeout(() => {
unregisterClear();
recursivelyForceDurationUpdate(mediaSource, duration, isRealEndKnown, cancelSignal);
}, 2000);
const unregisterClear = cancelSignal.register(() => {
clearTimeout(timeoutId);
});
}
function getMaximumLiveSeekablePosition(contentLastPosition) {
// Some targets poorly support setting a very high number for seekable
// ranges.
// Yet, in contents whose end is not yet known (e.g. live contents), we
// would prefer setting a value as high as possible to still be able to
// seek anywhere we want to (even ahead of the Manifest if we want to).
// As such, we put it at a safe default value of 2^32 excepted when the
// maximum position is already relatively close to that value, where we
// authorize exceptionally going over it.
return Math.max(Math.pow(2, 32), contentLastPosition + YEAR_IN_SECONDS);
}