diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf index 0dc57a26fc..9f4687fa07 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -210,7 +210,7 @@ http { include proxy.conf; } - location ~* /api/.*\.(jpg|jpeg|png|webp)$ { + location ~* /api/.*\.(jpg|jpeg|png|webp|gif)$ { rewrite ^/api/(.*)$ $1 break; proxy_pass http://frigate_api; include proxy.conf; diff --git a/web/src/components/image/AnimatedEventThumbnail.tsx b/web/src/components/image/AnimatedEventThumbnail.tsx index 2489c89a03..40bddd46c5 100644 --- a/web/src/components/image/AnimatedEventThumbnail.tsx +++ b/web/src/components/image/AnimatedEventThumbnail.tsx @@ -1,18 +1,17 @@ import { baseUrl } from "@/api/baseUrl"; import TimeAgo from "../dynamic/TimeAgo"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; -import { useCallback, useMemo } from "react"; -import { useApiHost } from "@/api"; +import { useCallback, useMemo, useState } from "react"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { ReviewSegment } from "@/types/review"; import { useNavigate } from "react-router-dom"; +import { Skeleton } from "../ui/skeleton"; type AnimatedEventThumbnailProps = { event: ReviewSegment; }; export function AnimatedEventThumbnail({ event }: AnimatedEventThumbnailProps) { - const apiHost = useApiHost(); const { data: config } = useSWR("config"); // interaction @@ -24,13 +23,15 @@ export function AnimatedEventThumbnail({ event }: AnimatedEventThumbnailProps) { // image behavior + const [loaded, setLoaded] = useState(false); + const [error, setError] = useState(0); const imageUrl = useMemo(() => { - if (Date.now() / 1000 < event.start_time + 20) { - return `${apiHost}api/preview/${event.camera}/${event.start_time}/thumbnail.jpg`; + if (error > 0) { + return `${baseUrl}api/review/${event.id}/preview.gif?key=${error}`; } return `${baseUrl}api/review/${event.id}/preview.gif`; - }, [apiHost, event]); + }, [error, event]); const aspectRatio = useMemo(() => { if (!config) { @@ -44,14 +45,22 @@ export function AnimatedEventThumbnail({ event }: AnimatedEventThumbnailProps) { return ( -
+
+ setLoaded(true)} + onError={() => { + if (error < 2) { + setError(error + 1); + } + }} + /> + {!loaded && }
diff --git a/web/src/components/player/DynamicVideoPlayer.tsx b/web/src/components/player/DynamicVideoPlayer.tsx index d3ef4f9e05..07495da195 100644 --- a/web/src/components/player/DynamicVideoPlayer.tsx +++ b/web/src/components/player/DynamicVideoPlayer.tsx @@ -10,7 +10,7 @@ import { Recording } from "@/types/record"; import { Preview } from "@/types/preview"; import { DynamicPlayback } from "@/types/playback"; import PreviewPlayer, { PreviewController } from "./PreviewPlayer"; -import { isDesktop, isMobile } from "react-device-detect"; +import { isDesktop } from "react-device-detect"; import { LuPause, LuPlay } from "react-icons/lu"; import { DropdownMenu, @@ -152,6 +152,19 @@ export default function DynamicVideoPlayer({ onKeyboardShortcut, ); + // mobile tap controls + + useEffect(() => { + if (isDesktop || !playerRef) { + return; + } + + const callback = () => setControls(!controls); + playerRef.on("touchstart", callback); + + return () => playerRef.off("touchstart", callback); + }, [controls, playerRef]); + // initial state const initialPlaybackSource = useMemo(() => { @@ -238,14 +251,6 @@ export default function DynamicVideoPlayer({ } : undefined } - onClick={ - isMobile - ? (e) => { - e.stopPropagation(); - setControls(!controls); - } - : undefined - } >
{ setPlayerRef(player); }} diff --git a/web/src/components/player/PreviewPlayer.tsx b/web/src/components/player/PreviewPlayer.tsx index fd92486973..9c8e9d6a4e 100644 --- a/web/src/components/player/PreviewPlayer.tsx +++ b/web/src/components/player/PreviewPlayer.tsx @@ -13,6 +13,7 @@ import { PreviewPlayback } from "@/types/playback"; import { isCurrentHour } from "@/utils/dateUtil"; import { baseUrl } from "@/api/baseUrl"; import { isAndroid } from "react-device-detect"; +import { Skeleton } from "../ui/skeleton"; type PreviewPlayerProps = { className?: string; @@ -119,6 +120,7 @@ function PreviewVideoPlayer({ // initial state + const [loaded, setLoaded] = useState(false); const initialPreview = useMemo(() => { return cameraPreviews.find( (preview) => @@ -152,6 +154,7 @@ function PreviewVideoPlayer({ Math.round(preview.start) >= timeRange.start && Math.floor(preview.end) <= timeRange.end, ); + setLoaded(false); setCurrentPreview(preview); controller.newPlayback({ @@ -186,6 +189,8 @@ function PreviewVideoPlayer({ disableRemotePlayback onSeeked={onPreviewSeeked} onLoadedData={() => { + setLoaded(true); + if (controller) { controller.previewReady(); } else { @@ -201,6 +206,7 @@ function PreviewVideoPlayer({ )} + {!loaded && } {cameraPreviews && !currentPreview && (
No Preview Found @@ -270,9 +276,15 @@ class PreviewVideoController extends PreviewController { if (isAndroid) { const currentTs = this.previewRef.current.currentTime + this.preview.start; - this.previewRef.current.currentTime = - this.previewRef.current.currentTime + - (this.timeToSeek - currentTs) / 2; + const diff = this.timeToSeek - currentTs; + + if (diff < 30) { + this.previewRef.current.currentTime = + this.previewRef.current.currentTime + diff / 2; + } else { + this.previewRef.current.currentTime = + this.timeToSeek - this.preview.start; + } } else { this.previewRef.current.currentTime = this.timeToSeek - this.preview.start; diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index 0e50ae871a..ffae25eabc 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -117,7 +117,7 @@ export default function LiveDashboardView({ {events && events.length > 0 && ( -
+
{events.map((event) => { return ; })}