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