/** * 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 type { IStreamOrchestratorPlaybackObservation, IBufferType, } from "../../core/types"; import { MediaError } from "../../errors"; import type { IManifest, IAdaptation, IRepresentationIndex, IPeriod, } from "../../manifest"; import type { IReadOnlyPlaybackObserver } from "../../playback_observer"; import type { IPlayerError } from "../../public_types"; import arrayIncludes from "../../utils/array_includes"; import EventEmitter from "../../utils/event_emitter"; import isNullOrUndefined from "../../utils/is_null_or_undefined"; import queueMicrotask from "../../utils/queue_microtask"; import SortedList from "../../utils/sorted_list"; import TaskCanceller from "../../utils/task_canceller"; /** * Observes what's being played and take care of media events relating to time * boundaries: * - Emits a `endingPositionChange` when the known maximum playable position * of the current content is known and every time it changes. * - Emits `endOfStream` API once segments have been pushed until the end and * `resumeStream` if downloads starts back. * - Emits a `periodChange` event when the currently-playing Period seemed to * have changed. * - emit "warning" events when what is being played is outside of the * Manifest range. * @class ContentTimeBoundariesObserver */ // `HTMLMediaElement.currentTime` can be off by a very small amount from the // Manifest-derived boundaries. We keep an epsilon so those rounding // differences do not trigger spurious boundary warnings. const EPSILON = 1e-3; export default class ContentTimeBoundariesObserver extends EventEmitter { /** Allows to interrupt everything the `ContentTimeBoundariesObserver` is doing. */ private _canceller: TaskCanceller; /** Store information on every created "Streams". */ private _activeStreams: Map; /** The `Manifest` object linked to the current content. */ private _manifest: IManifest; /** Allows to calculate at any time maximum positions of the content */ private _maximumPositionCalculator: MaximumPositionCalculator; /** Enumerate all possible buffer types in the current content. */ private _allBufferTypes: IBufferType[]; /** * Stores the `id` property of the last Period for which a `periodChange` * event has been sent. * Allows to avoid multiple times in a row `periodChange` for the same * Period. */ private _lastCurrentPeriodId: string | null; /** * @param {Object} manifest * @param {Object} playbackObserver */ constructor( manifest: IManifest, playbackObserver: IReadOnlyPlaybackObserver, bufferTypes: IBufferType[], ) { super(); this._canceller = new TaskCanceller("Boundaries Observation"); this._manifest = manifest; this._activeStreams = new Map(); this._allBufferTypes = bufferTypes; this._lastCurrentPeriodId = null; /** * Allows to calculate the minimum and maximum playable position on the * whole content. */ const maximumPositionCalculator = new MaximumPositionCalculator(manifest); // Indicate directly that no Adaptation of a particular type will be set if (!arrayIncludes(this._allBufferTypes, "video")) { maximumPositionCalculator.updateLastVideoAdaptation(null); } if (!arrayIncludes(this._allBufferTypes, "audio")) { maximumPositionCalculator.updateLastAudioAdaptation(null); } this._maximumPositionCalculator = maximumPositionCalculator; const cancelSignal = this._canceller.signal; // As the following code may send events synchronously, which would not be // catchable as a caller could not have called `addEventListener` yet, // we schedule it in a micro-task queueMicrotask(() => { playbackObserver.listen( ({ position }) => { const wantedPosition = position.getWanted(); const minimumPosition = manifest.getMinimumSafePosition(); const maximumPosition = maximumPositionCalculator.getMaximumAvailablePosition(); // Only report "before manifest" when the position is meaningfully // behind the minimum, not when `