// /// // import * as React from "react"; import { useEffect } from "react"; import { AppState, AppStateStatus, View } from "react-native"; import { TrackedView } from "../TrackedView"; import { useDimensions } from "@applicaster/zapp-react-native-utils/reactHooks/layout"; import { isApplePlatform, isTV, } from "@applicaster/zapp-react-native-utils/reactUtils"; import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager"; import { getUserCellPlayerMutedPreference } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/userCellPlayerMutedPreference"; import Image from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/DefaultComponents/Image"; import { LiveImage, LiveImageManager, LiveImageType } from "./LiveImageManager"; import { AnimatedInOut } from "@applicaster/zapp-react-native-ui-components/Components/AnimatedInOut"; import { overlayFadeIn } from "./animationUtils"; import { loggerLiveImageComponent } from "./loggerHelper"; import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer"; import { getAutoplaySettings } from "./utils"; import { isString } from "@applicaster/zapp-react-native-utils/stringUtils"; import { BufferAnimation } from "../PlayerContainer/BufferAnimation"; import { PreloadHookConfig } from "../MasterCell/DefaultComponents/LiveImage/executePreloadHooks"; const { log_error, log_debug } = loggerLiveImageComponent; const isMeasurement = (itemId: string | number | null): boolean => isString(itemId) && itemId.startsWith("pre-measurement-"); // Pixels by which the view can slightly extend outside the viewport and still be considered fully visible. const CLIP_THRESHOLD = 10; type Props = { item: ZappEntry; style: Record; playerId: string; playerPluginId?: string; imageKey?: string; screenConfig: Record; audioMutedByDefault?: boolean; uri: string; // cover image url preloadHooks?: PreloadHookConfig[]; }; const PlayerLiveImageComponent = (props: Props) => { const dimensions = useDimensions("screen", { fullDimensions: true, updateForInactiveScreens: false, }); const { item, style, playerId } = props; const timeoutRef = React.useRef(null); const [mode, setMode] = React.useState(LiveImageType.Image); const [isReadyToPlay, setIsReady] = React.useState(false); const [muted, setMuted] = React.useState( isTV() ? false : getUserCellPlayerMutedPreference(props.audioMutedByDefault) ); const ref = React.useRef(null); const trackViewRef = React.useRef(null); /** * Debounced setMode function. * * Sets to video with a 1sec delay. Set to image happens immediately and cancels the timeout. * * @param type video or image */ const setModeDebounced = React.useCallback( (type: LiveImageType) => { if (type === LiveImageType.Video) { if (type === mode) { return; } timeoutRef.current = setTimeout(() => { setMode(type); }, 1000) as unknown as NodeJS.Timeout; } else { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setMode(type); } }, [mode] ); const entryToViewIdMapping = React.useRef({}); useEffect(() => { const nativeTag = trackViewRef.current?._nativeTag?.toString(); if (!nativeTag) { return; } const prevValue = entryToViewIdMapping.current[nativeTag]; if (prevValue && prevValue.itemId !== item.id) { if (mode === LiveImageType.Video) { log_debug( "Entry to view association changed, stopping current live image" ); setMode(LiveImageType.Image); } } entryToViewIdMapping.current[nativeTag] = { itemId: item.id }; }, [item.id, mode]); const { screenConfig, playerPluginId, preloadHooks } = props; const liveImageItem: LiveImage = React.useMemo(() => { return new LiveImage({ playerId, preloadHooks, factoryConfig: { player: ref, playerId, playerPluginId: playerPluginId, screenConfig: screenConfig, entry: item, }, tag: item.title?.toString(), }); }, [playerId, preloadHooks, playerPluginId, screenConfig, item]); // Keep the muted state in sync with user preference and player state liveImageItem.initiallyMuted = muted; React.useEffect(() => { liveImageItem.setMode = setModeDebounced; }, [setModeDebounced, liveImageItem]); // todo: no need for it to be react, can be moved into `player.addListener` const { start, end } = React.useMemo( () => getAutoplaySettings(item), [item.id] ); const player = usePlayer(playerId); const _assignRoot = (component) => { ref.current = component; }; const onLiveImageCompleted = React.useCallback(() => { LiveImageManager.instance.onLiveImageCompleted(liveImageItem); }, [liveImageItem]); const onStop = React.useCallback(() => { LiveImageManager.instance.pauseLiveImage(liveImageItem); }, [liveImageItem]); React.useEffect(() => { // FIXME - find a more elegant way to disable live-image on cell for measurement if (isMeasurement(item.id)) { return; } const unregister = LiveImageManager.instance.register(liveImageItem); return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } unregister(); }; }, [item.id, liveImageItem]); React.useEffect(() => { if (!player) { return; } return player.addListener({ id: "mute-action-button", listener: { onLoad: () => { start && player.seekTo(start); setIsReady(true); }, onVideoProgress: ({ currentTime }) => { if (start && currentTime < start) { // fixes the case when the video is not seekable in onLoad callback player.seekTo(start); } if (end && currentTime > end) { onStop(); setIsReady(false); } }, onPlayerMute: () => { setMuted(true); }, onPlayerUnmute: () => { setMuted(false); }, onPlayerClose: () => { onLiveImageCompleted(); setIsReady(false); }, }, }); }, [player, onStop, onLiveImageCompleted, start, end]); React.useEffect(() => { const subscription = AppState.addEventListener( "change", (value: AppStateStatus) => { // TODO : fix on TV (WEB) if (!player) { return; } if (value === "background" || value === "inactive") { player.pause(); } if (value === "active") { player.play(); } } ); return () => { subscription?.remove(); }; }, [player]); React.useEffect(() => { return () => { setIsReady(false); }; }, [item.id]); React.useEffect(() => { if (isMeasurement(item.id) || !playerManager) { return; } const controller = liveImageItem.getPlayer(); if (!controller) { return; } const isPlayerRegistered = playerManager.isPlayerRegistered(playerId); if (isPlayerRegistered) { log_error( `PlayerLiveImageComponent: player ${playerId} already registered` ); } else { playerManager.registerPlayer({ id: playerId, playerController: controller, }); return () => { playerManager.unregisterPlayer(playerId); }; } }, [liveImageItem, playerId, mode, item.id]); const onPositionUpdated = React.useCallback( (data) => { const normalizedRect = (rect: { left: number; right: number; top: number; bottom: number; }) => { if (!rect) { return null; } const { left, right, top, bottom } = rect; return { left: left / dimensions.width, right: right / dimensions.width, top: top / dimensions.height, bottom: bottom / dimensions.height, centerX: (left + right) / 2 / dimensions.width, centerY: (top + bottom) / 2 / dimensions.height, }; }; liveImageItem.position = normalizedRect(data.rect); const newIsFullyVisible = !!data.rect; if (liveImageItem.isFullyVisible !== newIsFullyVisible) { liveImageItem.isFullyVisible = newIsFullyVisible; liveImageItem.isFullyVisible ? LiveImageManager.instance.onViewportEnter(liveImageItem) : LiveImageManager.instance.onViewportLeave(liveImageItem); } else { LiveImageManager.instance.onViewPositionChanged(liveImageItem); } }, [liveImageItem, dimensions.height, dimensions.width] ); const platformSpecificProps = isApplePlatform() ? { hideNativeControls: true, controls: false } : {}; const isImageMode = mode === LiveImageType.Image; const isVideoMode = mode === LiveImageType.Video; const isBackgroundImageVisible = isImageMode || !isReadyToPlay; const videoStyles = { ...style, position: "absolute", zIndex: -100 }; const Player: any = liveImageItem.component || BufferAnimation; // TODO: nice to have - resolve imageKey when imageUrl is not provided return ( {isVideoMode ? ( ) : null} { } ); }; export const PlayerLiveImage = React.memo(PlayerLiveImageComponent);