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;
|
onTimeUpdate?: (time: number) => void;
|
||||||
onPlaying?: () => void;
|
onPlaying?: () => void;
|
||||||
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
||||||
|
onJumpToEvent?: (direction: "next" | "previous") => void;
|
||||||
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||||
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
|
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
|
||||||
toggleFullscreen?: () => void;
|
toggleFullscreen?: () => void;
|
||||||
@ -73,6 +74,7 @@ export default function HlsVideoPlayer({
|
|||||||
onTimeUpdate,
|
onTimeUpdate,
|
||||||
onPlaying,
|
onPlaying,
|
||||||
onSeekToTime,
|
onSeekToTime,
|
||||||
|
onJumpToEvent,
|
||||||
setFullResolution,
|
setFullResolution,
|
||||||
onUploadFrame,
|
onUploadFrame,
|
||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
@ -257,6 +259,7 @@ export default function HlsVideoPlayer({
|
|||||||
playbackRate: true,
|
playbackRate: true,
|
||||||
plusUpload: config?.plus?.enabled == true,
|
plusUpload: config?.plus?.enabled == true,
|
||||||
fullscreen: supportsFullscreen,
|
fullscreen: supportsFullscreen,
|
||||||
|
eventNavigation: onJumpToEvent != undefined,
|
||||||
}}
|
}}
|
||||||
setControlsOpen={setControlsOpen}
|
setControlsOpen={setControlsOpen}
|
||||||
setMuted={(muted) => setMuted(muted)}
|
setMuted={(muted) => setMuted(muted)}
|
||||||
@ -296,6 +299,7 @@ export default function HlsVideoPlayer({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onJumpToEvent={onJumpToEvent}
|
||||||
fullscreen={fullscreen}
|
fullscreen={fullscreen}
|
||||||
toggleFullscreen={toggleFullscreen}
|
toggleFullscreen={toggleFullscreen}
|
||||||
containerRef={containerRef}
|
containerRef={containerRef}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
MdVolumeOff,
|
MdVolumeOff,
|
||||||
MdVolumeUp,
|
MdVolumeUp,
|
||||||
} from "react-icons/md";
|
} from "react-icons/md";
|
||||||
|
import { IoMdSkipBackward, IoMdSkipForward } from "react-icons/io";
|
||||||
import useKeyboardListener, {
|
import useKeyboardListener, {
|
||||||
KeyModifiers,
|
KeyModifiers,
|
||||||
} from "@/hooks/use-keyboard-listener";
|
} from "@/hooks/use-keyboard-listener";
|
||||||
@ -41,6 +42,7 @@ type VideoControls = {
|
|||||||
playbackRate?: boolean;
|
playbackRate?: boolean;
|
||||||
plusUpload?: boolean;
|
plusUpload?: boolean;
|
||||||
fullscreen?: boolean;
|
fullscreen?: boolean;
|
||||||
|
eventNavigation?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONTROLS_DEFAULT: VideoControls = {
|
const CONTROLS_DEFAULT: VideoControls = {
|
||||||
@ -49,6 +51,7 @@ const CONTROLS_DEFAULT: VideoControls = {
|
|||||||
playbackRate: true,
|
playbackRate: true,
|
||||||
plusUpload: false,
|
plusUpload: false,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
|
eventNavigation: false,
|
||||||
};
|
};
|
||||||
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
|
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
|
||||||
const MIN_ITEMS_WRAP = 6;
|
const MIN_ITEMS_WRAP = 6;
|
||||||
@ -71,6 +74,7 @@ type VideoControlsProps = {
|
|||||||
onSeek: (diff: number) => void;
|
onSeek: (diff: number) => void;
|
||||||
onSetPlaybackRate: (rate: number) => void;
|
onSetPlaybackRate: (rate: number) => void;
|
||||||
onUploadFrame?: () => void;
|
onUploadFrame?: () => void;
|
||||||
|
onJumpToEvent?: (direction: "next" | "previous") => void;
|
||||||
toggleFullscreen?: () => void;
|
toggleFullscreen?: () => void;
|
||||||
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
};
|
};
|
||||||
@ -92,6 +96,7 @@ export default function VideoControls({
|
|||||||
onSeek,
|
onSeek,
|
||||||
onSetPlaybackRate,
|
onSetPlaybackRate,
|
||||||
onUploadFrame,
|
onUploadFrame,
|
||||||
|
onJumpToEvent,
|
||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
containerRef,
|
containerRef,
|
||||||
}: VideoControlsProps) {
|
}: VideoControlsProps) {
|
||||||
@ -291,6 +296,26 @@ export default function VideoControls({
|
|||||||
containerRef={containerRef}
|
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 && (
|
{features.fullscreen && toggleFullscreen && (
|
||||||
<div className="cursor-pointer" onClick={toggleFullscreen}>
|
<div className="cursor-pointer" onClick={toggleFullscreen}>
|
||||||
{fullscreen ? <FaCompress /> : <FaExpand />}
|
{fullscreen ? <FaCompress /> : <FaExpand />}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ type DynamicVideoPlayerProps = {
|
|||||||
onTimestampUpdate?: (timestamp: number) => void;
|
onTimestampUpdate?: (timestamp: number) => void;
|
||||||
onClipEnded?: () => void;
|
onClipEnded?: () => void;
|
||||||
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
||||||
|
onJumpToEvent?: (direction: "next" | "previous") => void;
|
||||||
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||||
toggleFullscreen: () => void;
|
toggleFullscreen: () => void;
|
||||||
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
@ -52,6 +53,7 @@ export default function DynamicVideoPlayer({
|
|||||||
onTimestampUpdate,
|
onTimestampUpdate,
|
||||||
onClipEnded,
|
onClipEnded,
|
||||||
onSeekToTime,
|
onSeekToTime,
|
||||||
|
onJumpToEvent,
|
||||||
setFullResolution,
|
setFullResolution,
|
||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
containerRef,
|
containerRef,
|
||||||
@ -280,6 +282,7 @@ export default function DynamicVideoPlayer({
|
|||||||
onSeekToTime(timestamp, play);
|
onSeekToTime(timestamp, play);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onJumpToEvent={onJumpToEvent}
|
||||||
onPlaying={() => {
|
onPlaying={() => {
|
||||||
if (isScrubbing) {
|
if (isScrubbing) {
|
||||||
playerRef.current?.pause();
|
playerRef.current?.pause();
|
||||||
|
|||||||
@ -300,6 +300,69 @@ export function RecordingView({
|
|||||||
[currentTimeRange, updateSelectedSegment],
|
[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(() => {
|
useEffect(() => {
|
||||||
if (!scrubbing) {
|
if (!scrubbing) {
|
||||||
if (Math.abs(currentTime - playerTime) > 10) {
|
if (Math.abs(currentTime - playerTime) > 10) {
|
||||||
@ -746,6 +809,7 @@ export function RecordingView({
|
|||||||
}}
|
}}
|
||||||
onClipEnded={onClipEnded}
|
onClipEnded={onClipEnded}
|
||||||
onSeekToTime={manuallySetCurrentTime}
|
onSeekToTime={manuallySetCurrentTime}
|
||||||
|
onJumpToEvent={onJumpToEvent}
|
||||||
onControllerReady={(controller) => {
|
onControllerReady={(controller) => {
|
||||||
mainControllerRef.current = controller;
|
mainControllerRef.current = controller;
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user