import { FrigateConfig } from "@/types/frigateConfig"; import VideoPlayer from "./VideoPlayer"; import useSWR from "swr"; import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { useApiHost } from "@/api"; import Player from "video.js/dist/types/player"; import { AspectRatio } from "../ui/aspect-ratio"; import { LuPlayCircle } from "react-icons/lu"; type PreviewPlayerProps = { camera: string; relevantPreview?: Preview; startTs: number; eventId: string; isMobile: boolean; onClick?: () => void; }; type Preview = { camera: string; src: string; type: string; start: number; end: number; }; export default function PreviewThumbnailPlayer({ camera, relevantPreview, startTs, eventId, isMobile, onClick, }: PreviewPlayerProps) { const { data: config } = useSWR("config"); const playerRef = useRef(null); const isSafari = useMemo(() => { return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); }, []); const [visible, setVisible] = useState(false); const [isInitiallyVisible, setIsInitiallyVisible] = useState(false); const onPlayback = useCallback( (isHovered: Boolean) => { if (!relevantPreview) { return; } if (!playerRef.current) { setIsInitiallyVisible(true); return; } if (isHovered) { playerRef.current.play(); } else { playerRef.current.pause(); playerRef.current.currentTime(startTs - relevantPreview.start); } }, [relevantPreview, startTs, playerRef] ); const autoPlayObserver = useRef(); const preloadObserver = useRef(); const inViewRef = useCallback( (node: HTMLElement | null) => { if (!preloadObserver.current) { try { preloadObserver.current = new IntersectionObserver( (entries) => { const [{ isIntersecting }] = entries; setVisible(isIntersecting); }, { threshold: 0, root: document.getElementById("pageRoot"), rootMargin: "10% 0px 25% 0px", } ); if (node) preloadObserver.current.observe(node); } catch (e) { // no op } } if (isMobile && !autoPlayObserver.current) { try { autoPlayObserver.current = new IntersectionObserver( (entries) => { const [{ isIntersecting }] = entries; if (isIntersecting) { onPlayback(true); } else { onPlayback(false); } }, { threshold: 1.0, root: document.getElementById("pageRoot"), } ); if (node) autoPlayObserver.current.observe(node); } catch (e) { // no op } } }, [preloadObserver, autoPlayObserver, onPlayback] ); return ( onPlayback(true)} onMouseLeave={() => onPlayback(false)} > ); } type PreviewContentProps = { playerRef: React.MutableRefObject; config: FrigateConfig; camera: string; relevantPreview: Preview | undefined; eventId: string; isVisible: boolean; isInitiallyVisible: boolean; startTs: number; isMobile: boolean; isSafari: boolean; onClick?: () => void; }; function PreviewContent({ playerRef, config, camera, relevantPreview, eventId, isVisible, isInitiallyVisible, startTs, isMobile, isSafari, onClick, }: PreviewContentProps) { const apiHost = useApiHost(); // handle touchstart -> touchend as click const [touchStart, setTouchStart] = useState(0); const handleTouchStart = useCallback(() => { setTouchStart(new Date().getTime()); }, []); useEffect(() => { if (!isMobile || !playerRef.current || !onClick) { return; } playerRef.current.on("touchend", () => { if (!onClick) { return; } const touchEnd = new Date().getTime(); // consider tap less than 300 ms if (touchEnd - touchStart < 300) { onClick(); } }); }, [playerRef, touchStart]); if (relevantPreview && !isVisible) { return
; } else if (!relevantPreview) { if (isCurrentHour(startTs)) { return ( ); } else { return ( ); } } else { return ( <>
{ playerRef.current = player; if (!isInitiallyVisible) { player.pause(); // autoplay + pause is required for iOS } player.playbackRate(isSafari ? 2 : 8); player.currentTime(startTs - relevantPreview.start); if (isMobile && onClick) { player.on("touchstart", handleTouchStart); } }} onDispose={() => { playerRef.current = null; }} />
); } } function isCurrentHour(timestamp: number) { const now = new Date(); now.setMinutes(0, 0, 0); return timestamp > now.getTime() / 1000; } function getPreviewWidth(camera: string, config: FrigateConfig) { const detect = config.cameras[camera].detect; if (detect.width / detect.height < 1) { return "w-1/2"; } if (detect.width / detect.height < 16 / 9) { return "w-2/3"; } return "w-full"; }