UNPKG

rx-player

Version:
1,111 lines 112 kB
/** * Copyright 2015 CANAL+ Group * * Licensed under the Apache License, Version 2.0 (the "License");publicapi * 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 canRelyOnVideoVisibilityAndSize from "../../compat/can_rely_on_video_visibility_and_size"; import { getPictureOnPictureStateRef, getVideoVisibilityRef, getElementResolutionRef, getScreenResolutionRef, } from "../../compat/event_listeners"; import getStartDate from "../../compat/get_start_date"; import hasMseInWorker from "../../compat/has_mse_in_worker"; import hasWorkerApi from "../../compat/has_worker_api"; import isDebugModeEnabled from "../../compat/is_debug_mode_enabled"; import config from "../../config"; import { ErrorCodes, ErrorTypes, formatError, MediaError } from "../../errors"; import WorkerInitializationError from "../../errors/worker_initialization_error"; import features, { addFeatures } from "../../features"; import log from "../../log"; import { getLivePosition, getMaximumSafePosition, getMinimumSafePosition, createRepresentationFilterFromFnString, getPeriodForTime, } from "../../manifest"; import MediaElementPlaybackObserver from "../../playback_observer/media_element_playback_observer"; import arrayFind from "../../utils/array_find"; import arrayIncludes from "../../utils/array_includes"; import assert, { assertUnreachable } from "../../utils/assert"; import EventEmitter from "../../utils/event_emitter"; import idGenerator from "../../utils/id_generator"; import isNullOrUndefined from "../../utils/is_null_or_undefined"; import getMonotonicTimeStamp from "../../utils/monotonic_timestamp"; import objectAssign from "../../utils/object_assign"; import { getLeftSizeOfBufferedTimeRange } from "../../utils/ranges"; import SharedReference, { createMappedReference } from "../../utils/reference"; import TaskCanceller from "../../utils/task_canceller"; import { clearOnStop, disposeDecryptionResources, getKeySystemConfiguration, } from "../decrypt"; import renderThumbnail from "../render_thumbnail"; import TracksStore from "../tracks_store"; import { checkReloadOptions, parseConstructorOptions, parseLoadVideoOptions, } from "./option_utils"; import { constructPlayerStateReference, emitPlayPauseEvents, emitSeekEvents, isLoadedState, } from "./utils"; /* eslint-disable @typescript-eslint/naming-convention */ const generateContentId = idGenerator(); /** * Options of a `loadVideo` call which are for now not supported when running * in a "multithread" mode. * * TODO support those? */ const MULTI_THREAD_UNSUPPORTED_LOAD_VIDEO_OPTIONS = [ "manifestLoader", "segmentLoader", ]; /** * @class Player * @extends EventEmitter */ class Player extends EventEmitter { /** All possible Error types emitted by the RxPlayer. */ static get ErrorTypes() { return ErrorTypes; } /** All possible Error codes emitted by the RxPlayer. */ static get ErrorCodes() { return ErrorCodes; } /** * Current log level. * Update current log level. * Should be either (by verbosity ascending): * - "NONE" * - "ERROR" * - "WARNING" * - "INFO" * - "DEBUG" * Any other value will be translated to "NONE". */ static get LogLevel() { return log.getLevel(); } static set LogLevel(logLevel) { log.setLevel(logLevel, log.getFormat()); } /** * Current log format. * Should be either (by verbosity ascending): * - "standard": Regular log messages. * - "full": More verbose format, including a timestamp and a namespace. * Any other value will be translated to "standard". */ static get LogFormat() { return log.getFormat(); } static set LogFormat(format) { log.setLevel(log.getLevel(), format); } /** * Add feature(s) to the RxPlayer. * @param {Array.<Object>} featureList - Features wanted. */ static addFeatures(featureList) { addFeatures(featureList); } /** * Register the video element to the set of elements currently in use. * @param videoElement the video element to register. * @throws Error - Throws if the element is already used by another player instance. */ static _priv_registerVideoElement(videoElement) { if (Player._priv_currentlyUsedVideoElements.has(videoElement)) { const errorMessage = "The video element is already attached to another RxPlayer instance." + "\nMake sure to dispose the previous instance with player.dispose() before creating" + " a new player instance attaching that video element."; // eslint-disable-next-line no-console console.warn(errorMessage); /* * TODO: for next major version 5.0: this need to throw an error instead of just logging * this was not done for minor version as it could be considerated a breaking change. * * throw new Error(errorMessage); */ } Player._priv_currentlyUsedVideoElements.add(videoElement); } /** * Deregister the video element of the set of elements currently in use. * @param videoElement the video element to deregister. */ static _priv_deregisterVideoElement(videoElement) { if (Player._priv_currentlyUsedVideoElements.has(videoElement)) { Player._priv_currentlyUsedVideoElements.delete(videoElement); } } /** * @constructor * @param {Object} options */ constructor(options = {}) { super(); const { baseBandwidth, videoResolutionLimit, maxBufferAhead, maxBufferBehind, throttleVideoBitrateWhenHidden, videoElement, wantedBufferAhead, maxVideoBufferSize, } = parseConstructorOptions(options); // Workaround to support Firefox autoplay on FF 42. // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1194624 videoElement.preload = "auto"; this.version = /* PLAYER_VERSION */ "4.3.0"; this.log = log; this.state = "STOPPED"; this.videoElement = videoElement; Player._priv_registerVideoElement(this.videoElement); const destroyCanceller = new TaskCanceller(); this._destroyCanceller = destroyCanceller; this._priv_pictureInPictureRef = getPictureOnPictureStateRef(videoElement, destroyCanceller.signal); this._priv_speed = new SharedReference(videoElement.playbackRate, this._destroyCanceller.signal); this._priv_preferTrickModeTracks = false; this._priv_contentLock = new SharedReference(false, this._destroyCanceller.signal); this._priv_bufferOptions = { wantedBufferAhead: new SharedReference(wantedBufferAhead, this._destroyCanceller.signal), maxBufferAhead: new SharedReference(maxBufferAhead, this._destroyCanceller.signal), maxBufferBehind: new SharedReference(maxBufferBehind, this._destroyCanceller.signal), maxVideoBufferSize: new SharedReference(maxVideoBufferSize, this._destroyCanceller.signal), }; this._priv_bitrateInfos = { lastBitrates: { audio: baseBandwidth, video: baseBandwidth }, }; this._priv_throttleVideoBitrateWhenHidden = throttleVideoBitrateWhenHidden; this._priv_videoResolutionLimit = videoResolutionLimit; this._priv_currentError = null; this._priv_contentInfos = null; this._priv_contentEventsMemory = {}; this._priv_reloadingMetadata = {}; this._priv_lastAutoPlay = false; this._priv_worker = null; const onVolumeChange = () => { this.trigger("volumeChange", { volume: videoElement.volume, muted: videoElement.muted, }); }; videoElement.addEventListener("volumechange", onVolumeChange); destroyCanceller.signal.register(() => { videoElement.removeEventListener("volumechange", onVolumeChange); }); } /** * TODO returns promise? * @param {Object} workerSettings */ attachWorker(workerSettings) { return new Promise((res, rej) => { var _a; if (!hasWorkerApi()) { log.warn("API: Cannot rely on a WebWorker: Worker API unavailable"); return rej(new WorkerInitializationError("INCOMPATIBLE_ERROR", "Worker unavailable")); } if (typeof workerSettings.workerUrl === "string") { this._priv_worker = new Worker(workerSettings.workerUrl); } else { const blobUrl = URL.createObjectURL(workerSettings.workerUrl); this._priv_worker = new Worker(blobUrl); URL.revokeObjectURL(blobUrl); } this._priv_worker.onerror = (evt) => { if (this._priv_worker !== null) { this._priv_worker.terminate(); this._priv_worker = null; } log.error("API: Unexpected worker error", evt.error instanceof Error ? evt.error : undefined); rej(new WorkerInitializationError("UNKNOWN_ERROR", 'Unexpected Worker "error" event')); }; const handleInitMessages = (msg) => { const msgData = msg.data; if (msgData.type === "init-error" /* WorkerMessageType.InitError */) { log.warn("API: Processing InitError worker message: detaching worker"); if (this._priv_worker !== null) { this._priv_worker.removeEventListener("message", handleInitMessages); this._priv_worker.terminate(); this._priv_worker = null; } rej(new WorkerInitializationError("SETUP_ERROR", "Worker parser initialization failed: " + msgData.value.errorMessage)); } else if (msgData.type === "init-success" /* WorkerMessageType.InitSuccess */) { log.info("API: InitSuccess received from worker."); if (this._priv_worker !== null) { this._priv_worker.removeEventListener("message", handleInitMessages); } res(); } }; this._priv_worker.addEventListener("message", handleInitMessages); log.debug("---> Sending To Worker:", "init" /* MainThreadMessageType.Init */); this._priv_worker.postMessage({ type: "init" /* MainThreadMessageType.Init */, value: { dashWasmUrl: workerSettings.dashWasmUrl, logLevel: log.getLevel(), logFormat: log.getFormat(), sendBackLogs: isDebugModeEnabled(), date: Date.now(), timestamp: getMonotonicTimeStamp(), hasVideo: ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.nodeName.toLowerCase()) === "video", hasMseInWorker, }, }); log.addEventListener("onLogLevelChange", (logInfo) => { if (this._priv_worker === null) { return; } log.debug("---> Sending To Worker:", "log-level-update" /* MainThreadMessageType.LogLevelUpdate */); this._priv_worker.postMessage({ type: "log-level-update" /* MainThreadMessageType.LogLevelUpdate */, value: { logLevel: logInfo.level, logFormat: logInfo.format, sendBackLogs: isDebugModeEnabled(), }, }); }, this._destroyCanceller.signal); const sendConfigUpdates = (updates) => { if (this._priv_worker === null) { return; } log.debug("---> Sending To Worker:", "config-update" /* MainThreadMessageType.ConfigUpdate */); this._priv_worker.postMessage({ type: "config-update" /* MainThreadMessageType.ConfigUpdate */, value: updates, }); }; if (config.updated) { sendConfigUpdates(config.getCurrent()); } config.addEventListener("update", sendConfigUpdates, this._destroyCanceller.signal); }); } /** * Returns information on which "mode" the RxPlayer is running for the current * content (e.g. main logic running in a WebWorker or not, are we in * directfile mode...). * * Returns `null` if no content is loaded. * @returns {Object|null} */ getCurrentModeInformation() { if (this._priv_contentInfos === null) { return null; } return { isDirectFile: this._priv_contentInfos.isDirectFile, useWorker: this._priv_contentInfos.useWorker, }; } /** * Register a new callback for a player event event. * * @param {string} evt - The event to register a callback to * @param {Function} fn - The callback to call as that event is triggered. * The callback will take as argument the eventual payload of the event * (single argument). */ addEventListener(evt, fn) { // The EventEmitter's `addEventListener` method takes an optional third // argument that we do not want to expose in the public API. // We thus overwrite that function to remove any possible usage of that // third argument. return super.addEventListener(evt, fn); } /** * Stop the playback for the current content. */ stop() { if (this._priv_contentInfos !== null) { this._priv_contentInfos.currentContentCanceller.cancel(); } this._priv_cleanUpCurrentContentState(); if (this.state !== "STOPPED" /* PLAYER_STATES.STOPPED */) { this._priv_setPlayerState("STOPPED" /* PLAYER_STATES.STOPPED */); } } /** * Free the resources used by the player. * /!\ The player cannot be "used" anymore after this method has been called. */ dispose() { // free resources linked to the loaded content this.stop(); if (this.videoElement !== null) { Player._priv_deregisterVideoElement(this.videoElement); // free resources used for decryption management disposeDecryptionResources(this.videoElement).catch((err) => { const message = err instanceof Error ? err.message : "Unknown error"; log.error("API: Could not dispose decryption resources: " + message); }); } // free resources linked to the Player instance this._destroyCanceller.cancel(); this._priv_reloadingMetadata = {}; // un-attach video element this.videoElement = null; if (this._priv_worker !== null) { this._priv_worker.terminate(); this._priv_worker = null; } } /** * Load a new video. * @param {Object} opts */ loadVideo(opts) { const options = parseLoadVideoOptions(opts); log.info("API: Calling loadvideo", options.url, options.transport); this._priv_reloadingMetadata = { options }; this._priv_initializeContentPlayback(options); this._priv_lastAutoPlay = options.autoPlay; } /** * Reload the last loaded content. * @param {Object} reloadOpts */ reload(reloadOpts) { var _a, _b, _c; const { options, manifest, reloadPosition, reloadInPause } = this._priv_reloadingMetadata; if (options === undefined) { throw new Error("API: Can't reload without having previously loaded a content."); } checkReloadOptions(reloadOpts); let startAt; if (((_a = reloadOpts === null || reloadOpts === void 0 ? void 0 : reloadOpts.reloadAt) === null || _a === void 0 ? void 0 : _a.position) !== undefined) { startAt = { position: reloadOpts.reloadAt.position }; } else if (((_b = reloadOpts === null || reloadOpts === void 0 ? void 0 : reloadOpts.reloadAt) === null || _b === void 0 ? void 0 : _b.relative) !== undefined) { if (reloadPosition === undefined) { throw new Error("Can't reload to a relative position when previous content was not loaded."); } else { startAt = { position: reloadOpts.reloadAt.relative + reloadPosition }; } } else if (reloadPosition !== undefined) { startAt = { position: reloadPosition }; } let autoPlay; if ((reloadOpts === null || reloadOpts === void 0 ? void 0 : reloadOpts.autoPlay) !== undefined) { autoPlay = reloadOpts.autoPlay; } else if (reloadInPause !== undefined) { autoPlay = !reloadInPause; } let keySystems; if ((reloadOpts === null || reloadOpts === void 0 ? void 0 : reloadOpts.keySystems) !== undefined) { keySystems = reloadOpts.keySystems; } else if (((_c = this._priv_reloadingMetadata.options) === null || _c === void 0 ? void 0 : _c.keySystems) !== undefined) { keySystems = this._priv_reloadingMetadata.options.keySystems; } const newOptions = Object.assign(Object.assign({}, options), { initialManifest: manifest }); if (startAt !== undefined) { newOptions.startAt = startAt; } if (autoPlay !== undefined) { newOptions.autoPlay = autoPlay; } if (keySystems !== undefined) { newOptions.keySystems = keySystems; } this._priv_initializeContentPlayback(newOptions); } createDebugElement(element) { if (features.createDebugElement === null) { throw new Error("Feature `DEBUG_ELEMENT` not added to the RxPlayer"); } const canceller = new TaskCanceller(); features.createDebugElement(element, this, canceller.signal); return { dispose() { canceller.cancel(); }, }; } /** * Returns an array decribing the various thumbnail tracks that can be * encountered at the wanted time or Period. * @param {Object} arg * @param {number|undefined} [arg.time] - The position to check for thumbnail * tracks, in seconds. * @param {string|undefined} [arg.periodId] - The Period to check for * thumbnail tracks. * If not set and if `arg.time` is also not set, the current Period will be * considered. * @returns {Array.<Object>} */ getAvailableThumbnailTracks({ time, periodId, } = {}) { if (this._priv_contentInfos === null || this._priv_contentInfos.manifest === null) { return []; } const { manifest } = this._priv_contentInfos; let period; if (time !== undefined) { period = getPeriodForTime(this._priv_contentInfos.manifest, time); if (period === undefined || period.thumbnailTracks.length === 0) { return []; } } else if (periodId !== undefined) { period = arrayFind(manifest.periods, (p) => p.id === periodId); if (period === undefined) { log.error("API: getAvailableThumbnailTracks: periodId not found"); return []; } } else { const { currentPeriod } = this._priv_contentInfos; if (currentPeriod === null) { return []; } period = currentPeriod; } return period.thumbnailTracks.map((t) => { return { id: t.id, width: Math.floor(t.width / t.horizontalTiles), height: Math.floor(t.height / t.verticalTiles), mimeType: t.mimeType, }; }); } /** * Render inside the given `container` the thumbnail corresponding to the * given time. * * If no thumbnail is available at that time or if the RxPlayer does not succeed * to load or render it, reject the corresponding Promise and remove the * potential previous thumbnail from the container. * * If a new `renderThumbnail` call is made with the same `container` before it * had time to finish, the Promise is also rejected but the previous thumbnail * potentially found in the container is untouched. * * @param {Object|undefined} options * @returns {Promise} */ async renderThumbnail(options) { if (isNullOrUndefined(options.time)) { throw new Error("You have to provide a `time` property to `renderThumbnail`, indicating the wanted thumbnail time in seconds."); } if (isNullOrUndefined(options.container)) { throw new Error("You have to provide a `container` property to `renderThumbnail`, specifying the HTML Element in which the thumbnail should be inserted."); } return renderThumbnail(this._priv_contentInfos, options); } /** * From given options, initialize content playback. * @param {Object} options */ _priv_initializeContentPlayback(options) { var _a, _b, _c, _d, _f, _g; const { autoPlay, cmcd, defaultAudioTrackSwitchingMode, enableFastSwitching, initialManifest, keySystems, lowLatencyMode, minimumManifestUpdateInterval, requestConfig, onCodecSwitch, startAt, transport, checkMediaSegmentIntegrity, checkManifestIntegrity, manifestLoader, referenceDateTime, segmentLoader, serverSyncInfos, mode, experimentalOptions, __priv_manifestUpdateUrl, __priv_patchLastSegmentInSidx, url, } = options; // Perform multiple checks on the given options if (this.videoElement === null) { throw new Error("the attached video element is disposed"); } const isDirectFile = transport === "directfile"; /** Emit to stop the current content. */ const currentContentCanceller = new TaskCanceller(); const videoElement = this.videoElement; let initializer; let useWorker = false; let mediaElementTracksStore = null; if (!isDirectFile) { /** Interface used to load and refresh the Manifest. */ const manifestRequestSettings = { lowLatencyMode, maxRetry: (_a = requestConfig.manifest) === null || _a === void 0 ? void 0 : _a.maxRetry, requestTimeout: (_b = requestConfig.manifest) === null || _b === void 0 ? void 0 : _b.timeout, connectionTimeout: (_c = requestConfig.manifest) === null || _c === void 0 ? void 0 : _c.connectionTimeout, minimumManifestUpdateInterval, initialManifest, }; const relyOnVideoVisibilityAndSize = canRelyOnVideoVisibilityAndSize(); const throttlers = { throttleBitrate: {}, limitResolution: {}, }; if (this._priv_throttleVideoBitrateWhenHidden) { if (!relyOnVideoVisibilityAndSize) { log.warn("API: Can't apply throttleVideoBitrateWhenHidden because " + "browser can't be trusted for visibility."); } else { throttlers.throttleBitrate = { video: createMappedReference(getVideoVisibilityRef(this._priv_pictureInPictureRef, currentContentCanceller.signal), (isActive) => (isActive ? Infinity : 0), currentContentCanceller.signal), }; } } if (this._priv_videoResolutionLimit === "videoElement") { if (!relyOnVideoVisibilityAndSize) { log.warn("API: Can't apply videoResolutionLimit because browser can't be " + "trusted for video size."); } else { throttlers.limitResolution = { video: getElementResolutionRef(videoElement, this._priv_pictureInPictureRef, currentContentCanceller.signal), }; } } else if (this._priv_videoResolutionLimit === "screen") { throttlers.limitResolution = { video: getScreenResolutionRef(currentContentCanceller.signal), }; } /** Options used by the adaptive logic. */ const adaptiveOptions = { initialBitrates: this._priv_bitrateInfos.lastBitrates, lowLatencyMode, throttlers, }; /** Options used by the TextTrack SegmentSink. */ const textTrackOptions = options.textTrackMode === "native" ? { textTrackMode: "native" } : { textTrackMode: "html", textTrackElement: options.textTrackElement, }; const bufferOptions = objectAssign({ enableFastSwitching, onCodecSwitch }, this._priv_bufferOptions); const segmentRequestOptions = { lowLatencyMode, maxRetry: (_d = requestConfig.segment) === null || _d === void 0 ? void 0 : _d.maxRetry, requestTimeout: (_f = requestConfig.segment) === null || _f === void 0 ? void 0 : _f.timeout, connectionTimeout: (_g = requestConfig.segment) === null || _g === void 0 ? void 0 : _g.connectionTimeout, }; const canRunInMultiThread = features.multithread !== null && this._priv_worker !== null && transport === "dash" && MULTI_THREAD_UNSUPPORTED_LOAD_VIDEO_OPTIONS.every((option) => isNullOrUndefined(options[option])) && typeof options.representationFilter !== "function"; if (mode === "main" || (mode === "auto" && !canRunInMultiThread)) { if (features.mainThreadMediaSourceInit === null) { throw new Error("Cannot load video, neither in a WebWorker nor with the " + "`MEDIA_SOURCE_MAIN` feature"); } const transportFn = features.transports[transport]; if (typeof transportFn !== "function") { // Stop previous content and reset its state this.stop(); this._priv_currentError = null; throw new Error(`transport "${transport}" not supported`); } const representationFilter = typeof options.representationFilter === "string" ? createRepresentationFilterFromFnString(options.representationFilter) : options.representationFilter; log.info("API: Initializing MediaSource mode in the main thread"); const transportPipelines = transportFn({ lowLatencyMode, checkMediaSegmentIntegrity, checkManifestIntegrity, manifestLoader, referenceDateTime, representationFilter, segmentLoader, serverSyncInfos, __priv_manifestUpdateUrl, __priv_patchLastSegmentInSidx, }); initializer = new features.mainThreadMediaSourceInit({ adaptiveOptions, autoPlay, bufferOptions, cmcd, enableRepresentationAvoidance: experimentalOptions.enableRepresentationAvoidance, keySystems, lowLatencyMode, transport: transportPipelines, manifestRequestSettings, segmentRequestOptions, speed: this._priv_speed, startAt, textTrackOptions, url, }); } else { if (features.multithread === null) { throw new Error("Cannot load video in multithread mode: `MULTI_THREAD` " + "feature not imported."); } else if (this._priv_worker === null) { throw new Error("Cannot load video in multithread mode: `attachWorker` " + "method not called."); } assert(typeof options.representationFilter !== "function"); useWorker = true; log.info("API: Initializing MediaSource mode in a WebWorker"); const transportOptions = { lowLatencyMode, checkMediaSegmentIntegrity, checkManifestIntegrity, referenceDateTime, serverSyncInfos, manifestLoader: undefined, segmentLoader: undefined, representationFilter: options.representationFilter, __priv_manifestUpdateUrl, __priv_patchLastSegmentInSidx, }; initializer = new features.multithread.init({ adaptiveOptions, autoPlay, bufferOptions, cmcd, enableRepresentationAvoidance: experimentalOptions.enableRepresentationAvoidance, keySystems, lowLatencyMode, transportOptions, manifestRequestSettings, segmentRequestOptions, speed: this._priv_speed, startAt, textTrackOptions, worker: this._priv_worker, url, }); } } else { if (features.directfile === null) { this.stop(); this._priv_currentError = null; throw new Error("DirectFile feature not activated in your build."); } else if (isNullOrUndefined(url)) { throw new Error("No URL for a DirectFile content"); } log.info("API: Initializing DirectFile mode in the main thread"); mediaElementTracksStore = this._priv_initializeMediaElementTracksStore(currentContentCanceller.signal); if (currentContentCanceller.isUsed()) { return; } initializer = new features.directfile.initDirectFile({ autoPlay, keySystems, speed: this._priv_speed, startAt, url, }); } /** Future `this._priv_contentInfos` related to this content. */ const contentInfos = { contentId: generateContentId(), originalUrl: url, currentContentCanceller, defaultAudioTrackSwitchingMode, initializer, isDirectFile, manifest: null, currentPeriod: null, activeAdaptations: null, activeRepresentations: null, tracksStore: null, mediaElementTracksStore, useWorker, segmentSinkMetricsCallback: null, fetchThumbnailDataCallback: null, thumbnailRequestsInfo: { pendingRequests: new WeakMap(), lastResponse: null, }, }; // Bind events initializer.addEventListener("error", (error) => { this._priv_onFatalError(error, contentInfos); }); initializer.addEventListener("warning", (error) => { const formattedError = formatError(error, { defaultCode: "NONE", defaultReason: "An unknown error happened.", }); log.warn("API: Sending warning:", formattedError); this.trigger("warning", formattedError); }); initializer.addEventListener("reloadingMediaSource", (payload) => { if (contentInfos.tracksStore !== null) { contentInfos.tracksStore.resetPeriodObjects(); } if (this._priv_contentInfos !== null) { this._priv_contentInfos.segmentSinkMetricsCallback = null; } this._priv_lastAutoPlay = payload.autoPlay; }); initializer.addEventListener("inbandEvents", (inbandEvents) => this.trigger("inbandEvents", inbandEvents)); initializer.addEventListener("streamEvent", (streamEvent) => this.trigger("streamEvent", streamEvent)); initializer.addEventListener("streamEventSkip", (streamEventSkip) => this.trigger("streamEventSkip", streamEventSkip)); initializer.addEventListener("activePeriodChanged", (periodInfo) => this._priv_onActivePeriodChanged(contentInfos, periodInfo)); initializer.addEventListener("periodStreamReady", (periodReadyInfo) => this._priv_onPeriodStreamReady(contentInfos, periodReadyInfo)); initializer.addEventListener("periodStreamCleared", (periodClearedInfo) => this._priv_onPeriodStreamCleared(contentInfos, periodClearedInfo)); initializer.addEventListener("representationChange", (representationInfo) => this._priv_onRepresentationChange(contentInfos, representationInfo)); initializer.addEventListener("adaptationChange", (adaptationInfo) => this._priv_onAdaptationChange(contentInfos, adaptationInfo)); initializer.addEventListener("bitrateEstimateChange", (bitrateEstimateInfo) => this._priv_onBitrateEstimateChange(bitrateEstimateInfo)); initializer.addEventListener("manifestReady", (manifest) => this._priv_onManifestReady(contentInfos, manifest)); initializer.addEventListener("manifestUpdate", (updates) => this._priv_onManifestUpdate(contentInfos, updates)); initializer.addEventListener("codecSupportUpdate", () => this._priv_onCodecSupportUpdate(contentInfos)); initializer.addEventListener("decipherabilityUpdate", (updates) => this._priv_onDecipherabilityUpdate(contentInfos, updates)); initializer.addEventListener("loaded", (evt) => { if (this._priv_contentInfos !== null) { this._priv_contentInfos.segmentSinkMetricsCallback = evt.getSegmentSinkMetrics; this._priv_contentInfos.fetchThumbnailDataCallback = evt.getThumbnailData; } }); // Now, that most events are linked, prepare the next content. initializer.prepare(); // Now that the content is prepared, stop previous content and reset state // This is done after content preparation as `stop` could technically have // a long and synchronous blocking time. // Note that this call is done **synchronously** after all events linking. // This is **VERY** important so: // - the `STOPPED` state is switched to synchronously after loading a new // content. // - we can avoid involontarily catching events linked to the previous // content. this.stop(); /** Global "playback observer" which will emit playback conditions */ const playbackObserver = new MediaElementPlaybackObserver(videoElement, { withMediaSource: !isDirectFile, lowLatencyMode, }); currentContentCanceller.signal.register(() => { playbackObserver.stop(); }); // Update the RxPlayer's state at the right events const playerStateRef = constructPlayerStateReference(initializer, videoElement, playbackObserver, currentContentCanceller.signal); currentContentCanceller.signal.register(() => { initializer.dispose(); }); /** * Function updating `this._priv_reloadingMetadata` in function of the * current state and playback conditions. * To call when either might change. * @param {string} state - The player state we're about to switch to. */ const updateReloadingMetadata = (state) => { switch (state) { case "STOPPED": case "RELOADING": case "LOADING": break; // keep previous metadata case "ENDED": this._priv_reloadingMetadata.reloadInPause = true; this._priv_reloadingMetadata.reloadPosition = playbackObserver .getReference() .getValue() .position.getPolled(); break; default: { const o = playbackObserver.getReference().getValue(); this._priv_reloadingMetadata.reloadInPause = o.paused; this._priv_reloadingMetadata.reloadPosition = o.position.getWanted(); break; } } }; /** * `TaskCanceller` allowing to stop emitting `"play"` and `"pause"` * events. * `null` when such events are not emitted currently. */ let playPauseEventsCanceller = null; /** * Callback emitting `"play"` and `"pause`" events once the content is * loaded, starting from the state indicated in argument. * @param {boolean} willAutoPlay - If `false`, we're currently paused. */ const triggerPlayPauseEventsWhenReady = (willAutoPlay) => { if (playPauseEventsCanceller !== null) { playPauseEventsCanceller.cancel(); // cancel previous logic playPauseEventsCanceller = null; } playerStateRef.onUpdate((val, stopListeningToStateUpdates) => { if (!isLoadedState(val)) { return; // content not loaded yet: no event } stopListeningToStateUpdates(); if (playPauseEventsCanceller !== null) { playPauseEventsCanceller.cancel(); } playPauseEventsCanceller = new TaskCanceller(); playPauseEventsCanceller.linkToSignal(currentContentCanceller.signal); if (willAutoPlay !== !videoElement.paused) { // paused status is not at the expected value on load: emit event if (videoElement.paused) { this.trigger("pause", null); } else { this.trigger("play", null); } } emitPlayPauseEvents(videoElement, () => this.trigger("play", null), () => this.trigger("pause", null), currentContentCanceller.signal); }, { emitCurrentValue: false, clearSignal: currentContentCanceller.signal, }); }; triggerPlayPauseEventsWhenReady(autoPlay); initializer.addEventListener("reloadingMediaSource", (payload) => { triggerPlayPauseEventsWhenReady(payload.autoPlay); }); this._priv_currentError = null; this._priv_contentInfos = contentInfos; /** * `TaskCanceller` allowing to stop emitting `"seeking"` and `"seeked"` * events. * `null` when such events are not emitted currently. */ let seekEventsCanceller = null; // React to player state change playerStateRef.onUpdate((newState) => { updateReloadingMetadata(newState); this._priv_setPlayerState(newState); if (currentContentCanceller.isUsed()) { return; } if (seekEventsCanceller !== null) { if (!isLoadedState(this.state)) { seekEventsCanceller.cancel(); seekEventsCanceller = null; } } else if (isLoadedState(this.state)) { seekEventsCanceller = new TaskCanceller(); seekEventsCanceller.linkToSignal(currentContentCanceller.signal); emitSeekEvents(playbackObserver, () => this.trigger("seeking", null), () => this.trigger("seeked", null), seekEventsCanceller.signal); } }, { emitCurrentValue: true, clearSignal: currentContentCanceller.signal }); // React to playback conditions change playbackObserver.listen((observation) => { updateReloadingMetadata(this.state); this._priv_triggerPositionUpdate(contentInfos, observation); }, { clearSignal: currentContentCanceller.signal }); currentContentCanceller.signal.register(() => { initializer.removeEventListener(); }); // initialize the content only when the lock is inactive this._priv_contentLock.onUpdate((isLocked, stopListeningToLock) => { if (!isLocked) { stopListeningToLock(); // start playback! initializer.start(videoElement, playbackObserver); } }, { emitCurrentValue: true, clearSignal: currentContentCanceller.signal }); } /** * Returns fatal error if one for the current content. * null otherwise. * @returns {Object|null} - The current Error (`null` when no error). */ getError() { return this._priv_currentError; } /** * Returns the media DOM element used by the player. * You should not its HTML5 API directly and use the player's method instead, * to ensure a well-behaved player. * @returns {HTMLMediaElement|null} - The HTMLMediaElement used (`null` when * disposed) */ // eslint-disable-next-line @typescript-eslint/no-restricted-types getVideoElement() { // eslint-disable-next-line @typescript-eslint/no-restricted-types return this.videoElement; } /** * Returns the player's current state. * @returns {string} - The current Player's state */ getPlayerState() { return this.state; } /** * Returns true if a content is loaded. * @returns {Boolean} - `true` if a content is loaded, `false` otherwise. */ isContentLoaded() { return !arrayIncludes(["LOADING", "RELOADING", "STOPPED"], this.state); } /** * Returns true if the player is buffering. * @returns {Boolean} - `true` if the player is buffering, `false` otherwise. */ isBuffering() { return arrayIncludes(["BUFFERING", "SEEKING", "LOADING", "RELOADING"], this.state); } /** * Returns the play/pause status of the player : * - when `LOADING` or `RELOADING`, returns the scheduled play/pause condition * for when loading is over, * - in other states, returns the `<video>` element .paused value, * - if the player is disposed, returns `true`. * @returns {Boolean} - `true` if the player is paused or will be after loading, * `false` otherwise. */ isPaused() { if (this.videoElement !== null) { if (arrayIncludes(["LOADING", "RELOADING"], this.state)) { return !this._priv_lastAutoPlay; } else { return this.videoElement.paused; } } return true; } /** * Returns true if both: * - a content is loaded * - the content loaded is a live content * @returns {Boolean} - `true` if we're playing a live content, `false` otherwise. */ isLive() { if (this._priv_contentInfos === null) { return false; } const { isDirectFile, manifest } = this._priv_contentInfos; if (isDirectFile || manifest === null) { return false; } return manifest.isLive; } /** * Returns `true` if trickmode playback is active (usually through the usage * of the `setPlaybackRate` method), which means that the RxPlayer selects * "trickmode" video tracks in priority. * @returns {Boolean} */ areTrickModeTracksEnabled() { return this._priv_preferTrickModeTracks; } /** * Returns the URL(s) of the currently considered Manifest, or of the content for * directfile content. * @returns {Array.<string>|undefined} - Current URL. `undefined` if not known * or no URL yet. */ getContentUrls() { if (this._priv_contentInfos === null) { return undefined; } const { isDirectFile, manifest, originalUrl } = this._priv_contentInfos; if (isDirectFile) { return originalUrl === undefined ? undefined : [originalUrl]; } if (manifest !== null) { return manifest.uris; } return undefined; } /** * Update URL of the content currently being played (e.g. DASH's MPD). * @param {Array.<string>|undefined} urls - URLs to reach that content / * Manifest from the most prioritized URL to the least prioritized URL. * @param {Object|undefined} [params] * @param {boolean} params.refresh - If `true` the resource in question * (e.g. DASH's MPD) will be refreshed immediately. */ updateContentUrls(urls, params) { if (this._priv_contentInfos === null) { throw new Error("No content loaded"); } const refreshNow = (params === null || params === void 0 ? void 0 : params.refresh) === true; this._priv_contentInfos.initializer.updateContentUrls(urls, refreshNow); } /** * Returns the video duration, in seconds. * NaN if no video is playing. * @returns {Number} */ getMediaDuration() { if (this.videoElement === null) { throw new Error("Disposed player"); } return this.videoElement.duration; } /** * Returns in seconds the difference between: * - the end of the current contiguous loaded range. * - the current time * @returns {Number} */ getCurrentBufferGap() { if (this.videoElement === null) { throw new Error("Disposed player"); } const videoElement = this.videoElement; const bufferGap = getLeftSizeOfBufferedTimeRange(videoElement.buffered, videoElement.currentTime); if (bufferGap === Infinity) { return 0; } return bufferGap; } /** * Get the current position, in s, in wall-clock time. * That is: * - for live content, get a timestamp, in s, of the current played content. * - for static content, returns the position from beginning in s. * * If you do not know if you want to use this method or getPosition: * - If what you want is to display the current time to the user, use this * one. * - If what you want is to interact with the player's API or perform other * actions (like statistics) with the real player data, use getPosition. * * @returns {Number} */ getWallClockTime() { var _a; if (this.videoElement === null) { throw new Error("Disposed player"); } if (this._priv_contentInfos === null) { return this.videoElement.currentTime; } const { isDirectFile, manifest } = this._priv_contentInfos; if (isDirectFile) { const startDate = getStartDate(this.videoElement); return (startDate !== null && startDate !== void 0 ? startDate : 0) + this.videoElement.currentTime; } if (manifest !== null) { const currentTime = this.videoElement.currentTime; const ast = (_a = manifest.availabilityStartTime) !== null && _a !== void 0 ? _a : 0; return currentTime + ast; } return 0; } /** * Get the current position, in seconds, of the video element. * * If you do not know if you want to use this method or getWallClockTime: * - If what you want is to display the current time to the user, use * getWallClockTime. * - If what you want is to interact with the player's API or perform other * actions (like statistics) with the real player data, use this one. * * @returns {Number} */ getPosition() { if (this.videoElement === null) { throw new Error("Disposed player"); } return this.videoElement.currentTime; } /** * Returns the last stored content position, in seconds. * * @returns {number|undefined} */ getLastStoredContentPosition() { return this._priv_reloadingMetadata.reloadPosition; } /** * Returns the current playback rate at which the video plays. * @returns {Number} */ getPlaybackRate() { return this._priv_speed.getValue(); } /** * Update the playback rate of the video. * * This method's effect is persisted from content to content, and can be * called even when no content is playing (it will still have an effect for * the next contents). * *