@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
490 lines (384 loc) • 12.8 kB
text/typescript
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();