rx-player
Version:
Canal+ HTML5 Video Player
836 lines (835 loc) • 34.3 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 isSeekingApproximate from "../compat/is_seeking_approximate";
import config from "../config";
import ManualTimeRanges from "../core/segment_sinks/implementations/utils/manual_time_ranges";
import log from "../log";
import getMonotonicTimeStamp from "../utils/monotonic_timestamp";
import noop from "../utils/noop";
import objectAssign from "../utils/object_assign";
import { getBufferedTimeRange } from "../utils/ranges";
import SharedReference from "../utils/reference";
import TaskCanceller from "../utils/task_canceller";
import generateReadOnlyObserver from "./utils/generate_read_only_observer";
import ObservationPosition from "./utils/observation_position";
/**
* HTMLMediaElement Events for which playback observations are calculated and
* emitted.
*/
const 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}
*/
export default class PlaybackObserver {
/**
* 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
*/
constructor(options) {
this._internalSeeksIncoming = [];
this._mediaElementRef = new SharedReference(null);
this._withMediaSource = options.withMediaSource;
this._lowLatencyMode = options.lowLatencyMode;
this._canceller = new TaskCanceller("MediaElementPlaybackObserver");
this._expectedSeekingPosition = null;
this._pendingSeek = null;
this._isSeekBlocked = false;
this._observationIntervalId = null;
this._observationRef = this._createSharedReference();
this._canceller.signal.register(() => {
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.
*/
attachMediaElement(mediaElement) {
const 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.
*/
stop(reason) {
this._canceller.cancel(reason !== null && reason !== void 0 ? reason : "MediaElementPlaybackObserver stop");
}
/**
* Returns the current position advertised by the `HTMLMediaElement`, in
* seconds.
* @returns {number}
*/
getCurrentTime() {
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}
*/
getPlaybackRate() {
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}
*/
getIsPaused() {
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.
*/
blockSeeking() {
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).
*/
unblockSeeking() {
if (this._isSeekBlocked) {
this._isSeekBlocked = false;
const mediaElement = this._mediaElementRef.getValue();
if (mediaElement !== null &&
mediaElement.readyState >= 1 &&
this._pendingSeek !== null) {
const { position: positionToSeekTo, isInternal } = this._pendingSeek;
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.
*/
isSeekingBlocked() {
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.
*/
getPendingSeekInformation() {
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.
*/
setCurrentTime(time, isInternal = true) {
const mediaElement = this._mediaElementRef.getValue();
if (mediaElement !== null && !this._isSeekBlocked && mediaElement.readyState >= 1) {
this._actuallySetCurrentTime(mediaElement, time, isInternal);
}
else {
this._pendingSeek = { position: time, isInternal };
this._internalSeeksIncoming = [];
this._generateObservationForEvent("manual");
}
}
/**
* Update the playback rate of the `HTMLmediaElement`.
* @param {number} playbackRate
*/
setPlaybackRate(playbackRate) {
const mediaElement = this._mediaElementRef.getValue();
if (mediaElement === null) {
return;
}
mediaElement.playbackRate = playbackRate;
}
/**
* Returns the current `readyState` advertised by the `HTMLmediaElement`.
* @returns {number}
*/
getReadyState() {
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}
*/
getReference() {
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.
*/
listen(cb, params) {
if (this._canceller.isUsed() || params.clearSignal.isCancelled()) {
return noop;
}
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}
*/
deriveReadOnlyObserver(transform) {
return generateReadOnlyObserver(this, transform, this._canceller.signal);
}
_actuallySetCurrentTime(mediaElement, time, isInternal) {
log.info("media", "Actually seeking.", { time, isInternal });
if (isInternal) {
this._internalSeeksIncoming.push(time);
}
mediaElement.currentTime = time;
}
/**
* Creates the `IReadOnlySharedReference` that will generate playback
* observations.
* @returns {Object}
*/
_createSharedReference() {
if (this._observationRef !== undefined) {
return this._observationRef;
}
const returnedSharedReference = new SharedReference(this._getCurrentObservation("init"), this._canceller.signal);
this._restartInterval();
const mediaElement = this._mediaElementRef.getValue();
if (mediaElement !== null) {
this._registerMediaElementEvents(mediaElement);
}
this._canceller.signal.register(() => {
if (this._observationIntervalId !== null) {
clearInterval(this._observationIntervalId);
}
returnedSharedReference.finish();
});
return returnedSharedReference;
}
_getCurrentObservation(event) {
var _a, _b, _c, _d;
/** Actual event emitted through an observation. */
let tmpEvt = event;
const 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.
const 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.
*/
let isInternalSeeking = false;
/** If set, the position for which we plan to seek to as soon as possible. */
let 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. */
const mediaTimings = mediaElement === null ? getEmptyMediaInfo() : getMediaInfos(mediaElement);
const { buffered, readyState, position, seeking } = mediaTimings;
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";
const startedInternalSeekTime = this._internalSeeksIncoming.shift();
this._expectedSeekingPosition = isSeekingApproximate()
? 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 (isSeekingApproximate() &&
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.
const basePosition = (_d = this._expectedSeekingPosition) !== null && _d !== void 0 ? _d : position;
let currentRange;
let 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 = getBufferedTimeRange(buffered, basePosition);
bufferGap =
currentRange !== null
? currentRange.end - basePosition
: // TODO null/0 would probably be
// more appropriate
Infinity;
}
const fullyLoaded = hasLoadedUntilTheEnd(basePosition, currentRange, mediaTimings.ended, mediaTimings.duration, this._lowLatencyMode);
const rebufferingStatus = getRebufferingStatus({
previousObservation,
currentObservation: mediaTimings,
basePosition,
observationEvent: tmpEvt,
lowLatencyMode: this._lowLatencyMode,
withMediaSource: this._withMediaSource,
bufferGap,
fullyLoaded,
});
const freezingStatus = getFreezingStatus(previousObservation, mediaTimings, tmpEvt, bufferGap);
let seekingState;
if (isInternalSeeking) {
seekingState = 1 /* SeekingState.Internal */;
}
else if (seeking) {
seekingState = 2 /* SeekingState.External */;
}
else {
seekingState = 0 /* SeekingState.None */;
}
const timings = objectAssign({}, mediaTimings, {
position: new ObservationPosition(mediaTimings.position, pendingPosition),
event: tmpEvt,
seeking: seekingState,
rebuffering: rebufferingStatus,
freezing: freezingStatus,
bufferGap,
currentRange,
fullyLoaded,
});
if (log.hasLevel("DEBUG")) {
log.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,
});
}
return timings;
}
_generateObservationForEvent(event) {
const newObservation = this._getCurrentObservation(event);
if (log.hasLevel("DEBUG")) {
log.debug("media", "current playback timeline:\n" +
prettyPrintBuffered(newObservation.buffered, newObservation.position.getPolled()), `\n${event}`);
}
this._observationRef.setValue(newObservation);
}
_restartInterval() {
const { SAMPLING_INTERVAL_MEDIASOURCE, SAMPLING_INTERVAL_LOW_LATENCY, SAMPLING_INTERVAL_NO_MEDIASOURCE, } = config.getCurrent();
let 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(() => this._generateObservationForEvent("timeupdate"), interval);
}
_registerMediaElementEvents(mediaElement) {
SCANNED_MEDIA_ELEMENTS_EVENTS.map((eventName) => {
const onMediaEvent = () => {
this._restartInterval();
this._generateObservationForEvent(eventName);
};
mediaElement.addEventListener(eventName, onMediaEvent);
this._canceller.signal.register(() => {
mediaElement.removeEventListener(eventName, onMediaEvent);
});
});
}
_registerLoadedMetadataEvent(mediaElement) {
const loadedmetadataCb = this._onLoadedMetadataEvent.bind(this);
mediaElement.addEventListener("loadedmetadata", loadedmetadataCb);
this._canceller.signal.register(() => {
mediaElement.removeEventListener("loadedmetadata", loadedmetadataCb);
});
}
_onLoadedMetadataEvent() {
const mediaElement = this._mediaElementRef.getValue();
if (mediaElement !== null && this._pendingSeek !== null && !this._isSeekBlocked) {
const { position: positionToSeekTo, isInternal } = this._pendingSeek;
this._pendingSeek = null;
this._actuallySetCurrentTime(mediaElement, positionToSeekTo, isInternal);
}
}
}
/**
* 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;
}
const suffix = lowLatencyMode ? "LOW_LATENCY" : "DEFAULT";
const { RESUME_GAP_AFTER_SEEKING, RESUME_GAP_AFTER_NOT_ENOUGH_DATA, RESUME_GAP_AFTER_BUFFERING, } = config.getCurrent();
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) {
const { REBUFFERING_GAP } = config.getCurrent();
const 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 ManualTimeRanges(),
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) {
const { buffered, currentTime, duration, ended, paused, playbackRate, readyState, seeking, } = mediaElement;
return {
buffered,
position: currentTime,
duration,
ended,
paused,
playbackRate,
readyState,
seeking,
};
}
/**
* Infer the rebuffering status.
* @param {Object} options
* @returns {Object|null}
*/
function getRebufferingStatus({ previousObservation, currentObservation, basePosition, observationEvent, withMediaSource, lowLatencyMode, bufferGap, fullyLoaded, }) {
const { REBUFFERING_GAP } = config.getCurrent();
const { position: currentTime, paused, readyState, ended } = currentObservation;
const { rebuffering: prevRebuffering, event: prevEvt, position: prevTime, } = previousObservation;
const canSwitchToRebuffering = prevRebuffering === null && !(fullyLoaded || ended);
let rebufferEndPosition = null;
let shouldRebuffer;
let shouldStopRebuffer;
const 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) {
const 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) {
let reason;
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,
timestamp: getMonotonicTimeStamp(),
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) {
const { MINIMUM_BUFFER_AMOUNT_BEFORE_FREEZING } = config.getCurrent();
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: getMonotonicTimeStamp() }
: 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) {
let str = "";
let currentTimeStr = "";
for (let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
const fixedStart = start.toFixed(2);
const fixedEnd = end.toFixed(2);
const fixedDuration = (end - start).toFixed(2);
const newIntervalStr = `${fixedStart}|==${fixedDuration}==|${fixedEnd}`;
str += newIntervalStr;
if (currentTimeStr.length === 0 && end > currentTime) {
const padBefore = str.length - Math.floor(newIntervalStr.length / 2);
currentTimeStr = " ".repeat(padBefore) + `^${currentTime}`;
}
if (i < buffered.length - 1) {
const nextStart = buffered.start(i + 1);
const fixedDiff = (nextStart - end).toFixed(2);
const holeStr = ` ~${fixedDiff}~ `;
str += holeStr;
if (currentTimeStr.length === 0 && currentTime < nextStart) {
const padBefore = str.length - Math.floor(holeStr.length / 2);
currentTimeStr = " ".repeat(padBefore) + `^${currentTime}`;
}
}
}
if (currentTimeStr.length === 0) {
currentTimeStr = " ".repeat(str.length) + `^${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) {
const mediaTimings = mediaElement === null ? getEmptyMediaInfo() : getMediaInfos(mediaElement);
return objectAssign(mediaTimings, {
rebuffering: null,
event: "init",
seeking: 0 /* SeekingState.None */,
position: new ObservationPosition(mediaTimings.position, null),
freezing: null,
bufferGap: 0,
currentRange: null,
fullyLoaded: false,
});
}