UNPKG

rx-player

Version:
846 lines (845 loc) 37 kB
"use strict"; /** * 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 is_seeking_approximate_1 = require("../compat/is_seeking_approximate"); var config_1 = require("../config"); var manual_time_ranges_1 = require("../core/segment_sinks/implementations/utils/manual_time_ranges"); var log_1 = require("../log"); var monotonic_timestamp_1 = require("../utils/monotonic_timestamp"); var noop_1 = require("../utils/noop"); var object_assign_1 = require("../utils/object_assign"); var ranges_1 = require("../utils/ranges"); var reference_1 = require("../utils/reference"); var task_canceller_1 = require("../utils/task_canceller"); var generate_read_only_observer_1 = require("./utils/generate_read_only_observer"); var observation_position_1 = require("./utils/observation_position"); /** * HTMLMediaElement Events for which playback observations are calculated and * emitted. */ var SCANNED_MEDIA_ELEMENTS_EVENTS = [ "canplay", "ended", "play", "pause", "seeking", "seeked", "loadedmetadata", "ratechange", ]; /** * Class allowing to "observe" current playback conditions so the RxPlayer is * then able to react upon them. * * This is a central class of the RxPlayer as many modules rely on the * `PlaybackObserver` to know the current state of the media being played. * * You can use the PlaybackObserver to either get the last observation * performed, get the current media state or listen to media observation sent * at a regular interval. * * @class {PlaybackObserver} */ var PlaybackObserver = /** @class */ (function () { /** * Create a new `PlaybackObserver`, which allows to produce new "playback * observations" on various media events and intervals. * * Once a `PlaybackObserver` is created, you will want to "attach" the * media element to it through the `attachMediaElement` method once that * element is ready to play your content. * * Note that creating a `PlaybackObserver` lead to the usage of resources, * such as event listeners which will only be freed once the `stop` method is * called. * @param {Object} options */ function PlaybackObserver(options) { var _this = this; this._internalSeeksIncoming = []; this._mediaElementRef = new reference_1.default(null); this._withMediaSource = options.withMediaSource; this._lowLatencyMode = options.lowLatencyMode; this._canceller = new task_canceller_1.default("MediaElementPlaybackObserver"); this._expectedSeekingPosition = null; this._pendingSeek = null; this._isSeekBlocked = false; this._observationIntervalId = null; this._observationRef = this._createSharedReference(); this._canceller.signal.register(function () { _this._mediaElementRef.finish(); }); } /** * "Link" the actual `HTMLMediaElement` to this `PlaybackObserver`. * * This is done in a step separate from the constructor to allow complex * situations where you want to inialize the polling logic before the media * element is ready to play your content (e.g. when pre-loading the next * content). * * @param {HTMLMediaElement} mediaElement - The `HTMLMediaElement` on which * the content plays. */ PlaybackObserver.prototype.attachMediaElement = function (mediaElement) { var prevMediaElement = this._mediaElementRef.getValue(); if (prevMediaElement !== null) { throw new Error("A media element was already attached to this PlaybackObserver"); } this._mediaElementRef.setValue(mediaElement); if (this._canceller.isUsed()) { return; } this._registerLoadedMetadataEvent(mediaElement); this._restartInterval(); this._registerMediaElementEvents(mediaElement); this._generateObservationForEvent("init"); if (this._canceller.isUsed()) { return; } if (mediaElement.readyState >= 1) { this._onLoadedMetadataEvent(); } }; /** * Stop the `PlaybackObserver` from emitting playback observations and free all * resources reserved to emitting them such as event listeners and intervals. * * Once `stop` is called, no new playback observation will ever be emitted. * * Note that it is important to call stop once the `PlaybackObserver` is no * more needed to avoid unnecessarily leaking resources. * @param {string | undefined} reason - Human-inspectable reason behind the * stop. Used for debugging matters, especially for debug log * inspection. */ PlaybackObserver.prototype.stop = function (reason) { this._canceller.cancel(reason !== null && reason !== void 0 ? reason : "MediaElementPlaybackObserver stop"); }; /** * Returns the current position advertised by the `HTMLMediaElement`, in * seconds. * @returns {number} */ PlaybackObserver.prototype.getCurrentTime = function () { var _a, _b; return ((_b = (_a = this._mediaElementRef.getValue()) === null || _a === void 0 ? void 0 : _a.currentTime) !== null && _b !== void 0 ? _b : this._observationRef.getValue().position.getPolled()); }; /** * Returns the current playback rate advertised by the `HTMLMediaElement`. * @returns {number|undefined} */ PlaybackObserver.prototype.getPlaybackRate = function () { var _a, _b; return ((_b = (_a = this._mediaElementRef.getValue()) === null || _a === void 0 ? void 0 : _a.playbackRate) !== null && _b !== void 0 ? _b : this._observationRef.getValue().playbackRate); }; /** * Returns the current `paused` status advertised by the `HTMLMediaElement`. * * Use this instead of the same status emitted on an observation when you want * to be sure you're using the current value. * @returns {boolean|undefined} */ PlaybackObserver.prototype.getIsPaused = function () { var _a, _b; return ((_b = (_a = this._mediaElementRef.getValue()) === null || _a === void 0 ? void 0 : _a.paused) !== null && _b !== void 0 ? _b : this._observationRef.getValue().paused); }; /** * Prevent seeking operations from being performed from inside the * `MediaElementPlaybackObserver` until the `unblockSeeking` method is called. * * You might want to call this method when you want to ensure that the next * seek operation on the media element happens at a specific, controlled, * point in time. */ PlaybackObserver.prototype.blockSeeking = function () { this._isSeekBlocked = true; }; /** * Remove seeking block created by the `blockSeeking` method if it was called. * * If a seek operation was requested while the block was active, the * `MediaElementPlaybackObserver` will seek at the last seeked position as * soon as possible (either right now, or when the `readyState` of the * `HTMLMediaElement` will have at least reached the `"HAVE_METADATA"` state). */ PlaybackObserver.prototype.unblockSeeking = function () { if (this._isSeekBlocked) { this._isSeekBlocked = false; var mediaElement = this._mediaElementRef.getValue(); if (mediaElement !== null && mediaElement.readyState >= 1 && this._pendingSeek !== null) { var _a = this._pendingSeek, positionToSeekTo = _a.position, isInternal = _a.isInternal; this._pendingSeek = null; this._actuallySetCurrentTime(mediaElement, positionToSeekTo, isInternal); } } }; /** * Returns `true` if seeking operations are currently blocked due to a call to * `blockSeeking` that was not yet undone by a call to `unblockSeeking`. * @returns {boolean} - `true` if seeking operations are blocked currently. */ PlaybackObserver.prototype.isSeekingBlocked = function () { return this._isSeekBlocked; }; /** * Seek operations, as performed by the `setCurrentTime` method, might be not * yet performed due to either of those reasons: * * - Seek operations are blocked due to a call to the `blockSeeking` method. * * - The `HTMLMediaElement`'s `readyState` property has not yet reached the * `"HAVE_METADATA"` state. * * Under any of those two conditions, this method will return the position * that is planned to be seeked to as soon as both conditions are not met * anymore. * * If seeks are possible right now, no seek should be "pending" and as such * this method will return `null`. * * @returns {Object|null} - If a seek is planned, the position to seek to. * `null` otherwise. */ PlaybackObserver.prototype.getPendingSeekInformation = function () { return this._pendingSeek; }; /** * Update the current position (seek) on the `HTMLMediaElement`, by giving a * new position in seconds. * * Note that seeks performed through this method are caracherized as * "internal" seeks. They don't result into the exact same playback * observation than regular seeks (which most likely comes from the outside, * e.g. the user). * @param {number} time * @param {boolean} [isInternal=true] - If `false`, the seek was performed by * the user. */ PlaybackObserver.prototype.setCurrentTime = function (time, isInternal) { if (isInternal === void 0) { isInternal = true; } var mediaElement = this._mediaElementRef.getValue(); if (mediaElement !== null && !this._isSeekBlocked && mediaElement.readyState >= 1) { this._actuallySetCurrentTime(mediaElement, time, isInternal); } else { this._pendingSeek = { position: time, isInternal: isInternal }; this._internalSeeksIncoming = []; this._generateObservationForEvent("manual"); } }; /** * Update the playback rate of the `HTMLmediaElement`. * @param {number} playbackRate */ PlaybackObserver.prototype.setPlaybackRate = function (playbackRate) { var mediaElement = this._mediaElementRef.getValue(); if (mediaElement === null) { return; } mediaElement.playbackRate = playbackRate; }; /** * Returns the current `readyState` advertised by the `HTMLmediaElement`. * @returns {number} */ PlaybackObserver.prototype.getReadyState = function () { var _a, _b; return ((_b = (_a = this._mediaElementRef.getValue()) === null || _a === void 0 ? void 0 : _a.readyState) !== null && _b !== void 0 ? _b : this._observationRef.getValue().readyState); }; /** * Returns an `IReadOnlySharedReference` storing the last playback observation * produced by the `PlaybackObserver` and updated each time a new one is * produced. * * This value can then be for example listened to to be notified of future * playback observations. * * @returns {Object} */ PlaybackObserver.prototype.getReference = function () { return this._observationRef; }; /** * Register a callback so it regularly receives playback observations. * @param {Function} cb * @param {Object} params - Configuration parameters: * - `includeLastObservation`: If set to `true` the last observation will * be first emitted synchronously. * - `clearSignal`: If set, the callback will be unregistered when this * CancellationSignal emits. */ PlaybackObserver.prototype.listen = function (cb, params) { if (this._canceller.isUsed() || params.clearSignal.isCancelled()) { return noop_1.default; } this._observationRef.onUpdate(cb, { clearSignal: params.clearSignal, emitCurrentValue: params.includeLastObservation, }); }; /** * Generate a new playback observer which can listen to other * properties and which can only be accessed to read observations (e.g. * it cannot ask to perform a seek). * * The object returned will respect the `IReadOnlyPlaybackObserver` interface * and will inherit this `PlaybackObserver`'s lifecycle: it will emit when * the latter emits. * * As argument, this method takes a function which will allow to produce * the new set of properties to be present on each observation. * @param {Function} transform * @returns {Object} */ PlaybackObserver.prototype.deriveReadOnlyObserver = function (transform) { return (0, generate_read_only_observer_1.default)(this, transform, this._canceller.signal); }; PlaybackObserver.prototype._actuallySetCurrentTime = function (mediaElement, time, isInternal) { log_1.default.info("media", "Actually seeking.", { time: time, isInternal: isInternal }); if (isInternal) { this._internalSeeksIncoming.push(time); } mediaElement.currentTime = time; }; /** * Creates the `IReadOnlySharedReference` that will generate playback * observations. * @returns {Object} */ PlaybackObserver.prototype._createSharedReference = function () { var _this = this; if (this._observationRef !== undefined) { return this._observationRef; } var returnedSharedReference = new reference_1.default(this._getCurrentObservation("init"), this._canceller.signal); this._restartInterval(); var mediaElement = this._mediaElementRef.getValue(); if (mediaElement !== null) { this._registerMediaElementEvents(mediaElement); } this._canceller.signal.register(function () { if (_this._observationIntervalId !== null) { clearInterval(_this._observationIntervalId); } returnedSharedReference.finish(); }); return returnedSharedReference; }; PlaybackObserver.prototype._getCurrentObservation = function (event) { var _a, _b, _c, _d; /** Actual event emitted through an observation. */ var tmpEvt = event; var mediaElement = this._mediaElementRef.getValue(); // NOTE: `this._observationRef` may be `undefined` because we might here be // called in the constructor when that property is not yet set. var previousObservation = this._observationRef === undefined ? getInitialObservation(mediaElement) : this._observationRef.getValue(); /** * If `true`, there is a seek operation ongoing but it was done from the * `PlaybackObserver`'s `setCurrentTime` method, not from external code. */ var isInternalSeeking = false; /** If set, the position for which we plan to seek to as soon as possible. */ var pendingPosition = (_b = (_a = this._pendingSeek) === null || _a === void 0 ? void 0 : _a.position) !== null && _b !== void 0 ? _b : null; /** Initially-polled playback observation, before adjustments. */ var mediaTimings = mediaElement === null ? getEmptyMediaInfo() : getMediaInfos(mediaElement); var buffered = mediaTimings.buffered, readyState = mediaTimings.readyState, position = mediaTimings.position, seeking = mediaTimings.seeking; if (tmpEvt === "seeking") { // We just began seeking. // Let's find out if the seek is internal or external and handle approximate // seeking if (this._internalSeeksIncoming.length > 0) { isInternalSeeking = true; tmpEvt = "internal-seeking"; var startedInternalSeekTime = this._internalSeeksIncoming.shift(); this._expectedSeekingPosition = (0, is_seeking_approximate_1.default)() ? Math.max(position, startedInternalSeekTime !== null && startedInternalSeekTime !== void 0 ? startedInternalSeekTime : 0) : position; } else { this._expectedSeekingPosition = position; } } else if (seeking) { // we're still seeking, this time without a "seeking" event so it's an // already handled one, keep track of the last wanted position we wanted // to seek to, to work-around devices re-seeking silently. this._expectedSeekingPosition = Math.max(position, (_c = this._expectedSeekingPosition) !== null && _c !== void 0 ? _c : 0); } else if ((0, is_seeking_approximate_1.default)() && this._expectedSeekingPosition !== null && position < this._expectedSeekingPosition) { // We're on a target with aproximate seeking, we're not seeking anymore, but // we're not yet at the expected seeking position. // Signal to the rest of the application that the intented position is not // the current position but the one contained in `this._expectedSeekingPosition` pendingPosition = this._expectedSeekingPosition; } else { this._expectedSeekingPosition = null; } if (seeking && previousObservation.seeking === 1 /* SeekingState.Internal */ && event !== "seeking") { isInternalSeeking = true; } // NOTE: Devices which decide to not exactly seek where we want to seek // (e.g. to start on an intra video frame instead) bother us when it // comes to defining rebuffering and freezing statuses, because we might // for example believe that we're rebuffering whereas it's just that the // device decided to bring us just before the buffered data. // // After many major issues on those devices (namely Tizen), we decided to // just consider the position WE wanted to seek to as the real current // position for buffer-starvation related metrics like the current range, // the bufferGap, the rebuffering status, the freezing status... // // This specificity should only apply to those devices, other devices rely // on the actual current position. var basePosition = (_d = this._expectedSeekingPosition) !== null && _d !== void 0 ? _d : position; var currentRange; var bufferGap; if (!this._withMediaSource && buffered.length === 0 && readyState >= 3) { // Sometimes `buffered` stay empty for directfile contents yet we are able // to play. This seems to be linked to browser-side issues but has been // encountered on enough platforms (Chrome desktop and PlayStation 4's // WebKit for us to do something about it in the player. currentRange = undefined; bufferGap = undefined; } else { currentRange = (0, ranges_1.getBufferedTimeRange)(buffered, basePosition); bufferGap = currentRange !== null ? currentRange.end - basePosition : // TODO null/0 would probably be // more appropriate Infinity; } var fullyLoaded = hasLoadedUntilTheEnd(basePosition, currentRange, mediaTimings.ended, mediaTimings.duration, this._lowLatencyMode); var rebufferingStatus = getRebufferingStatus({ previousObservation: previousObservation, currentObservation: mediaTimings, basePosition: basePosition, observationEvent: tmpEvt, lowLatencyMode: this._lowLatencyMode, withMediaSource: this._withMediaSource, bufferGap: bufferGap, fullyLoaded: fullyLoaded, }); var freezingStatus = getFreezingStatus(previousObservation, mediaTimings, tmpEvt, bufferGap); var seekingState; if (isInternalSeeking) { seekingState = 1 /* SeekingState.Internal */; } else if (seeking) { seekingState = 2 /* SeekingState.External */; } else { seekingState = 0 /* SeekingState.None */; } var timings = (0, object_assign_1.default)({}, mediaTimings, { position: new observation_position_1.default(mediaTimings.position, pendingPosition), event: tmpEvt, seeking: seekingState, rebuffering: rebufferingStatus, freezing: freezingStatus, bufferGap: bufferGap, currentRange: currentRange, fullyLoaded: fullyLoaded, }); if (log_1.default.hasLevel("DEBUG")) { log_1.default.debug("media", "Current media element state tick.", { evt: timings.event, position: timings.position.getPolled(), seeking: timings.seeking, internalSeek: isInternalSeeking, rebuffering: timings.rebuffering !== null, freezing: timings.freezing !== null, ended: timings.ended, paused: timings.paused, playbackRate: timings.playbackRate, readyState: timings.readyState, pendingPosition: pendingPosition, }); } return timings; }; PlaybackObserver.prototype._generateObservationForEvent = function (event) { var newObservation = this._getCurrentObservation(event); if (log_1.default.hasLevel("DEBUG")) { log_1.default.debug("media", "current playback timeline:\n" + prettyPrintBuffered(newObservation.buffered, newObservation.position.getPolled()), "\n".concat(event)); } this._observationRef.setValue(newObservation); }; PlaybackObserver.prototype._restartInterval = function () { var _this = this; var _a = config_1.default.getCurrent(), SAMPLING_INTERVAL_MEDIASOURCE = _a.SAMPLING_INTERVAL_MEDIASOURCE, SAMPLING_INTERVAL_LOW_LATENCY = _a.SAMPLING_INTERVAL_LOW_LATENCY, SAMPLING_INTERVAL_NO_MEDIASOURCE = _a.SAMPLING_INTERVAL_NO_MEDIASOURCE; var interval; if (this._lowLatencyMode) { interval = SAMPLING_INTERVAL_LOW_LATENCY; } else if (this._withMediaSource) { interval = SAMPLING_INTERVAL_MEDIASOURCE; } else { interval = SAMPLING_INTERVAL_NO_MEDIASOURCE; } if (this._observationIntervalId !== null) { clearInterval(this._observationIntervalId); } this._observationIntervalId = setInterval(function () { return _this._generateObservationForEvent("timeupdate"); }, interval); }; PlaybackObserver.prototype._registerMediaElementEvents = function (mediaElement) { var _this = this; SCANNED_MEDIA_ELEMENTS_EVENTS.map(function (eventName) { var onMediaEvent = function () { _this._restartInterval(); _this._generateObservationForEvent(eventName); }; mediaElement.addEventListener(eventName, onMediaEvent); _this._canceller.signal.register(function () { mediaElement.removeEventListener(eventName, onMediaEvent); }); }); }; PlaybackObserver.prototype._registerLoadedMetadataEvent = function (mediaElement) { var loadedmetadataCb = this._onLoadedMetadataEvent.bind(this); mediaElement.addEventListener("loadedmetadata", loadedmetadataCb); this._canceller.signal.register(function () { mediaElement.removeEventListener("loadedmetadata", loadedmetadataCb); }); }; PlaybackObserver.prototype._onLoadedMetadataEvent = function () { var mediaElement = this._mediaElementRef.getValue(); if (mediaElement !== null && this._pendingSeek !== null && !this._isSeekBlocked) { var _a = this._pendingSeek, positionToSeekTo = _a.position, isInternal = _a.isInternal; this._pendingSeek = null; this._actuallySetCurrentTime(mediaElement, positionToSeekTo, isInternal); } }; return PlaybackObserver; }()); exports.default = PlaybackObserver; /** * Returns the amount of time in seconds the buffer should have ahead of the * current position before resuming playback. Based on the infos of the * rebuffering status. * * Waiting time differs between a rebuffering happening after a "seek" or one * happening after a buffer starvation occured. * @param {Object|null} rebufferingStatus * @param {Boolean} lowLatencyMode * @returns {Number} */ function getRebufferingEndGap(rebufferingStatus, lowLatencyMode) { if (rebufferingStatus === null) { return 0; } var suffix = lowLatencyMode ? "LOW_LATENCY" : "DEFAULT"; var _a = config_1.default.getCurrent(), RESUME_GAP_AFTER_SEEKING = _a.RESUME_GAP_AFTER_SEEKING, RESUME_GAP_AFTER_NOT_ENOUGH_DATA = _a.RESUME_GAP_AFTER_NOT_ENOUGH_DATA, RESUME_GAP_AFTER_BUFFERING = _a.RESUME_GAP_AFTER_BUFFERING; switch (rebufferingStatus.reason) { case "seeking": return RESUME_GAP_AFTER_SEEKING[suffix]; case "not-ready": return RESUME_GAP_AFTER_NOT_ENOUGH_DATA[suffix]; case "buffering": return RESUME_GAP_AFTER_BUFFERING[suffix]; } } /** * @param {Object} currentRange * @param {Number} duration * @param {Boolean} lowLatencyMode * @returns {Boolean} */ function hasLoadedUntilTheEnd(currentTime, currentRange, ended, duration, lowLatencyMode) { var REBUFFERING_GAP = config_1.default.getCurrent().REBUFFERING_GAP; var suffix = lowLatencyMode ? "LOW_LATENCY" : "DEFAULT"; if (currentRange === undefined) { return ended && Math.abs(duration - currentTime) <= REBUFFERING_GAP[suffix]; } return currentRange !== null && duration - currentRange.end <= REBUFFERING_GAP[suffix]; } /** * Get polled media metrics for when the `HTMLMediaElement` is not yet "attached" * to the `PlaybackObserver`. * * Those metrics actually corresponds to an idle media element. * @returns {Object} */ function getEmptyMediaInfo() { return { buffered: new manual_time_ranges_1.default(), position: 0, duration: NaN, ended: false, paused: true, playbackRate: 1, readyState: 0, seeking: false, }; } /** * Get basic playback information. * @param {HTMLmediaElement} mediaElement * @returns {Object} */ function getMediaInfos(mediaElement) { var buffered = mediaElement.buffered, currentTime = mediaElement.currentTime, duration = mediaElement.duration, ended = mediaElement.ended, paused = mediaElement.paused, playbackRate = mediaElement.playbackRate, readyState = mediaElement.readyState, seeking = mediaElement.seeking; return { buffered: buffered, position: currentTime, duration: duration, ended: ended, paused: paused, playbackRate: playbackRate, readyState: readyState, seeking: seeking, }; } /** * Infer the rebuffering status. * @param {Object} options * @returns {Object|null} */ function getRebufferingStatus(_a) { var previousObservation = _a.previousObservation, currentObservation = _a.currentObservation, basePosition = _a.basePosition, observationEvent = _a.observationEvent, withMediaSource = _a.withMediaSource, lowLatencyMode = _a.lowLatencyMode, bufferGap = _a.bufferGap, fullyLoaded = _a.fullyLoaded; var REBUFFERING_GAP = config_1.default.getCurrent().REBUFFERING_GAP; var currentTime = currentObservation.position, paused = currentObservation.paused, readyState = currentObservation.readyState, ended = currentObservation.ended; var prevRebuffering = previousObservation.rebuffering, prevEvt = previousObservation.event, prevTime = previousObservation.position; var canSwitchToRebuffering = prevRebuffering === null && !(fullyLoaded || ended); var rebufferEndPosition = null; var shouldRebuffer; var shouldStopRebuffer; var rebufferGap = lowLatencyMode ? REBUFFERING_GAP.LOW_LATENCY : REBUFFERING_GAP.DEFAULT; if (withMediaSource) { if (canSwitchToRebuffering) { if (bufferGap === Infinity) { shouldRebuffer = true; rebufferEndPosition = basePosition; } else if (bufferGap === undefined) { if (readyState < 3) { shouldRebuffer = true; rebufferEndPosition = undefined; } } else if (bufferGap <= rebufferGap) { shouldRebuffer = true; rebufferEndPosition = basePosition + bufferGap; } } else if (prevRebuffering !== null) { var resumeGap = getRebufferingEndGap(prevRebuffering, lowLatencyMode); if ((shouldRebuffer !== true && prevRebuffering !== null && readyState > 1 && (fullyLoaded || ended || (bufferGap !== undefined && isFinite(bufferGap) && bufferGap > resumeGap))) || (bufferGap === undefined && readyState >= 3)) { shouldStopRebuffer = true; } else if (bufferGap === undefined) { rebufferEndPosition = undefined; } else if (bufferGap === Infinity) { rebufferEndPosition = basePosition; } else if (bufferGap <= resumeGap) { rebufferEndPosition = basePosition + bufferGap; } } } // when using a direct file, the media will stall and unstall on its // own, so we only try to detect when the media timestamp has not changed // between two consecutive timeupdates else { if (canSwitchToRebuffering && // TODO what about when paused: e.g. when loading initially the content ((!paused && observationEvent === "timeupdate" && prevEvt === "timeupdate" && currentTime === prevTime.getPolled()) || (observationEvent === "seeking" && (bufferGap === Infinity || (bufferGap === undefined && readyState < 3))))) { shouldRebuffer = true; } else if (prevRebuffering !== null && ((observationEvent !== "seeking" && currentTime !== prevTime.getPolled()) || observationEvent === "canplay" || (bufferGap === undefined && readyState >= 3) || (bufferGap !== undefined && bufferGap < Infinity && (bufferGap > getRebufferingEndGap(prevRebuffering, lowLatencyMode) || fullyLoaded || ended)))) { shouldStopRebuffer = true; } } if (shouldStopRebuffer === true) { return null; } else if (shouldRebuffer === true || prevRebuffering !== null) { var reason = void 0; if (observationEvent === "seeking" || (prevRebuffering !== null && prevRebuffering.reason === "seeking")) { reason = "seeking"; } else if (currentObservation.seeking) { reason = "seeking"; } else if (readyState === 1) { reason = "not-ready"; } else { reason = "buffering"; } if (prevRebuffering !== null && prevRebuffering.reason === reason) { return { reason: prevRebuffering.reason, timestamp: prevRebuffering.timestamp, position: rebufferEndPosition, }; } return { reason: reason, timestamp: (0, monotonic_timestamp_1.default)(), position: rebufferEndPosition, }; } return null; } /** * Detect if the current media can be considered as "freezing" (i.e. not * advancing for unknown reasons). * * Returns a corresponding `IFreezingStatus` object if that's the case and * `null` if not. * @param {Object} prevObservation * @param {Object} currentInfo * @param {string} currentEvt * @param {number|undefined} bufferGap * @returns {Object|null} */ function getFreezingStatus(prevObservation, currentInfo, currentEvt, bufferGap) { var MINIMUM_BUFFER_AMOUNT_BEFORE_FREEZING = config_1.default.getCurrent().MINIMUM_BUFFER_AMOUNT_BEFORE_FREEZING; if (prevObservation.freezing !== null) { if (currentInfo.ended || currentInfo.paused || currentInfo.readyState === 0 || currentInfo.playbackRate === 0 || prevObservation.position.getPolled() !== currentInfo.position) { return null; // Quit freezing status } return prevObservation.freezing; // Stay in it } return currentEvt === "timeupdate" && bufferGap !== undefined && bufferGap > MINIMUM_BUFFER_AMOUNT_BEFORE_FREEZING && !currentInfo.ended && !currentInfo.paused && currentInfo.readyState >= 1 && currentInfo.playbackRate !== 0 && currentInfo.position === prevObservation.position.getPolled() ? { timestamp: (0, monotonic_timestamp_1.default)() } : null; } /** * Pretty print a TimeRanges Object, to see the current content of it in a * one-liner string. * * @example * This function is called by giving it directly the TimeRanges, such as: * ```js * prettyPrintBuffered(document.getElementsByTagName("video")[0].buffered); * ``` * * Let's consider this possible return: * * ``` * 0.00|==29.95==|29.95 ~30.05~ 60.00|==29.86==|89.86 * ^14 * ``` * This means that our video element has 29.95 seconds of buffer between 0 and * 29.95 seconds. * Then 30.05 seconds where no buffer is found. * Then 29.86 seconds of buffer between 60.00 and 89.86 seconds. * * A caret on the second line indicates the current time we're at. * The number coming after it is the current time. * @param {TimeRanges} buffered * @param {number} currentTime * @returns {string} */ function prettyPrintBuffered(buffered, currentTime) { var str = ""; var currentTimeStr = ""; for (var i = 0; i < buffered.length; i++) { var start = buffered.start(i); var end = buffered.end(i); var fixedStart = start.toFixed(2); var fixedEnd = end.toFixed(2); var fixedDuration = (end - start).toFixed(2); var newIntervalStr = "".concat(fixedStart, "|==").concat(fixedDuration, "==|").concat(fixedEnd); str += newIntervalStr; if (currentTimeStr.length === 0 && end > currentTime) { var padBefore = str.length - Math.floor(newIntervalStr.length / 2); currentTimeStr = " ".repeat(padBefore) + "^".concat(currentTime); } if (i < buffered.length - 1) { var nextStart = buffered.start(i + 1); var fixedDiff = (nextStart - end).toFixed(2); var holeStr = " ~".concat(fixedDiff, "~ "); str += holeStr; if (currentTimeStr.length === 0 && currentTime < nextStart) { var padBefore = str.length - Math.floor(holeStr.length / 2); currentTimeStr = " ".repeat(padBefore) + "^".concat(currentTime); } } } if (currentTimeStr.length === 0) { currentTimeStr = " ".repeat(str.length) + "^".concat(currentTime); } return str + "\n" + currentTimeStr; } /** * Generate the initial playback observation for when no event has yet been * emitted to lead to one. * @param {HTMLMediaElement} mediaElement * @returns {Object} */ function getInitialObservation(mediaElement) { var mediaTimings = mediaElement === null ? getEmptyMediaInfo() : getMediaInfos(mediaElement); return (0, object_assign_1.default)(mediaTimings, { rebuffering: null, event: "init", seeking: 0 /* SeekingState.None */, position: new observation_position_1.default(mediaTimings.position, null), freezing: null, bufferGap: 0, currentRange: null, fullyLoaded: false, }); }