mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-19 11:36:43 +03:00
Add event navigation functionality to video player components
This commit is contained in:
parent
2c480b9a89
commit
0f0d6bd83a
@ -49,6 +49,7 @@ type HlsVideoPlayerProps = {
|
||||
onTimeUpdate?: (time: number) => void;
|
||||
onPlaying?: () => void;
|
||||
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
||||
onJumpToEvent?: (direction: "next" | "previous") => void;
|
||||
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
|
||||
toggleFullscreen?: () => void;
|
||||
@ -73,6 +74,7 @@ export default function HlsVideoPlayer({
|
||||
onTimeUpdate,
|
||||
onPlaying,
|
||||
onSeekToTime,
|
||||
onJumpToEvent,
|
||||
setFullResolution,
|
||||
onUploadFrame,
|
||||
toggleFullscreen,
|
||||
@ -257,6 +259,7 @@ export default function HlsVideoPlayer({
|
||||
playbackRate: true,
|
||||
plusUpload: config?.plus?.enabled == true,
|
||||
fullscreen: supportsFullscreen,
|
||||
eventNavigation: onJumpToEvent != undefined,
|
||||
}}
|
||||
setControlsOpen={setControlsOpen}
|
||||
setMuted={(muted) => setMuted(muted)}
|
||||
@ -296,6 +299,7 @@ export default function HlsVideoPlayer({
|
||||
}
|
||||
}
|
||||
}}
|
||||
onJumpToEvent={onJumpToEvent}
|
||||
fullscreen={fullscreen}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
containerRef={containerRef}
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
MdVolumeOff,
|
||||
MdVolumeUp,
|
||||
} from "react-icons/md";
|
||||
import { IoMdSkipBackward, IoMdSkipForward } from "react-icons/io";
|
||||
import useKeyboardListener, {
|
||||
KeyModifiers,
|
||||
} from "@/hooks/use-keyboard-listener";
|
||||
@ -41,6 +42,7 @@ type VideoControls = {
|
||||
playbackRate?: boolean;
|
||||
plusUpload?: boolean;
|
||||
fullscreen?: boolean;
|
||||
eventNavigation?: boolean;
|
||||
};
|
||||
|
||||
const CONTROLS_DEFAULT: VideoControls = {
|
||||
@ -49,6 +51,7 @@ const CONTROLS_DEFAULT: VideoControls = {
|
||||
playbackRate: true,
|
||||
plusUpload: false,
|
||||
fullscreen: false,
|
||||
eventNavigation: false,
|
||||
};
|
||||
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
|
||||
const MIN_ITEMS_WRAP = 6;
|
||||
@ -71,6 +74,7 @@ type VideoControlsProps = {
|
||||
onSeek: (diff: number) => void;
|
||||
onSetPlaybackRate: (rate: number) => void;
|
||||
onUploadFrame?: () => void;
|
||||
onJumpToEvent?: (direction: "next" | "previous") => void;
|
||||
toggleFullscreen?: () => void;
|
||||
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
||||
};
|
||||
@ -92,6 +96,7 @@ export default function VideoControls({
|
||||
onSeek,
|
||||
onSetPlaybackRate,
|
||||
onUploadFrame,
|
||||
onJumpToEvent,
|
||||
toggleFullscreen,
|
||||
containerRef,
|
||||
}: VideoControlsProps) {
|
||||
@ -291,6 +296,26 @@ export default function VideoControls({
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
)}
|
||||
{features.eventNavigation && onJumpToEvent && (
|
||||
<>
|
||||
<IoMdSkipBackward
|
||||
className="size-5 cursor-pointer"
|
||||
title="Previous Event"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onJumpToEvent("previous");
|
||||
}}
|
||||
/>
|
||||
<IoMdSkipForward
|
||||
className="size-5 cursor-pointer"
|
||||
title="Next Event"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onJumpToEvent("next");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{features.fullscreen && toggleFullscreen && (
|
||||
<div className="cursor-pointer" onClick={toggleFullscreen}>
|
||||
{fullscreen ? <FaCompress /> : <FaExpand />}
|
||||
|
||||
@ -34,6 +34,7 @@ type DynamicVideoPlayerProps = {
|
||||
onTimestampUpdate?: (timestamp: number) => void;
|
||||
onClipEnded?: () => void;
|
||||
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
||||
onJumpToEvent?: (direction: "next" | "previous") => void;
|
||||
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||
toggleFullscreen: () => void;
|
||||
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
||||
@ -52,6 +53,7 @@ export default function DynamicVideoPlayer({
|
||||
onTimestampUpdate,
|
||||
onClipEnded,
|
||||
onSeekToTime,
|
||||
onJumpToEvent,
|
||||
setFullResolution,
|
||||
toggleFullscreen,
|
||||
containerRef,
|
||||
@ -280,6 +282,7 @@ export default function DynamicVideoPlayer({
|
||||
onSeekToTime(timestamp, play);
|
||||
}
|
||||
}}
|
||||
onJumpToEvent={onJumpToEvent}
|
||||
onPlaying={() => {
|
||||
if (isScrubbing) {
|
||||
playerRef.current?.pause();
|
||||
|
||||
@ -300,6 +300,69 @@ export function RecordingView({
|
||||
[currentTimeRange, updateSelectedSegment],
|
||||
);
|
||||
|
||||
// event navigation
|
||||
const onJumpToEvent = useCallback(
|
||||
(direction: "next" | "previous") => {
|
||||
if (!mainCameraReviewItems || mainCameraReviewItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort all events by start time to ensure correct order
|
||||
const sortedEvents = [...mainCameraReviewItems].sort((a, b) => a.start_time - b.start_time);
|
||||
|
||||
// Find which event we're currently viewing
|
||||
// Check if current time is between (event start - REVIEW_PADDING) and (event end or start + 60s)
|
||||
const currentEventIndex = sortedEvents.findIndex((item) => {
|
||||
const eventStart = item.start_time - REVIEW_PADDING;
|
||||
const eventEnd = item.end_time || item.start_time + 60; // Assume max 60s if no end_time
|
||||
return currentTime >= eventStart && currentTime <= eventEnd;
|
||||
});
|
||||
|
||||
let targetEvent;
|
||||
|
||||
if (currentEventIndex >= 0) {
|
||||
// We identified the current event - use index-based navigation
|
||||
if (direction === "next") {
|
||||
if (currentEventIndex < sortedEvents.length - 1) {
|
||||
targetEvent = sortedEvents[currentEventIndex + 1];
|
||||
} else {
|
||||
// At the last event, loop to the first
|
||||
targetEvent = sortedEvents[0];
|
||||
}
|
||||
} else {
|
||||
if (currentEventIndex > 0) {
|
||||
targetEvent = sortedEvents[currentEventIndex - 1];
|
||||
} else {
|
||||
// At the first event, loop to the last
|
||||
targetEvent = sortedEvents[sortedEvents.length - 1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Can't identify current event - fall back to time-based navigation
|
||||
if (direction === "next") {
|
||||
// Find the first event that starts after current time
|
||||
targetEvent = sortedEvents.find(
|
||||
(item) => item.start_time > currentTime,
|
||||
);
|
||||
} else {
|
||||
// Find the last event that starts before current time
|
||||
const previousEvents = sortedEvents.filter(
|
||||
(item) => item.start_time < currentTime,
|
||||
);
|
||||
if (previousEvents.length > 0) {
|
||||
targetEvent = previousEvents[previousEvents.length - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only navigate if we found a target event
|
||||
if (targetEvent) {
|
||||
manuallySetCurrentTime(targetEvent.start_time - REVIEW_PADDING, true);
|
||||
}
|
||||
},
|
||||
[mainCameraReviewItems, currentTime, manuallySetCurrentTime],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrubbing) {
|
||||
if (Math.abs(currentTime - playerTime) > 10) {
|
||||
@ -746,6 +809,7 @@ export function RecordingView({
|
||||
}}
|
||||
onClipEnded={onClipEnded}
|
||||
onSeekToTime={manuallySetCurrentTime}
|
||||
onJumpToEvent={onJumpToEvent}
|
||||
onControllerReady={(controller) => {
|
||||
mainControllerRef.current = controller;
|
||||
}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user