UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

490 lines (384 loc) • 12.8 kB
import * as R from "ramda"; import { subscriber } from "../../functionUtils"; import { utilsLogger, createLogger } from "../../logger"; import { Player } from "./player"; import { hasTizenAPIs, subscribeToDeviceNetworkChanges, unsubscribeToDeviceNetworkChanges, } from "@applicaster/quick-brick-core/App/NetworkStatusProvider/utils"; import { setUserLanguagePreference } from "./userLanguagePreference"; import { PlayerRole } from "./conts"; import { RNPlayerManager } from "./nativePlayerManager"; const { log_error, log_debug, log_warning } = createLogger({ category: "General", subsystem: "PlayerManager", parent: utilsLogger, }); const loggedMethods = {}; function deprecationWarning(method) { if (!loggedMethods[method]) { log_warning( `${method} is being deprecated. Please use playerInstanceController.${method} instead.` ); loggedMethods[method] = true; } } export interface PlayerLifecycleListener { onRegistered?: (player: Player) => void; onUnRegistered?: (player: Player) => void; } type PlayersMap = { [id: string]: { player: Player; isInViewport: boolean; }; }; export class PlayerManager { private static instance: PlayerManager; private activePlayerId: string; private playersMap: PlayersMap = {}; private manager = subscriber(); private castingReceiver: Player; private lifecycleListeners: PlayerLifecycleListener[] = []; public static getInstance() { if (!PlayerManager.instance) { PlayerManager.instance = new PlayerManager(); } return PlayerManager.instance; } public getActivePlayerId = (): string | null => { return this.activePlayerId; }; public getCastingReceiver = (): Player | null => this.castingReceiver || null; public getActivePlayer = (): Player | null => { return this.playersMap[this.activePlayerId]?.player; }; public getPlayerWithId(id?: string): Player | null { if (!id) return null; return this.playersMap[id]?.player; } public isActivePlayer(id: string) { return id === this.activePlayerId; } public isCasting() { return !!this.castingReceiver; } // TODO: this should not call play on native / web players before they are initialized // remove the call to this.play() or check for initialization private setActivePlayerAndPlayIfNeed(id: string) { if (!this.activePlayerId) { this.play(id); } else { const activePlayer = this.getActivePlayer(); // if registering non cell player - it will be active if (activePlayer) { const isActivePlayerOfCellType = activePlayer?.isCellPlayer(); if (isActivePlayerOfCellType) { this.pause(this.activePlayerId); this.play(id); } } } } private addPlayer = (id: string, player: Player) => { this.playersMap[id] = { player: player, isInViewport: false, }; }; public isPlayerRegistered = (id: string) => !!this.playersMap[id]; private getPlayerListenerId = (id: string) => `player-id-${id}`; /** * Callback to handle device network changes. This method is a way to consume the native network state. * TODO: we should restore previous playback state onNetworkChange, not just play */ public onNetworkChange = (deviceStatus) => { const online = deviceStatus.online; const player = this.getInstanceController(); if (online && player?.isPaused()) { log_debug("Resuming playback because connection restored", { deviceStatus, }); this.play(); } else if (!online && !player?.isPaused()) { log_debug("Pausing playback because connection lost", { deviceStatus }); this.pause(); } }; /** * Register to external events that should affect player behavior * i.e. network changes, or visibility changes */ public registerEvents = () => { if (hasTizenAPIs()) { subscribeToDeviceNetworkChanges(this.onNetworkChange); } }; /** * Unregister any external events that should affect player behavior */ public unregisterEvents = () => { if (hasTizenAPIs()) { unsubscribeToDeviceNetworkChanges(); } }; public registerPlayer = ({ id, playerController, }: { id: string; playerController: Player; }) => { if (this.isPlayerRegistered(id)) { log_error(`Player with ${id} has already registered`); return this.manager; } this.addPlayer(id, playerController); if (playerController.playerRole === PlayerRole.Primary) { if (RNPlayerManager) { Object.keys(RNPlayerManager).forEach((pluginId) => { if (pluginId !== playerController.playerPluginId) { RNPlayerManager[pluginId]?.stopBackgroundPlayback?.(); } }); } playerController.addListener({ id: this.getPlayerListenerId(id), listener: { onVideoEnd: (event) => this.invokeHandler("ended", event), onError: (event) => this.invokeHandler("error", event), onLoad: (event) => this.invokeHandler("load", event), onVideoLoad: (event) => this.invokeHandler("videoLoad", event), onPlayerPause: (event) => this.invokeHandler("pause", event), onPlayerResume: (event) => this.invokeHandler("play", event), onVideoProgress: (event) => this.invokeHandler("timeupdate", event), onTracksChanged: (event) => this.invokeHandler("tracksChanged", event), }, }); this.setActivePlayerAndPlayIfNeed(id); } this.lifecycleListeners.forEach((listener) => { listener.onRegistered?.(playerController); }); // On initialization register to any events needed for the players // i.e. register to network changes that affect playback // TODO: not sure if this is the best place for this after player refactor this.registerEvents(); return this.manager; }; public unregisterPlayer(id: string) { const playerListenerId = this.getPlayerListenerId(id); const player = this.getPlayerWithId(id); if (!player) { log_error(`Player with id: ${id} not found`); } if (playerListenerId) { player?.removeListener(this.getPlayerListenerId(id)); } if (id === this.activePlayerId) { if (!this.castingReceiver) { this.getActivePlayer()?.closeNativePlayer(); } if (this.activePlayerId === id) { this.activePlayerId = null; } } delete this.playersMap[id]; this.lifecycleListeners.forEach((listener) => { listener.onUnRegistered?.(player); }); return this.manager; } public closeNativePlayer() { if (!this.castingReceiver) { if (this.getActivePlayer()?.playerRole === PlayerRole.Primary) { this.getActivePlayer()?.closeNativePlayer(); } } else { this.castingReceiver.close(); } return this.manager; } public registerCastingReceiver(receiver: Player) { this.castingReceiver = receiver; } public unregisterCastingReceiver() { this.castingReceiver = null; } /** * Method retrieves the instantiated player instance controller * Instance controller exposes methods like getContentDuration, getContentPosision * and it returns the current player ref with getInstance() method * Every plugin that wants to interact with the player should use this method to retrieve class * or import the class directly into your component / plugin */ public getInstanceController() { return this.getActivePlayer(); } public togglePlayPause() { deprecationWarning("togglePlayPause"); this.getActivePlayer()?.togglePlayPause(); } public play(id: string = this.activePlayerId, isFromUnregister = false) { deprecationWarning("play"); if (id) { this.activePlayerId = id; } if (this.castingReceiver && !this.getIsPlayerOfCellType(id)) { this.castingReceiver.play(); } // if true, need to pause all players except active, to avoid playing all players after re-register on render if (isFromUnregister) { Object.keys(this.playersMap) .filter((k) => k !== id) .forEach((k) => this.pause(k)); } if (this.getIsPlayerOfCellType(id) && !this.getIsPlayerInViewport(id)) { return null; } return this.getPlayerWithId(id)?.play(); } public setPlayerInViewport(value: boolean, id = this.activePlayerId) { if (this.playersMap[id]) { this.playersMap[id].isInViewport = value; } } public pauseActiveCellPlayer() { if (this.getIsActivePlayerOfCellType()) { this.pause(); } } public playActiveCellPlayer() { if (this.getIsActivePlayerOfCellType()) { this.play(); } } private getIsActivePlayerOfCellType() { return this.getIsPlayerOfCellType(this.activePlayerId); } private getIsPlayerOfCellType(id: string) { const player = this.getPlayerWithId(id); if (player) { return player.isCellPlayer(); } return false; } private getIsPlayerInViewport(id = this.activePlayerId) { return this.playersMap[id].isInViewport; } public pause(id?: string) { deprecationWarning("pause"); if (this.castingReceiver && !this.getIsPlayerOfCellType(id)) { this.castingReceiver.pause(); } return this.getPlayerWithId(id || this.activePlayerId)?.pause(); } public rewind(deltaTime = null) { deprecationWarning("rewind"); // TODO: Remove if (this.castingReceiver) { this.castingReceiver.rewind(deltaTime); } else { this.getActivePlayer()?.rewind(deltaTime); } } public forward(deltaTime = null) { deprecationWarning("forward"); if (this.castingReceiver) { this.castingReceiver.forward(deltaTime); } else { this.getActivePlayer()?.forward(deltaTime); } } public seeking(time) { deprecationWarning("seeking"); // TODO: remove if (this.castingReceiver) { this.castingReceiver.seekTo(time); } else { this.getActivePlayer()?.seekTo(time); } } public on(event, handler) { return this.manager.on(event, handler); } public removeHandler(event, handler) { return this.manager.removeHandler(event, handler); } // TODO: should not be public invokeHandler(event, ...args) { return this.manager.invokeHandler(event, ...args); } public getDuration() { deprecationWarning("getDuration"); return this.getActivePlayer()?.getDuration(); } // TODO: Needs to implement public disableBufferAnimation() { return this.getActivePlayer()?.disableBufferAnimation(); } public getCurrentTime() { deprecationWarning("getCurrentTime"); return this.getActivePlayer()?.getPosition(); } public getState(id?: string) { deprecationWarning("getState"); return this.getPlayerWithId(id)?.getState(); } // TODO: Do refactor to set audio/text on separate functions public setLanguage( track: QuickBrickPlayer.TextTrack | QuickBrickPlayer.AudioTrack ) { deprecationWarning("setLanguage"); void setUserLanguagePreference(track); if (this.castingReceiver) { this.castingReceiver.selectTrack?.(track); } // Call method on player just in case return this.getActivePlayer()?.selectTrack(track); } public isRegistered() { return !!this.activePlayerId; } public getPluginConfiguration() { deprecationWarning("getPluginConfiguration"); return this.getActivePlayer()?.getPluginConfiguration(); } public appStateChange(appState, previousAppState) { deprecationWarning("appStateChange"); return this.getActivePlayer()?.appStateChange(appState, previousAppState); } public close() { deprecationWarning("close"); if (this.castingReceiver) { this.castingReceiver.closeNativePlayer(); } else { this.getActivePlayer().closeNativePlayer(); this.invokeHandler("close"); } } // TODO: Should be retrieved from player instance controller public getTracksState(playerId?: string) { if (this.castingReceiver) { return this.castingReceiver.getTracksState(); } const playerState = this.getState(playerId); const data = R.tryCatch( R.pick(["audioTracks", "textTracks", "textTrackId", "audioTrackId"]), R.F )(playerState); return data; } public addLifecycleListener(listener: PlayerLifecycleListener): () => void { this.lifecycleListeners.push(listener); return () => this.removeLifecycleListener(listener); } public removeLifecycleListener(listener: PlayerLifecycleListener) { this.lifecycleListeners = this.lifecycleListeners.filter( (l) => l !== listener ); } } export const playerManager = PlayerManager.getInstance();