rx-player
Version:
Canal+ HTML5 Video Player
278 lines (277 loc) • 12.8 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
var event_listeners_1 = require("../../compat/event_listeners");
var has_issues_with_high_media_source_duration_1 = require("../../compat/has_issues_with_high_media_source_duration");
var log_1 = require("../../log");
var reference_1 = require("../../utils/reference");
var task_canceller_1 = require("../../utils/task_canceller");
/** Number of seconds in a regular year. */
var 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
*/
var MediaSourceDurationUpdater = /** @class */ (function () {
/**
* Create a new `MediaSourceDurationUpdater`,
* @param {MediaSource} mediaSource - The MediaSource on which the content is
* played.
*/
function MediaSourceDurationUpdater(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).
*/
MediaSourceDurationUpdater.prototype.updateDuration = function (newDuration, isRealEndKnown) {
if (this._currentMediaSourceDurationUpdateCanceller !== null) {
this._currentMediaSourceDurationUpdateCanceller.cancel("manual duration update");
}
this._currentMediaSourceDurationUpdateCanceller = new task_canceller_1.default("MediaSource Duration Update");
var mediaSource = this._mediaSource;
var currentSignal = this._currentMediaSourceDurationUpdateCanceller.signal;
var isMediaSourceOpened = createMediaSourceOpenReference(mediaSource, currentSignal);
/** TaskCanceller triggered each time the MediaSource switches to and from "open". */
var msOpenStatusCanceller = new task_canceller_1.default(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 task_canceller_1.default(undefined /* we do not care for logs here */);
msOpenStatusCanceller.linkToSignal(currentSignal);
var areSourceBuffersUpdating = createSourceBuffersUpdatingReference(mediaSource.sourceBuffers, msOpenStatusCanceller.signal);
/** TaskCanceller triggered each time SourceBuffers' updating status changes */
var sourceBuffersUpdatingCanceller = new task_canceller_1.default(undefined);
sourceBuffersUpdatingCanceller.linkToSignal(msOpenStatusCanceller.signal);
return areSourceBuffersUpdating.onUpdate(function (areUpdating) {
sourceBuffersUpdatingCanceller.cancel("SourceBuffer status update");
sourceBuffersUpdatingCanceller = new task_canceller_1.default(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.
*/
MediaSourceDurationUpdater.prototype.stopUpdating = function (reason) {
if (this._currentMediaSourceDurationUpdateCanceller !== null) {
this._currentMediaSourceDurationUpdateCanceller.cancel(reason !== null && reason !== void 0 ? reason : "stop MSDU");
this._currentMediaSourceDurationUpdateCanceller = null;
}
};
return MediaSourceDurationUpdater;
}());
exports.default = MediaSourceDurationUpdater;
/**
* 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) {
var newDuration = duration;
if (!isRealEndKnown) {
newDuration = (0, has_issues_with_high_media_source_duration_1.default)()
? Infinity
: getMaximumLiveSeekablePosition(duration);
}
var maxBufferedEnd = 0;
for (var i = 0; i < mediaSource.sourceBuffers.length; i++) {
var sourceBuffer = mediaSource.sourceBuffers[i];
var 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_1.default.info("mse", "Updating duration to what is currently buffered", {
maxBufferedEnd: maxBufferedEnd,
});
mediaSource.duration = maxBufferedEnd;
}
catch (err) {
log_1.default.warn("mse", "Can't update duration on the MediaSource.", err instanceof Error ? err : "");
return "failed" /* MediaSourceDurationUpdateStatus.Failed */;
}
}
return "partial" /* MediaSourceDurationUpdateStatus.Partial */;
}
else {
var oldDuration = mediaSource.duration;
try {
log_1.default.info("mse", "Updating duration", { newDuration: newDuration });
mediaSource.duration = newDuration;
if (mediaSource.readyState === "open" && !isFinite(newDuration)) {
var maxSeekable = getMaximumLiveSeekablePosition(duration);
log_1.default.info("mse", "calling `mediaSource.setLiveSeekableRange`", { maxSeekable: maxSeekable });
mediaSource.setLiveSeekableRange(0, maxSeekable);
}
}
catch (err) {
log_1.default.warn("mse", "Can't update duration on the MediaSource.", err instanceof Error ? err : "");
return "failed" /* MediaSourceDurationUpdateStatus.Failed */;
}
var deltaToExpected = Math.abs(mediaSource.duration - newDuration);
if (deltaToExpected >= 0.1) {
var 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) {
var notOpenedRef = new reference_1.default(false);
notOpenedRef.finish();
return notOpenedRef;
}
var areUpdatingRef = new reference_1.default(false, cancelSignal);
reCheck();
var _loop_1 = function (i) {
var sourceBuffer = sourceBuffers[i];
sourceBuffer.addEventListener("updatestart", reCheck);
sourceBuffer.addEventListener("update", reCheck);
cancelSignal.register(function () {
sourceBuffer.removeEventListener("updatestart", reCheck);
sourceBuffer.removeEventListener("update", reCheck);
});
};
for (var i = 0; i < sourceBuffers.length; i++) {
_loop_1(i);
}
return areUpdatingRef;
function reCheck() {
for (var i = 0; i < sourceBuffers.length; i++) {
var 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) {
var isMediaSourceOpen = new reference_1.default(mediaSource.readyState === "open", cancelSignal);
(0, event_listeners_1.onSourceOpen)(mediaSource, function () {
log_1.default.debug("mse", "Reacting to MediaSource open in duration updater");
isMediaSourceOpen.setValueIfChanged(true);
}, cancelSignal);
(0, event_listeners_1.onSourceEnded)(mediaSource, function () {
log_1.default.debug("mse", "Reacting to MediaSource ended in duration updater");
isMediaSourceOpen.setValueIfChanged(false);
}, cancelSignal);
(0, event_listeners_1.onSourceClose)(mediaSource, function () {
log_1.default.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) {
var res = setMediaSourceDuration(mediaSource, duration, isRealEndKnown);
if (res === "success" /* MediaSourceDurationUpdateStatus.Success */) {
return;
}
var timeoutId = setTimeout(function () {
unregisterClear();
recursivelyForceDurationUpdate(mediaSource, duration, isRealEndKnown, cancelSignal);
}, 2000);
var unregisterClear = cancelSignal.register(function () {
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);
}