fix motion previews on safari and ios

match the logic used in ScrubbablePreview for manually stepping currentTime at the correct rate
This commit is contained in:
Josh Hawkins 2026-03-13 12:12:13 -05:00
parent 361b092007
commit 8f5698f261

View File

@ -9,6 +9,7 @@ import {
useState, useState,
} from "react"; } from "react";
import { isCurrentHour } from "@/utils/dateUtil"; import { isCurrentHour } from "@/utils/dateUtil";
import { isFirefox, isMobile, isSafari } from "react-device-detect";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CameraConfig } from "@/types/frigateConfig"; import { CameraConfig } from "@/types/frigateConfig";
import useSWR from "swr"; import useSWR from "swr";
@ -305,31 +306,46 @@ function MotionPreviewClip({
); );
}, [clipStart, preview, range.end_time]); }, [clipStart, preview, range.end_time]);
const compatIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
return () => {
if (compatIntervalRef.current) {
clearInterval(compatIntervalRef.current);
}
};
}, []);
const resetPlayback = useCallback(() => { const resetPlayback = useCallback(() => {
if (!videoRef.current || !preview) { if (!videoRef.current || !preview) {
return; return;
} }
if (compatIntervalRef.current) {
clearInterval(compatIntervalRef.current);
compatIntervalRef.current = null;
}
videoRef.current.currentTime = clipStart; videoRef.current.currentTime = clipStart;
videoRef.current.playbackRate = playbackRate;
}, [clipStart, playbackRate, preview]);
useEffect(() => { if (isSafari || (isFirefox && isMobile)) {
if (!videoRef.current || !preview) { // Safari / iOS can't play at speeds > 2x, so manually step through frames
return;
}
if (!isVisible) {
videoRef.current.pause(); videoRef.current.pause();
videoRef.current.currentTime = clipStart; compatIntervalRef.current = setInterval(() => {
return; if (!videoRef.current) {
} return;
}
if (videoRef.current.readyState >= 2) { videoRef.current.currentTime += 1;
resetPlayback();
void videoRef.current.play().catch(() => undefined); if (videoRef.current.currentTime >= clipEnd) {
videoRef.current.currentTime = clipStart;
}
}, 1000 / playbackRate);
} else {
videoRef.current.playbackRate = playbackRate;
} }
}, [clipStart, isVisible, preview, resetPlayback]); }, [clipStart, clipEnd, playbackRate, preview]);
const drawDimOverlay = useCallback(() => { const drawDimOverlay = useCallback(() => {
if (!dimOverlayCanvasRef.current) { if (!dimOverlayCanvasRef.current) {
@ -463,15 +479,17 @@ function MotionPreviewClip({
{showLoadingIndicator && ( {showLoadingIndicator && (
<Skeleton className="absolute inset-0 z-10 rounded-lg md:rounded-2xl" /> <Skeleton className="absolute inset-0 z-10 rounded-lg md:rounded-2xl" />
)} )}
{preview ? ( {preview && isVisible ? (
<> <>
<video <video
ref={videoRef} ref={videoRef}
className="size-full bg-black object-contain" className="size-full bg-black object-contain"
preload="auto"
autoPlay
playsInline playsInline
preload={isVisible ? "metadata" : "none"}
muted muted
autoPlay={isVisible} disableRemotePlayback
loop
onLoadedMetadata={() => { onLoadedMetadata={() => {
setVideoLoaded(true); setVideoLoaded(true);
@ -481,36 +499,21 @@ function MotionPreviewClip({
height: videoRef.current.videoHeight, height: videoRef.current.videoHeight,
}); });
} }
if (!isVisible) {
return;
}
resetPlayback();
if (videoRef.current) {
void videoRef.current.play().catch(() => undefined);
}
}} }}
onCanPlay={() => { onCanPlay={() => {
setVideoLoaded(true); setVideoLoaded(true);
if (!isVisible) {
return;
}
if (videoRef.current) {
void videoRef.current.play().catch(() => undefined);
}
}} }}
onPlay={() => setVideoPlaying(true)} onPlay={() => {
setVideoPlaying(true);
resetPlayback();
}}
onLoadedData={() => setVideoLoaded(true)} onLoadedData={() => setVideoLoaded(true)}
onError={() => { onError={() => {
setVideoLoaded(true); setVideoLoaded(true);
setVideoPlaying(true); setVideoPlaying(true);
}} }}
onTimeUpdate={() => { onTimeUpdate={() => {
if (!videoRef.current || !preview || !isVisible) { if (!videoRef.current || !preview) {
return; return;
} }
@ -519,12 +522,10 @@ function MotionPreviewClip({
} }
}} }}
> >
{isVisible && ( <source
<source src={`${baseUrl}${preview.src.substring(1)}`}
src={`${baseUrl}${preview.src.substring(1)}`} type={preview.type}
type={preview.type} />
/>
)}
</video> </video>
{motionHeatmap && ( {motionHeatmap && (
<canvas <canvas