UNPKG

rx-player

Version:
917 lines (916 loc) 46.6 kB
/** * 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 isCodecSupported from "../../compat/is_codec_supported"; import mayMediaElementFailOnUndecipherableData from "../../compat/may_media_element_fail_on_undecipherable_data"; import shouldReloadMediaSourceOnDecipherabilityUpdate from "../../compat/should_reload_media_source_on_decipherability_update"; import config from "../../config"; import AdaptiveRepresentationSelector from "../../core/adaptive"; import CmcdDataBuilder from "../../core/cmcd"; import { CdnPrioritizer, createThumbnailFetcher, ManifestFetcher, SegmentQueueCreator, } from "../../core/fetchers"; import createContentTimeBoundariesObserver from "../../core/main/common/create_content_time_boundaries_observer"; import FreezeResolver from "../../core/main/common/FreezeResolver"; import getThumbnailData from "../../core/main/common/get_thumbnail_data"; import synchronizeSegmentSinksOnObservation from "../../core/main/common/synchronize_sinks_on_observation"; import SegmentSinksStore from "../../core/segment_sinks"; import StreamOrchestrator from "../../core/stream"; import { MediaError } from "../../errors"; import features from "../../features"; import log from "../../log"; import areArraysOfNumbersEqual from "../../utils/are_arrays_of_numbers_equal"; import assert, { assertUnreachable } from "../../utils/assert"; import createCancellablePromise from "../../utils/create_cancellable_promise"; import isNullOrUndefined from "../../utils/is_null_or_undefined"; import noop from "../../utils/noop"; import objectAssign from "../../utils/object_assign"; import SyncOrAsync from "../../utils/sync_or_async"; import TaskCanceller from "../../utils/task_canceller"; import { ContentDecryptorState, getKeySystemConfiguration } from "../decrypt"; import { ContentInitializer } from "./types"; import createCorePlaybackObserver from "./utils/create_core_playback_observer"; import createMediaSource from "./utils/create_media_source"; import getInitialTime from "./utils/get_initial_time"; import getLoadedReference from "./utils/get_loaded_reference"; import performInitialSeekAndPlay from "./utils/initial_seek_and_play"; import initializeContentDecryption from "./utils/initialize_content_decryption"; import MainThreadTextDisplayerInterface from "./utils/main_thread_text_displayer_interface"; import RebufferingController from "./utils/rebuffering_controller"; import StreamEventsEmitter from "./utils/stream_events_emitter"; import listenToMediaError from "./utils/throw_on_media_error"; /** * Allows to load a new content thanks to the MediaSource Extensions (a.k.a. MSE) * Web APIs. * * Through this `ContentInitializer`, a Manifest will be fetched (and depending * on the situation, refreshed), a `MediaSource` instance will be linked to the * wanted `HTMLMediaElement` and chunks of media data, called segments, will be * pushed on buffers associated to this `MediaSource` instance. * * @class MediaSourceContentInitializer */ export default class MediaSourceContentInitializer extends ContentInitializer { /** * Create a new `MediaSourceContentInitializer`, associated to the given * settings. * @param {Object} settings */ constructor(settings) { super(); this._initSettings = settings; this._initCanceller = new TaskCanceller(); this._manifest = null; this._decryptionCapabilities = { status: "uninitialized", value: null }; const urls = settings.url === undefined ? undefined : [settings.url]; this._cmcdDataBuilder = settings.cmcd === undefined ? null : new CmcdDataBuilder(settings.cmcd); this._manifestFetcher = new ManifestFetcher(urls, settings.transport, Object.assign(Object.assign({}, settings.manifestRequestSettings), { lowLatencyMode: settings.lowLatencyMode, cmcdDataBuilder: this._cmcdDataBuilder })); } /** * Perform non-destructive preparation steps, to prepare a future content. * For now, this mainly mean loading the Manifest document. */ prepare() { if (this._manifest !== null) { return; } this._manifest = SyncOrAsync.createAsync(createCancellablePromise(this._initCanceller.signal, (res, rej) => { this._manifestFetcher.addEventListener("warning", (err) => this.trigger("warning", err)); this._manifestFetcher.addEventListener("error", (err) => { this.trigger("error", err); rej(err); }); this._manifestFetcher.addEventListener("manifestReady", (manifest) => { res(manifest); }); })); this._manifestFetcher.start(); this._initCanceller.signal.register(() => { this._manifestFetcher.dispose(); }); } /** * @param {HTMLMediaElement} mediaElement * @param {Object} playbackObserver */ start(mediaElement, playbackObserver) { this.prepare(); // Load Manifest if not already done /** Translate errors coming from the media element into RxPlayer errors. */ listenToMediaError(mediaElement, (error) => this._onFatalError(error), this._initCanceller.signal); this._setupInitialMediaSourceAndDecryption(mediaElement) .then((initResult) => this._onInitialMediaSourceReady(mediaElement, initResult.mediaSource, playbackObserver, initResult.drmSystemId, initResult.unlinkMediaSource)) .catch((err) => { this._onFatalError(err); }); } /** * Update URL of the Manifest. * @param {Array.<string>|undefined} urls - URLs to reach that Manifest from * the most prioritized URL to the least prioritized URL. * @param {boolean} refreshNow - If `true` the resource in question (e.g. * DASH's MPD) will be refreshed immediately. */ updateContentUrls(urls, refreshNow) { this._manifestFetcher.updateContentUrls(urls, refreshNow); } /** * Stop content and free all resources linked to this * `MediaSourceContentInitializer`. */ dispose() { this._initCanceller.cancel(); } /** * Callback called when an error interrupting playback arised. * @param {*} err */ _onFatalError(err) { if (this._initCanceller.isUsed()) { return; } this._initCanceller.cancel(); this.trigger("error", err); } /** * Initialize decryption mechanisms if needed and begin creating and relying * on the initial `MediaSourceInterface` for this content. * @param {HTMLMediaElement|null} mediaElement * @returns {Promise.<Object>} */ _setupInitialMediaSourceAndDecryption(mediaElement) { const initCanceller = this._initCanceller; return createCancellablePromise(initCanceller.signal, (resolve) => { const { keySystems } = this._initSettings; /** Initialize decryption capabilities. */ const { statusRef: drmInitRef, contentDecryptor } = initializeContentDecryption(mediaElement, keySystems, { onWarning: (err) => this.trigger("warning", err), onError: (err) => this._onFatalError(err), onBlackListProtectionData: (val) => { // Ugly IIFE workaround to allow async event listener (async () => { var _a; if (this._manifest === null) { return; } const manifest = (_a = this._manifest.syncValue) !== null && _a !== void 0 ? _a : (await this._manifest.getValueAsAsync()); blackListProtectionDataOnManifest(manifest, val); })().catch(noop); }, onKeyIdsCompatibilityUpdate: (updates) => { // Ugly IIFE workaround to allow async event listener (async () => { var _a; if (this._manifest === null) { return; } const manifest = (_a = this._manifest.syncValue) !== null && _a !== void 0 ? _a : (await this._manifest.getValueAsAsync()); updateKeyIdsDecipherabilityOnManifest(manifest, updates.whitelistedKeyIds, updates.blacklistedKeyIds, updates.delistedKeyIds); })().catch(noop); }, onCodecSupportUpdate: () => { var _a, _b; const syncManifest = (_a = this._manifest) === null || _a === void 0 ? void 0 : _a.syncValue; if (isNullOrUndefined(syncManifest)) { // The Manifest is not yet fetched, but we will be able to check // the codecs once it is the case (_b = this._manifest) === null || _b === void 0 ? void 0 : _b.getValueAsAsync().then((loadedManifest) => { if (this._initCanceller.isUsed()) { return; } this._refreshManifestCodecSupport(loadedManifest); }, noop); } else { this._refreshManifestCodecSupport(syncManifest); } }, }, initCanceller.signal); if (contentDecryptor.enabled) { this._decryptionCapabilities = { status: "enabled", value: contentDecryptor.value, }; } else { this._decryptionCapabilities = { status: "disabled", value: contentDecryptor.value, }; } drmInitRef.onUpdate((drmStatus, stopListeningToDrmUpdates) => { if (drmStatus.initializationState.type === "uninitialized") { return; } stopListeningToDrmUpdates(); const mediaSourceCanceller = new TaskCanceller(); mediaSourceCanceller.linkToSignal(initCanceller.signal); createMediaSource(mediaElement, mediaSourceCanceller.signal) .then((mediaSource) => { const lastDrmStatus = drmInitRef.getValue(); if (lastDrmStatus.initializationState.type === "awaiting-media-link") { lastDrmStatus.initializationState.value.isMediaLinked.setValue(true); drmInitRef.onUpdate((newDrmStatus, stopListeningToDrmUpdatesAgain) => { if (newDrmStatus.initializationState.type === "initialized") { stopListeningToDrmUpdatesAgain(); resolve({ mediaSource, drmSystemId: newDrmStatus.drmSystemId, unlinkMediaSource: mediaSourceCanceller, }); return; } }, { emitCurrentValue: true, clearSignal: initCanceller.signal }); } else if (drmStatus.initializationState.type === "initialized") { resolve({ mediaSource, drmSystemId: drmStatus.drmSystemId, unlinkMediaSource: mediaSourceCanceller, }); return; } }) .catch((err) => { if (mediaSourceCanceller.isUsed()) { return; } this._onFatalError(err); }); }, { emitCurrentValue: true, clearSignal: initCanceller.signal }); }); } async _onInitialMediaSourceReady(mediaElement, initialMediaSource, playbackObserver, drmSystemId, initialMediaSourceCanceller) { var _a; const { adaptiveOptions, autoPlay, bufferOptions, lowLatencyMode, segmentRequestOptions, speed, startAt, textTrackOptions, transport, } = this._initSettings; const initCanceller = this._initCanceller; assert(this._manifest !== null); let manifest; try { manifest = (_a = this._manifest.syncValue) !== null && _a !== void 0 ? _a : (await this._manifest.getValueAsAsync()); } catch (_e) { return; // The error should already have been processed through an event listener } manifest.addEventListener("manifestUpdate", (updates) => { this.trigger("manifestUpdate", updates); this._refreshManifestCodecSupport(manifest); }, initCanceller.signal); manifest.addEventListener("decipherabilityUpdate", (elts) => { this.trigger("decipherabilityUpdate", elts); }, initCanceller.signal); manifest.addEventListener("supportUpdate", () => { this.trigger("codecSupportUpdate", null); }, initCanceller.signal); log.debug("Init: Calculating initial time"); const initialTime = getInitialTime(manifest, lowLatencyMode, startAt); log.debug("Init: Initial time calculated:", initialTime); /** Choose the right "Representation" for a given "Adaptation". */ const representationEstimator = AdaptiveRepresentationSelector(adaptiveOptions); const subBufferOptions = objectAssign({ textTrackOptions, drmSystemId }, bufferOptions); const cdnPrioritizer = new CdnPrioritizer(initCanceller.signal); const segmentQueueCreator = new SegmentQueueCreator(transport, cdnPrioritizer, this._cmcdDataBuilder, segmentRequestOptions); this._refreshManifestCodecSupport(manifest); this.trigger("manifestReady", manifest); if (initCanceller.isUsed()) { return; } // handle initial load and reloads this._setupContentWithNewMediaSource({ mediaElement, playbackObserver, mediaSource: initialMediaSource, initialTime, autoPlay, manifest, representationEstimator, cdnPrioritizer, segmentQueueCreator, speed, bufferOptions: subBufferOptions, }, initialMediaSourceCanceller); } /** * Load the content defined by the Manifest in the mediaSource given at the * given position and playing status. * This function recursively re-call itself when a MediaSource reload is * wanted. * @param {Object} args * @param {Object} currentCanceller */ _setupContentWithNewMediaSource(args, currentCanceller) { this._startLoadingContentOnMediaSource(args, this._createReloadMediaSourceCallback(args, currentCanceller), currentCanceller.signal); } /** * Create `IReloadMediaSourceCallback` allowing to handle reload orders. * @param {Object} args * @param {Object} currentCanceller */ _createReloadMediaSourceCallback(args, currentCanceller) { const initCanceller = this._initCanceller; return (reloadOrder) => { currentCanceller.cancel(); if (initCanceller.isUsed()) { return; } this.trigger("reloadingMediaSource", reloadOrder); if (initCanceller.isUsed()) { return; } const newCanceller = new TaskCanceller(); newCanceller.linkToSignal(initCanceller.signal); createMediaSource(args.mediaElement, newCanceller.signal) .then((newMediaSource) => { this._setupContentWithNewMediaSource(Object.assign(Object.assign({}, args), { mediaSource: newMediaSource, initialTime: reloadOrder.position, autoPlay: reloadOrder.autoPlay }), newCanceller); }) .catch((err) => { if (newCanceller.isUsed()) { return; } this._onFatalError(err); }); }; } /** * Buffer the content on the given MediaSource. * @param {Object} args * @param {function} onReloadOrder * @param {Object} cancelSignal */ _startLoadingContentOnMediaSource(args, onReloadOrder, cancelSignal) { var _a, _b; const { autoPlay, bufferOptions, initialTime, manifest, mediaElement, mediaSource, playbackObserver, representationEstimator, cdnPrioritizer, segmentQueueCreator, speed, } = args; const { transport } = this._initSettings; const initialPeriod = (_a = manifest.getPeriodForTime(initialTime)) !== null && _a !== void 0 ? _a : manifest.getNextPeriod(initialTime); if (initialPeriod === undefined) { const error = new MediaError("MEDIA_STARTING_TIME_NOT_FOUND", "Wanted starting time not found in the Manifest."); return this._onFatalError(error); } let textDisplayerInterface = null; const textDisplayer = createTextDisplayer(mediaElement, this._initSettings.textTrackOptions); if (textDisplayer !== null) { const sender = new MainThreadTextDisplayerInterface(textDisplayer); textDisplayerInterface = sender; cancelSignal.register(() => { sender.stop(); textDisplayer === null || textDisplayer === void 0 ? void 0 : textDisplayer.stop(); }); } /** Interface to create media buffers. */ const segmentSinksStore = new SegmentSinksStore(mediaSource, mediaElement.nodeName === "VIDEO", textDisplayerInterface); cancelSignal.register(() => { segmentSinksStore.disposeAll(); }); const { autoPlayResult, initialPlayPerformed } = performInitialSeekAndPlay({ mediaElement, playbackObserver, startTime: initialTime, mustAutoPlay: autoPlay, onWarning: (err) => { this.trigger("warning", err); }, isDirectfile: false, }, cancelSignal); if (cancelSignal.isCancelled()) { return; } initialPlayPerformed.onUpdate((isPerformed, stopListening) => { if (isPerformed) { stopListening(); const streamEventsEmitter = new StreamEventsEmitter(manifest, playbackObserver); manifest.addEventListener("manifestUpdate", () => { streamEventsEmitter.onManifestUpdate(manifest); }, cancelSignal); streamEventsEmitter.addEventListener("event", (payload) => { this.trigger("streamEvent", payload); }, cancelSignal); streamEventsEmitter.addEventListener("eventSkip", (payload) => { this.trigger("streamEventSkip", payload); }, cancelSignal); streamEventsEmitter.start(); cancelSignal.register(() => { streamEventsEmitter.stop(); }); } }, { clearSignal: cancelSignal, emitCurrentValue: true }); const coreObserver = createCorePlaybackObserver(playbackObserver, { autoPlay, manifest, mediaSource, textDisplayer, initialPlayPerformed, speed, }, cancelSignal); (_b = this._cmcdDataBuilder) === null || _b === void 0 ? void 0 : _b.startMonitoringPlayback(coreObserver); cancelSignal.register(() => { var _a; (_a = this._cmcdDataBuilder) === null || _a === void 0 ? void 0 : _a.stopMonitoringPlayback(); }); const rebufferingController = this._createRebufferingController(playbackObserver, manifest, speed, cancelSignal); const freezeResolver = new FreezeResolver(segmentSinksStore); if (mayMediaElementFailOnUndecipherableData) { // On some devices, just reload immediately when data become undecipherable manifest.addEventListener("decipherabilityUpdate", (elts) => { if (elts.some((e) => e.representation.decipherable !== true)) { reloadMediaSource(0, undefined, undefined); } }, cancelSignal); } coreObserver.listen((observation) => { synchronizeSegmentSinksOnObservation(observation, segmentSinksStore); const freezeResolution = freezeResolver.onNewObservation(observation); if (freezeResolution === null) { return; } // TODO: The following method looks generic, we may be able to factorize // it with other reload handlers after some work. const triggerReload = () => { var _a; const lastObservation = playbackObserver.getReference().getValue(); const position = lastObservation.position.isAwaitingFuturePosition() ? lastObservation.position.getWanted() : ((_a = coreObserver.getCurrentTime()) !== null && _a !== void 0 ? _a : lastObservation.position.getPolled()); const autoplay = initialPlayPerformed.getValue() ? !playbackObserver.getIsPaused() : autoPlay; onReloadOrder({ position, autoPlay: autoplay }); }; handleFreezeResolution(freezeResolution, { enableRepresentationAvoidance: this._initSettings.enableRepresentationAvoidance, manifest, triggerReload, playbackObserver, }); }, { clearSignal: cancelSignal }); const contentTimeBoundariesObserver = createContentTimeBoundariesObserver(manifest, mediaSource, coreObserver, segmentSinksStore, { onWarning: (err) => this.trigger("warning", err), onPeriodChanged: (period) => this.trigger("activePeriodChanged", { period }), }, cancelSignal); /** * Emit a "loaded" events once the initial play has been performed and the * media can begin playback. * Also emits warning events if issues arise when doing so. */ autoPlayResult .then(() => { getLoadedReference(playbackObserver, false, cancelSignal).onUpdate((isLoaded, stopListening) => { if (isLoaded) { stopListening(); this.trigger("loaded", { getSegmentSinkMetrics: async () => { return new Promise((resolve) => resolve(segmentSinksStore.getSegmentSinksMetrics())); }, getThumbnailData: async (periodId, thumbnailTrackId, time) => { const fetchThumbnails = createThumbnailFetcher(transport.thumbnails, cdnPrioritizer); return getThumbnailData(fetchThumbnails, manifest, periodId, thumbnailTrackId, time); }, }); } }, { emitCurrentValue: true, clearSignal: cancelSignal }); }) .catch((err) => { if (cancelSignal.isCancelled()) { return; // Current loading cancelled, no need to trigger the error } this._onFatalError(err); }); // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; StreamOrchestrator({ manifest, initialPeriod }, coreObserver, representationEstimator, segmentSinksStore, segmentQueueCreator, bufferOptions, handleStreamOrchestratorCallbacks(), cancelSignal); /** * Returns Object handling the callbacks from a `StreamOrchestrator`, which * are basically how it communicates about events. * @returns {Object} */ function handleStreamOrchestratorCallbacks() { return { needsBufferFlush: (payload) => { var _a; let wantedSeekingTime; const lastObservation = playbackObserver.getReference().getValue(); const currentTime = lastObservation.position.isAwaitingFuturePosition() ? lastObservation.position.getWanted() : mediaElement.currentTime; const relativeResumingPosition = (_a = payload === null || payload === void 0 ? void 0 : payload.relativeResumingPosition) !== null && _a !== void 0 ? _a : 0; const canBeApproximateSeek = Boolean(payload === null || payload === void 0 ? void 0 : payload.relativePosHasBeenDefaulted); if (relativeResumingPosition === 0 && canBeApproximateSeek) { // in case relativeResumingPosition is 0, we still perform // a tiny seek to be sure that the browser will correclty reload the video. wantedSeekingTime = currentTime + 0.001; } else { wantedSeekingTime = currentTime + relativeResumingPosition; } playbackObserver.setCurrentTime(wantedSeekingTime); // Seek again once data begins to be buffered. // This is sadly necessary on some browsers to avoid decoding // issues after a flush. // // NOTE: there's in theory a potential race condition in the following // logic as the callback could be called when media data is still // being removed by the browser - which is an asynchronous process. // The following condition checking for buffered data could thus lead // to a false positive where we're actually checking previous data. // For now, such scenario is avoided by setting the // `includeLastObservation` option to `false` and calling // `needsBufferFlush` once MSE media removal operations have been // explicitely validated by the browser, but that's a complex and easy // to break system. playbackObserver.listen((obs, stopListening) => { if ( // Data is buffered around the current position obs.currentRange !== null || // Or, for whatever reason, we have no buffer but we're already advancing obs.position.getPolled() > wantedSeekingTime + 0.1) { stopListening(); playbackObserver.setCurrentTime(obs.position.getWanted() + 0.001); } }, { includeLastObservation: false, clearSignal: cancelSignal }); }, streamStatusUpdate(value) { // Announce discontinuities if found const { period, bufferType, imminentDiscontinuity, position } = value; rebufferingController.updateDiscontinuityInfo({ period, bufferType, discontinuity: imminentDiscontinuity, position, }); if (cancelSignal.isCancelled()) { return; // Previous call has stopped streams due to a side-effect } // If the status for the last Period indicates that segments are all loaded // or on the contrary that the loading resumed, announce it to the // ContentTimeBoundariesObserver. if (manifest.isLastPeriodKnown && value.period.id === manifest.periods[manifest.periods.length - 1].id) { const hasFinishedLoadingLastPeriod = value.hasFinishedLoading || value.isEmptyStream; if (hasFinishedLoadingLastPeriod) { contentTimeBoundariesObserver.onLastSegmentFinishedLoading(value.bufferType); } else { contentTimeBoundariesObserver.onLastSegmentLoadingResume(value.bufferType); } } }, needsManifestRefresh: () => self._manifestFetcher.scheduleManualRefresh({ enablePartialRefresh: true, canUseUnsafeMode: true, }), manifestMightBeOufOfSync: () => { const { OUT_OF_SYNC_MANIFEST_REFRESH_DELAY } = config.getCurrent(); self._manifestFetcher.scheduleManualRefresh({ enablePartialRefresh: false, canUseUnsafeMode: false, delay: OUT_OF_SYNC_MANIFEST_REFRESH_DELAY, }); }, lockedStream: (value) => rebufferingController.onLockedStream(value.bufferType, value.period), adaptationChange: (value) => { self.trigger("adaptationChange", value); if (cancelSignal.isCancelled()) { return; // Previous call has stopped streams due to a side-effect } contentTimeBoundariesObserver.onAdaptationChange(value.type, value.period, value.adaptation); }, representationChange: (value) => { self.trigger("representationChange", value); if (cancelSignal.isCancelled()) { return; // Previous call has stopped streams due to a side-effect } contentTimeBoundariesObserver.onRepresentationChange(value.type, value.period); }, inbandEvent: (value) => self.trigger("inbandEvents", value), warning: (value) => self.trigger("warning", value), periodStreamReady: (value) => self.trigger("periodStreamReady", value), periodStreamCleared: (value) => { contentTimeBoundariesObserver.onPeriodCleared(value.type, value.period); if (cancelSignal.isCancelled()) { return; // Previous call has stopped streams due to a side-effect } self.trigger("periodStreamCleared", { type: value.type, periodId: value.period.id, }); }, bitrateEstimateChange: (value) => { var _a; (_a = self._cmcdDataBuilder) === null || _a === void 0 ? void 0 : _a.updateThroughput(value.type, value.bitrate); self.trigger("bitrateEstimateChange", value); }, needsMediaSourceReload: (payload) => { reloadMediaSource(payload.timeOffset, payload.minimumPosition, payload.maximumPosition); }, needsDecipherabilityFlush() { var _a, _b, _c, _d; const keySystem = getKeySystemConfiguration(mediaElement); if (shouldReloadMediaSourceOnDecipherabilityUpdate(keySystem === null || keySystem === void 0 ? void 0 : keySystem[0])) { const lastObservation = coreObserver.getReference().getValue(); const position = lastObservation.position.isAwaitingFuturePosition() ? lastObservation.position.getWanted() : ((_a = coreObserver.getCurrentTime()) !== null && _a !== void 0 ? _a : lastObservation.position.getPolled()); const isPaused = (_c = (_b = lastObservation.paused.pending) !== null && _b !== void 0 ? _b : coreObserver.getIsPaused()) !== null && _c !== void 0 ? _c : lastObservation.paused.last; onReloadOrder({ position, autoPlay: !isPaused }); } else { const lastObservation = coreObserver.getReference().getValue(); const position = lastObservation.position.isAwaitingFuturePosition() ? lastObservation.position.getWanted() : ((_d = coreObserver.getCurrentTime()) !== null && _d !== void 0 ? _d : lastObservation.position.getPolled()); // simple seek close to the current position // to flush the buffers if (position + 0.001 < lastObservation.duration) { playbackObserver.setCurrentTime(mediaElement.currentTime + 0.001); } else { playbackObserver.setCurrentTime(position); } } }, encryptionDataEncountered: (value) => { if (self._decryptionCapabilities.status === "disabled") { self._onFatalError(self._decryptionCapabilities.value); return; } else if (self._decryptionCapabilities.status === "uninitialized") { // Should never happen log.error("Init: received encryption data without known decryption capabilities"); return; } for (const protectionData of value) { self._decryptionCapabilities.value.onInitializationData(protectionData); if (cancelSignal.isCancelled()) { return; // Previous call has stopped streams due to a side-effect } } }, error: (err) => self._onFatalError(err), }; } /** * Callback allowing to reload the current content. * @param {number} deltaPosition - Position you want to seek to after * reloading, as a delta in seconds from the last polled playing position. * @param {number|undefined} minimumPosition - If set, minimum time bound * in seconds after `deltaPosition` has been applied. * @param {number|undefined} maximumPosition - If set, minimum time bound * in seconds after `deltaPosition` has been applied. */ function reloadMediaSource(deltaPosition, minimumPosition, maximumPosition) { var _a, _b, _c; const lastObservation = coreObserver.getReference().getValue(); const currentPosition = lastObservation.position.isAwaitingFuturePosition() ? lastObservation.position.getWanted() : ((_a = coreObserver.getCurrentTime()) !== null && _a !== void 0 ? _a : lastObservation.position.getPolled()); const isPaused = (_c = (_b = lastObservation.paused.pending) !== null && _b !== void 0 ? _b : coreObserver.getIsPaused()) !== null && _c !== void 0 ? _c : lastObservation.paused.last; let position = currentPosition + deltaPosition; if (minimumPosition !== undefined) { position = Math.max(minimumPosition, position); } if (maximumPosition !== undefined) { position = Math.min(maximumPosition, position); } onReloadOrder({ position, autoPlay: !isPaused }); } } /** * Creates a `RebufferingController`, a class trying to avoid various stalling * situations (such as rebuffering periods), and returns it. * * Various methods from that class need then to be called at various events * (see `RebufferingController` definition). * * This function also handles the `RebufferingController`'s events: * - emit "stalled" events when stalling situations cannot be prevented, * - emit "unstalled" events when we could get out of one, * - emit "warning" on various rebuffering-related minor issues * like discontinuity skipping. * @param {Object} playbackObserver * @param {Object} manifest * @param {Object} speed * @param {Object} cancelSignal * @returns {Object} */ _createRebufferingController(playbackObserver, manifest, speed, cancelSignal) { const rebufferingController = new RebufferingController(playbackObserver, manifest, speed); // Bubble-up events rebufferingController.addEventListener("stalled", (evt) => this.trigger("stalled", evt)); rebufferingController.addEventListener("unstalled", () => this.trigger("unstalled", null)); rebufferingController.addEventListener("warning", (err) => this.trigger("warning", err)); cancelSignal.register(() => rebufferingController.destroy()); rebufferingController.start(); return rebufferingController; } /** * Evaluates a list of codecs to determine their support status. * * @param {Array} codecsToCheck - The list of codecs to check. * @returns {Array} - The list of evaluated codecs with their support status updated. */ getCodecsSupportInfo(codecsToCheck) { const codecsSupportInfo = codecsToCheck.map((codecToCheck) => { var _a; const inputCodec = `${codecToCheck.mimeType};codecs="${codecToCheck.codec}"`; const isSupported = isCodecSupported(inputCodec); if (!isSupported) { return { mimeType: codecToCheck.mimeType, codec: codecToCheck.codec, supported: false, supportedIfEncrypted: false, }; } /** * `true` if the codec is supported when encrypted, `false` if it is not * supported, or `undefined` if we cannot obtain that information. */ let supportedIfEncrypted; if (this._decryptionCapabilities.status === "uninitialized") { supportedIfEncrypted = undefined; } else if (this._decryptionCapabilities.status === "disabled") { // It's ambiguous here, but let's say that no ContentDecryptor means that // the codec is supported by it. supportedIfEncrypted = true; } else { const contentDecryptor = this._decryptionCapabilities.value; if (contentDecryptor.getState() !== ContentDecryptorState.Initializing) { // No information is available regarding the support status. // Defaulting to assume the codec is supported. supportedIfEncrypted = (_a = contentDecryptor.isCodecSupported(codecToCheck.mimeType, codecToCheck.codec)) !== null && _a !== void 0 ? _a : true; } } return { mimeType: codecToCheck.mimeType, codec: codecToCheck.codec, supported: isSupported, supportedIfEncrypted, }; }); return codecsSupportInfo; } /** * Update the support status of all Representations in the Manifest. * * To call anytime either the Manifest is linked to new codecs or new means * to test for codec support are available. * @param {Object} manifest */ _refreshManifestCodecSupport(manifest) { const codecsToTest = manifest.getCodecsWithUnknownSupport(); const codecsSupportInfo = this.getCodecsSupportInfo(codecsToTest); if (codecsSupportInfo.length > 0) { try { manifest.updateCodecSupport(codecsSupportInfo); } catch (err) { this._onFatalError(err); } } } } function createTextDisplayer(mediaElement, textTrackOptions) { if (textTrackOptions.textTrackMode === "html" && features.htmlTextDisplayer !== null) { return new features.htmlTextDisplayer(mediaElement, textTrackOptions.textTrackElement); } else if (features.nativeTextDisplayer !== null) { return new features.nativeTextDisplayer(mediaElement); } return null; } /** * Change the decipherability of Representations which have their key id in one * of the given Arrays: * * - Those who have a key id listed in `whitelistedKeyIds` will have their * decipherability updated to `true` * * - Those who have a key id listed in `blacklistedKeyIds` will have their * decipherability updated to `false` * * - Those who have a key id listed in `delistedKeyIds` will have their * decipherability updated to `undefined`. * * @param {Object} manifest * @param {Array.<Uint8Array>} whitelistedKeyIds * @param {Array.<Uint8Array>} blacklistedKeyIds * @param {Array.<Uint8Array>} delistedKeyIds */ function updateKeyIdsDecipherabilityOnManifest(manifest, whitelistedKeyIds, blacklistedKeyIds, delistedKeyIds) { manifest.updateRepresentationsDeciperability((ctx) => { const { representation } = ctx; if (representation.contentProtections === undefined) { return representation.decipherable; } const contentKIDs = representation.contentProtections.keyIds; if (contentKIDs !== undefined) { for (const elt of contentKIDs) { for (const blacklistedKeyId of blacklistedKeyIds) { if (areArraysOfNumbersEqual(blacklistedKeyId, elt)) { return false; } } for (const whitelistedKeyId of whitelistedKeyIds) { if (areArraysOfNumbersEqual(whitelistedKeyId, elt)) { return true; } } for (const delistedKeyId of delistedKeyIds) { if (areArraysOfNumbersEqual(delistedKeyId, elt)) { return undefined; } } } } return representation.decipherable; }); } /** * Update decipherability to `false` to any Representation which is linked to * the given initialization data. * @param {Object} manifest * @param {Object} initData */ function blackListProtectionDataOnManifest(manifest, initData) { manifest.updateRepresentationsDeciperability((ctx) => { var _a, _b; const rep = ctx.representation; if (rep.decipherable === false) { return false; } const segmentProtections = (_b = (_a = rep.contentProtections) === null || _a === void 0 ? void 0 : _a.initData) !== null && _b !== void 0 ? _b : []; for (const protection of segmentProtections) { if (initData.type === undefined || protection.type === initData.type) { const containedInitData = initData.values .getFormattedValues() .every((undecipherableVal) => { return protection.values.some((currVal) => { return ((undecipherableVal.systemId === undefined || currVal.systemId === undecipherableVal.systemId) && areArraysOfNumbersEqual(currVal.data, undecipherableVal.data)); }); }); if (containedInitData) { return false; } } } return rep.decipherable; }); } /** * Handle accordingly an `IFreezeResolution` object. * @param {Object|null} freezeResolution - The `IFreezeResolution` suggested. * @param {Object} param - Parameters that might be needed to implement the * resolution. * @param {Object} param.manifest - The current content's Manifest object. * @param {Object} param.playbackObserver - Object regularly emitting playback * conditions. * @param {Function} param.triggerReload - Function to call if we need to ask * for a "MediaSource reload". * @param {Boolean} param.enableRepresentationAvoidance - If `true`, this * function is authorized to mark `Representation` as "to avoid" if the * `IFreezeResolution` object suggest it. */ function handleFreezeResolution(freezeResolution, { playbackObserver, enableRepresentationAvoidance, manifest, triggerReload, }) { switch (freezeResolution.type) { case "reload": { log.info("Init: Planning reload due to freeze"); triggerReload(); break; } case "flush": { log.info("Init: Flushing buffer due to freeze"); const observation = playbackObserver.getReference().getValue(); const currentTime = observation.position.isAwaitingFuturePosition() ? observation.position.getWanted() : playbackObserver.getCurrentTime(); const relativeResumingPosition = freezeResolution.value.relativeSeek; const wantedSeekingTime = currentTime + relativeResumingPosition; playbackObserver.setCurrentTime(wantedSeekingTime); break; } case "avoid-representations": { const contents = freezeResolution.value; if (enableRepresentationAvoidance) { manifest.addRepresentationsToAvoid(contents); } triggerReload(); break; } default: assertUnreachable(freezeResolution); } }