// ///
//
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);