diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 8ef5f1c42..55caa0d65 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -22,6 +22,11 @@ import { ASPECT_VERTICAL_LAYOUT, RecordingPlayerError } from "@/types/record"; import { useTranslation } from "react-i18next"; import ObjectTrackOverlay from "@/components/overlay/ObjectTrackOverlay"; import { useIsAdmin } from "@/hooks/use-is-admin"; +import { + downloadSnapshot, + generateSnapshotFilename, + grabVideoSnapshot, +} from "@/utils/snapshotUtil"; // Android native hls does not seek correctly const USE_NATIVE_HLS = false; @@ -89,7 +94,7 @@ export default function HlsVideoPlayer({ currentTimeOverride, transformedOverlay, }: HlsVideoPlayerProps) { - const { t } = useTranslation("components/player"); + const { t } = useTranslation(["components/player", "views/live"]); const { data: config } = useSWR("config"); const isAdmin = useIsAdmin(); @@ -317,6 +322,7 @@ export default function HlsVideoPlayer({ seek: true, playbackRate: true, plusUpload: isAdmin && config?.plus?.enabled == true, + snapshot: true, fullscreen: supportsFullscreen, }} setControlsOpen={setControlsOpen} diff --git a/web/src/components/player/VideoControls.tsx b/web/src/components/player/VideoControls.tsx index 020c54d7b..2995f9fe3 100644 --- a/web/src/components/player/VideoControls.tsx +++ b/web/src/components/player/VideoControls.tsx @@ -34,12 +34,14 @@ import { import { cn } from "@/lib/utils"; import { FaCompress, FaExpand } from "react-icons/fa"; import { useTranslation } from "react-i18next"; +import { TbCameraDown } from "react-icons/tb"; type VideoControls = { volume?: boolean; seek?: boolean; playbackRate?: boolean; plusUpload?: boolean; + snapshot?: boolean; fullscreen?: boolean; }; @@ -48,6 +50,7 @@ const CONTROLS_DEFAULT: VideoControls = { seek: true, playbackRate: true, plusUpload: false, + snapshot: false, fullscreen: false, }; const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16]; @@ -71,6 +74,8 @@ type VideoControlsProps = { onSeek: (diff: number) => void; onSetPlaybackRate: (rate: number) => void; onUploadFrame?: () => void; + onSnapshot?: () => void; + snapshotTitle?: string; toggleFullscreen?: () => void; containerRef?: React.MutableRefObject; }; @@ -92,6 +97,8 @@ export default function VideoControls({ onSeek, onSetPlaybackRate, onUploadFrame, + onSnapshot, + snapshotTitle, toggleFullscreen, containerRef, }: VideoControlsProps) { @@ -292,6 +299,17 @@ export default function VideoControls({ fullscreen={fullscreen} /> )} + {features.snapshot && onSnapshot && ( + ) => { + e.stopPropagation(); + onSnapshot(); + }} + /> + )} {features.fullscreen && toggleFullscreen && (
{fullscreen ? : } diff --git a/web/src/utils/snapshotUtil.ts b/web/src/utils/snapshotUtil.ts index 16c11d62a..f892bcdf7 100644 --- a/web/src/utils/snapshotUtil.ts +++ b/web/src/utils/snapshotUtil.ts @@ -119,12 +119,13 @@ export function generateSnapshotFilename( return `${cameraName}_snapshot_${safeTimestamp}.jpg`; } -export async function grabVideoSnapshot(): Promise { +export async function grabVideoSnapshot( + targetVideo?: HTMLVideoElement | null, +): Promise { try { - // Find the video element in the player - const videoElement = document.querySelector( - "#player-container video", - ) as HTMLVideoElement; + const videoElement = + targetVideo ?? + (document.querySelector("#player-container video") as HTMLVideoElement); if (!videoElement) { return {