Fix android/chrome seeking on previews (#10512)

This commit is contained in:
Nicolas Mowen 2024-03-17 18:53:47 -06:00 committed by GitHub
parent bb6f153e2e
commit 880bae1eb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 47 additions and 19 deletions

View File

@ -12,7 +12,7 @@ import { Preview } from "@/types/preview";
import { PreviewPlayback } from "@/types/playback"; import { PreviewPlayback } from "@/types/playback";
import { isCurrentHour } from "@/utils/dateUtil"; import { isCurrentHour } from "@/utils/dateUtil";
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import { isAndroid } from "react-device-detect"; import { isAndroid, isChrome, isMobile } from "react-device-detect";
import { Skeleton } from "../ui/skeleton"; import { Skeleton } from "../ui/skeleton";
type PreviewPlayerProps = { type PreviewPlayerProps = {
@ -21,6 +21,7 @@ type PreviewPlayerProps = {
timeRange: { start: number; end: number }; timeRange: { start: number; end: number };
cameraPreviews: Preview[]; cameraPreviews: Preview[];
startTime?: number; startTime?: number;
isScrubbing: boolean;
onControllerReady: (controller: PreviewController) => void; onControllerReady: (controller: PreviewController) => void;
onClick?: () => void; onClick?: () => void;
}; };
@ -30,6 +31,7 @@ export default function PreviewPlayer({
timeRange, timeRange,
cameraPreviews, cameraPreviews,
startTime, startTime,
isScrubbing,
onControllerReady, onControllerReady,
onClick, onClick,
}: PreviewPlayerProps) { }: PreviewPlayerProps) {
@ -53,6 +55,7 @@ export default function PreviewPlayer({
timeRange={timeRange} timeRange={timeRange}
cameraPreviews={cameraPreviews} cameraPreviews={cameraPreviews}
startTime={startTime} startTime={startTime}
isScrubbing={isScrubbing}
onControllerReady={onControllerReady} onControllerReady={onControllerReady}
onClick={onClick} onClick={onClick}
/> />
@ -79,6 +82,7 @@ type PreviewVideoPlayerProps = {
timeRange: { start: number; end: number }; timeRange: { start: number; end: number };
cameraPreviews: Preview[]; cameraPreviews: Preview[];
startTime?: number; startTime?: number;
isScrubbing: boolean;
onControllerReady: (controller: PreviewVideoController) => void; onControllerReady: (controller: PreviewVideoController) => void;
onClick?: () => void; onClick?: () => void;
}; };
@ -88,6 +92,7 @@ function PreviewVideoPlayer({
timeRange, timeRange,
cameraPreviews, cameraPreviews,
startTime, startTime,
isScrubbing,
onControllerReady, onControllerReady,
onClick, onClick,
}: PreviewVideoPlayerProps) { }: PreviewVideoPlayerProps) {
@ -118,6 +123,14 @@ function PreviewVideoPlayer({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [controller]); }, [controller]);
useEffect(() => {
if (!controller) {
return;
}
controller.scrubbing = isScrubbing;
}, [controller, isScrubbing]);
// initial state // initial state
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
@ -140,7 +153,12 @@ function PreviewVideoPlayer({
return; return;
} }
controller.finishedSeeking(); if (isAndroid && isChrome) {
// android/chrome glitches when setting currentTime at the same time as onSeeked
setTimeout(() => controller.finishedSeeking(), 25);
} else {
controller.finishedSeeking();
}
}, [controller]); }, [controller]);
useEffect(() => { useEffect(() => {
@ -227,6 +245,7 @@ class PreviewVideoController extends PreviewController {
// preview // preview
private preview: Preview | undefined = undefined; private preview: Preview | undefined = undefined;
private timeToSeek: number | undefined = undefined; private timeToSeek: number | undefined = undefined;
public scrubbing = false;
private seeking = false; private seeking = false;
constructor( constructor(
@ -253,6 +272,18 @@ class PreviewVideoController extends PreviewController {
return false; return false;
} }
const seekTime = time - this.preview.start;
if (
isAndroid &&
isChrome &&
this.scrubbing &&
Math.abs(seekTime - this.previewRef.current.currentTime) > 400
) {
// android/chrome has incorrect timestamps sent that are before the expected seek time
return false;
}
if (this.seeking) { if (this.seeking) {
this.timeToSeek = time; this.timeToSeek = time;
} else { } else {
@ -276,21 +307,12 @@ class PreviewVideoController extends PreviewController {
Math.round(this.timeToSeek) - Math.round(this.timeToSeek) -
Math.round(this.previewRef.current.currentTime + this.preview.start); Math.round(this.previewRef.current.currentTime + this.preview.start);
if (Math.abs(diff) > 1) { const scrubLimit = isMobile ? 1 : 0.5;
let seekTime;
if (isAndroid) {
if (diff < 30) {
seekTime = Math.round(
this.previewRef.current.currentTime + diff / 2,
);
} else {
seekTime = Math.round(this.timeToSeek - this.preview.start);
}
} else {
seekTime = this.timeToSeek - this.preview.start;
}
this.previewRef.current.currentTime = seekTime; if (Math.abs(diff) >= scrubLimit) {
// only seek if there is an appropriate amount of time difference
this.previewRef.current.currentTime =
this.timeToSeek - this.preview.start;
} else { } else {
this.seeking = false; this.seeking = false;
this.timeToSeek = undefined; this.timeToSeek = undefined;

View File

@ -18,10 +18,10 @@ type DynamicVideoPlayerProps = {
timeRange: { start: number; end: number }; timeRange: { start: number; end: number };
cameraPreviews: Preview[]; cameraPreviews: Preview[];
startTimestamp?: number; startTimestamp?: number;
isScrubbing: boolean;
onControllerReady: (controller: DynamicVideoController) => void; onControllerReady: (controller: DynamicVideoController) => void;
onTimestampUpdate?: (timestamp: number) => void; onTimestampUpdate?: (timestamp: number) => void;
onClipEnded?: () => void; onClipEnded?: () => void;
isScrubbing: boolean;
}; };
export default function DynamicVideoPlayer({ export default function DynamicVideoPlayer({
className, className,
@ -29,10 +29,10 @@ export default function DynamicVideoPlayer({
timeRange, timeRange,
cameraPreviews, cameraPreviews,
startTimestamp, startTimestamp,
isScrubbing,
onControllerReady, onControllerReady,
onTimestampUpdate, onTimestampUpdate,
onClipEnded, onClipEnded,
isScrubbing,
}: DynamicVideoPlayerProps) { }: DynamicVideoPlayerProps) {
const apiHost = useApiHost(); const apiHost = useApiHost();
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -192,6 +192,7 @@ export default function DynamicVideoPlayer({
timeRange={timeRange} timeRange={timeRange}
cameraPreviews={cameraPreviews} cameraPreviews={cameraPreviews}
startTime={startTimestamp} startTime={startTimestamp}
isScrubbing={isScrubbing}
onControllerReady={(previewController) => { onControllerReady={(previewController) => {
setPreviewController(previewController); setPreviewController(previewController);
}} }}

View File

@ -616,6 +616,8 @@ function MotionReview({
[selectedRangeIdx, timeRangeSegments], [selectedRangeIdx, timeRangeSegments],
); );
const [scrubbing, setScrubbing] = useState(false);
// move to next clip // move to next clip
useEffect(() => { useEffect(() => {
@ -670,6 +672,7 @@ function MotionReview({
timeRange={currentTimeRange} timeRange={currentTimeRange}
startTime={startTime} startTime={startTime}
cameraPreviews={relevantPreviews || []} cameraPreviews={relevantPreviews || []}
isScrubbing={scrubbing}
onControllerReady={(controller) => { onControllerReady={(controller) => {
videoPlayersRef.current[camera.name] = controller; videoPlayersRef.current[camera.name] = controller;
}} }}
@ -694,6 +697,7 @@ function MotionReview({
motion_events={motionData ?? []} motion_events={motionData ?? []}
severityType="significant_motion" severityType="significant_motion"
contentRef={contentRef} contentRef={contentRef}
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
/> />
</div> </div>
</> </>

View File

@ -192,7 +192,7 @@ export function RecordingView({
const grow = useMemo(() => { const grow = useMemo(() => {
if (mainCameraAspect == "wide") { if (mainCameraAspect == "wide") {
return "w-full aspect-wide"; return "w-full aspect-wide";
} else if (mainCameraAspect == "tall") { } else if (isDesktop && mainCameraAspect == "tall") {
return "h-full aspect-tall"; return "h-full aspect-tall";
} else { } else {
return "w-full aspect-video"; return "w-full aspect-video";
@ -288,6 +288,7 @@ export function RecordingView({
timeRange={currentTimeRange} timeRange={currentTimeRange}
cameraPreviews={allPreviews ?? []} cameraPreviews={allPreviews ?? []}
startTime={startTime} startTime={startTime}
isScrubbing={scrubbing}
onControllerReady={(controller) => { onControllerReady={(controller) => {
previewRefs.current[cam] = controller; previewRefs.current[cam] = controller;
controller.scrubToTimestamp(startTime); controller.scrubToTimestamp(startTime);