import WebRtcPlayer from "./WebRTCPlayer"; import { CameraConfig } from "@/types/frigateConfig"; import AutoUpdatingCameraImage from "../camera/AutoUpdatingCameraImage"; import ActivityIndicator from "../indicators/activity-indicator"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import MSEPlayer from "./MsePlayer"; import JSMpegPlayer from "./JSMpegPlayer"; import { MdCircle } from "react-icons/md"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { useCameraActivity } from "@/hooks/use-camera-activity"; import { LivePlayerError, LivePlayerMode, VideoResolutionType, } from "@/types/live"; import { getIconForLabel } from "@/utils/iconUtil"; import Chip from "../indicators/Chip"; import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { cn } from "@/lib/utils"; import { TbExclamationCircle } from "react-icons/tb"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import { baseUrl } from "@/api/baseUrl"; type LivePlayerProps = { cameraRef?: (ref: HTMLDivElement | null) => void; containerRef?: React.MutableRefObject; className?: string; cameraConfig: CameraConfig; preferredLiveMode: LivePlayerMode; showStillWithoutActivity?: boolean; windowVisible?: boolean; playAudio?: boolean; micEnabled?: boolean; // only webrtc supports mic iOSCompatFullScreen?: boolean; pip?: boolean; autoLive?: boolean; onClick?: () => void; setFullResolution?: React.Dispatch>; onError?: (error: LivePlayerError) => void; onResetLiveMode?: () => void; }; export default function LivePlayer({ cameraRef = undefined, containerRef, className, cameraConfig, preferredLiveMode, showStillWithoutActivity = true, windowVisible = true, playAudio = false, micEnabled = false, iOSCompatFullScreen = false, pip, autoLive = true, onClick, setFullResolution, onError, onResetLiveMode, }: LivePlayerProps) { const internalContainerRef = useRef(null); // camera activity const { activeMotion, activeTracking, objects, offline } = useCameraActivity(cameraConfig); const cameraActive = useMemo( () => !showStillWithoutActivity || (windowVisible && (activeMotion || activeTracking)), [activeMotion, activeTracking, showStillWithoutActivity, windowVisible], ); // camera live state const [liveReady, setLiveReady] = useState(false); const liveReadyRef = useRef(liveReady); const cameraActiveRef = useRef(cameraActive); useEffect(() => { liveReadyRef.current = liveReady; cameraActiveRef.current = cameraActive; }, [liveReady, cameraActive]); useEffect(() => { if (!autoLive || !liveReady) { return; } if (!cameraActive) { const timer = setTimeout(() => { if (liveReadyRef.current && !cameraActiveRef.current) { setLiveReady(false); onResetLiveMode?.(); } }, 500); return () => { clearTimeout(timer); }; } // live mode won't change // eslint-disable-next-line react-hooks/exhaustive-deps }, [autoLive, cameraActive, liveReady]); // camera still state const stillReloadInterval = useMemo(() => { if (!windowVisible || offline || !showStillWithoutActivity) { return -1; // no reason to update the image when the window is not visible } if (liveReady && !cameraActive) { return 300; } if (liveReady) { return 60000; } if (activeMotion || activeTracking) { if (autoLive) { return 200; } else { return 59000; } } return 30000; }, [ autoLive, showStillWithoutActivity, liveReady, activeMotion, activeTracking, offline, windowVisible, cameraActive, ]); useEffect(() => { setLiveReady(false); }, [preferredLiveMode]); const playerIsPlaying = useCallback(() => { setLiveReady(true); }, []); if (!cameraConfig) { return ; } let player; if (!autoLive) { player = null; } else if (preferredLiveMode == "webrtc") { player = ( ); } else if (preferredLiveMode == "mse") { if ("MediaSource" in window || "ManagedMediaSource" in window) { player = ( ); } else { player = (
iOS 17.1 or greater is required for this live stream type.
); } } else if (preferredLiveMode == "jsmpeg") { if (cameraActive || !showStillWithoutActivity || liveReady) { player = ( ); } else { player = null; } } else { player = ; } return (
{ if (e.button === 1) { window.open(`${baseUrl}#${cameraConfig.name}`, "_blank")?.focus(); } }} > {((showStillWithoutActivity && !liveReady) || liveReady) && ( <>
)} {player} {!offline && !showStillWithoutActivity && !liveReady && ( )} {((showStillWithoutActivity && !liveReady) || liveReady) && objects.length > 0 && (
{[ ...new Set([ ...(objects || []).map(({ label }) => label), ]), ] .map((label) => { return getIconForLabel(label, "size-3 text-white"); }) .sort()}
{[ ...new Set([ ...(objects || []).map(({ label, sub_label }) => label.endsWith("verified") ? sub_label : label, ), ]), ] .filter((label) => label?.includes("-verified") == false) .map((label) => capitalizeFirstLetter(label)) .sort() .join(", ") .replaceAll("-verified", "")}
)}
{offline && !showStillWithoutActivity && (

Stream offline

No frames have been received on the{" "} {capitalizeFirstLetter(cameraConfig.name)} detect{" "} stream, check error logs

)}
{autoLive && !offline && activeMotion && ((showStillWithoutActivity && !liveReady) || liveReady) && ( )} {offline && showStillWithoutActivity && ( {cameraConfig.name.replaceAll("_", " ")} )}
); }