UNPKG

rx-player

Version:
220 lines (219 loc) 9 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 config from "../../../../config"; import EventEmitter from "../../../../utils/event_emitter"; import SharedReference from "../../../../utils/reference"; import TaskCanceller from "../../../../utils/task_canceller"; import refreshScheduledEventsList from "./refresh_scheduled_events_list"; /** * Get events from manifest and emit each time an event has to be emitted */ export default class StreamEventsEmitter extends EventEmitter { /** * @param {Object} playbackObserver */ constructor(playbackObserver) { super(); this._playbackObserver = playbackObserver; this._canceller = null; this._scheduledEventsRef = new SharedReference([]); this._eventsBeingPlayed = new WeakMap(); this._isPaused = true; this._previousObservation = null; } /** * Initialize the emitter for the given content manifest. * * The emitter starts in a paused state so the caller can resume it only once * the corresponding media instance is actually ready. * @param {Object} manifest */ start(manifest) { if (this._canceller !== null) { return; } this._canceller = new TaskCanceller("StreamEventsEmitter"); const cancelSignal = this._canceller.signal; this._isPaused = true; this._previousObservation = null; this._eventsBeingPlayed = new WeakMap(); let isPollingEvents = false; let cancelCurrentPolling = new TaskCanceller("StreamEventsEmitter Polling"); cancelCurrentPolling.linkToSignal(cancelSignal); this._scheduledEventsRef.setValue(refreshScheduledEventsList([], manifest)); this._scheduledEventsRef.onUpdate(({ length: scheduledEventsLength }) => { if (scheduledEventsLength === 0) { if (isPollingEvents) { cancelCurrentPolling.cancel("no stream event"); cancelCurrentPolling = new TaskCanceller("StreamEventsEmitter Polling"); cancelCurrentPolling.linkToSignal(cancelSignal); isPollingEvents = false; } return; } else if (isPollingEvents) { return; } isPollingEvents = true; const checkStreamEvents = () => { if (this._isPaused) { return; } const newObservation = this._constructObservation(); if (this._previousObservation === null) { this._previousObservation = newObservation; return; } this._emitStreamEvents(this._scheduledEventsRef.getValue(), this._previousObservation, newObservation, cancelCurrentPolling.signal); this._previousObservation = newObservation; }; const { STREAM_EVENT_EMITTER_POLL_INTERVAL } = config.getCurrent(); const intervalId = setInterval(checkStreamEvents, STREAM_EVENT_EMITTER_POLL_INTERVAL); this._playbackObserver.listen(checkStreamEvents, { includeLastObservation: false, clearSignal: cancelCurrentPolling.signal, }); cancelCurrentPolling.signal.register(() => { clearInterval(intervalId); }); }, { emitCurrentValue: true, clearSignal: cancelSignal }); } /** * Suspend event evaluation until `resume` has been called. * * To use when playback metrics should be temporarily ignored, for example * while reloading a content. */ pause() { this._isPaused = true; this._previousObservation = null; } /** * Resume event evaluation from the current playback state. */ resume() { this._isPaused = false; // Take a snapshot right now to avoid comparing a post-reload position // with a stale pre-reload observation. this._previousObservation = this._constructObservation(); } /** Refresh the tracked stream-event list after a manifest update. */ onManifestUpdate(man) { const prev = this._scheduledEventsRef.getValue(); this._scheduledEventsRef.setValue(refreshScheduledEventsList(prev, man)); } /** * @param {string | undefined} reason - Human-inspectable reason behind the * stop. Used for debugging matters, especially for debug log * inspection. */ stop(reason) { if (this._canceller !== null) { this._canceller.cancel(reason !== null && reason !== void 0 ? reason : "StreamEventsEmitter stop"); this._canceller = null; this._previousObservation = null; this._eventsBeingPlayed = new WeakMap(); } } /** * Construct observation object relied on by the `StreamEventsEmitted` by * generating it from this instance's `PlaybackObserver`. */ _constructObservation() { var _a; const lastObservation = this._playbackObserver.getReference().getValue(); const currentTime = (_a = this._playbackObserver.getCurrentTime()) !== null && _a !== void 0 ? _a : this._playbackObserver.getReference().getValue().position.getPolled(); const isSeeking = lastObservation.seeking !== 0 /* SeekingState.None */; return { currentTime, isSeeking }; } /** * Examine playback situation from playback observations to emit stream events and * prepare `onExit` callbacks if needed. * @param {Array.<Object>} scheduledEvents * @param {Object} oldObservation * @param {Object} newObservation * @param {Object} stopSignal */ _emitStreamEvents(scheduledEvents, oldObservation, newObservation, stopSignal) { const { currentTime: previousTime } = oldObservation; const { isSeeking, currentTime } = newObservation; const eventsToSend = []; const eventsToExit = []; for (let i = 0; i < scheduledEvents.length; i++) { const event = scheduledEvents[i]; const start = event.start; const end = isFiniteStreamEvent(event) ? event.end : undefined; const isBeingPlayed = this._eventsBeingPlayed.has(event); if (isBeingPlayed) { if (start > currentTime || (end !== undefined && currentTime >= end)) { if (isFiniteStreamEvent(event)) { eventsToExit.push(event.publicEvent); } this._eventsBeingPlayed.delete(event); } } else if (start <= currentTime && end !== undefined && currentTime < end) { eventsToSend.push({ type: "stream-event", value: event.publicEvent }); this._eventsBeingPlayed.set(event, true); } else if (previousTime < start && currentTime >= (end !== null && end !== void 0 ? end : start)) { if (isSeeking) { eventsToSend.push({ type: "stream-event-skip", value: event.publicEvent, }); } else { eventsToSend.push({ type: "stream-event", value: event.publicEvent }); if (isFiniteStreamEvent(event)) { eventsToExit.push(event.publicEvent); } } } } if (eventsToSend.length > 0) { for (const event of eventsToSend) { if (event.type === "stream-event") { this.trigger("event", event.value); } else { this.trigger("eventSkip", event.value); } if (stopSignal.isCancelled()) { return; } } } if (eventsToExit.length > 0) { for (const event of eventsToExit) { if (typeof event.onExit === "function") { event.onExit(); } if (stopSignal.isCancelled()) { return; } } } } } /** * Tells if a stream event has a duration * @param {Object} evt * @returns {Boolean} */ function isFiniteStreamEvent(evt) { return evt.end !== undefined; }