Add event navigation functionality to video player components

This commit is contained in:
Ran Mizrachi 2025-10-26 10:35:58 +02:00
parent 2c480b9a89
commit 0f0d6bd83a
4 changed files with 96 additions and 0 deletions

View File

@ -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}

View File

@ -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 />}

View File

@ -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();

View File

@ -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;
}}