@@ -22,6 +22,11 @@ import { ASPECT_VERTICAL_LAYOUT, RecordingPlayerError } from "@/types/record";
2222import { useTranslation } from "react-i18next" ;
2323import ObjectTrackOverlay from "@/components/overlay/ObjectTrackOverlay" ;
2424import { useIsAdmin } from "@/hooks/use-is-admin" ;
25+ import {
26+ downloadSnapshot ,
27+ generateSnapshotFilename ,
28+ grabVideoSnapshot ,
29+ } from "@/utils/snapshotUtil" ;
2530
2631// Android native hls does not seek correctly
2732const USE_NATIVE_HLS = false ;
@@ -58,6 +63,7 @@ type HlsVideoPlayerProps = {
5863 isDetailMode ?: boolean ;
5964 camera ?: string ;
6065 currentTimeOverride ?: number ;
66+ supportsSnapshot ?: boolean ;
6167 transformedOverlay ?: ReactNode ;
6268} ;
6369
@@ -83,9 +89,10 @@ export default function HlsVideoPlayer({
8389 isDetailMode = false ,
8490 camera,
8591 currentTimeOverride,
92+ supportsSnapshot = false ,
8693 transformedOverlay,
8794} : HlsVideoPlayerProps ) {
88- const { t } = useTranslation ( "components/player" ) ;
95+ const { t } = useTranslation ( [ "components/player" , "views/live" ] ) ;
8996 const { data : config } = useSWR < FrigateConfig > ( "config" ) ;
9097 const isAdmin = useIsAdmin ( ) ;
9198
@@ -264,13 +271,36 @@ export default function HlsVideoPlayer({
264271 const getVideoTime = useCallback ( ( ) => {
265272 const currentTime = videoRef . current ?. currentTime ;
266273
267- if ( ! currentTime ) {
274+ if ( currentTime == undefined ) {
268275 return undefined ;
269276 }
270277
271278 return currentTime + inpointOffset ;
272279 } , [ videoRef , inpointOffset ] ) ;
273280
281+ const handleSnapshot = useCallback ( async ( ) => {
282+ const frameTime = getVideoTime ( ) ;
283+ const result = await grabVideoSnapshot ( videoRef . current ) ;
284+
285+ if ( result . success ) {
286+ downloadSnapshot (
287+ result . data . dataUrl ,
288+ generateSnapshotFilename (
289+ camera ?? "recording" ,
290+ currentTime ?? frameTime ,
291+ config ?. ui ?. timezone ,
292+ ) ,
293+ ) ;
294+ toast . success ( t ( "snapshot.downloadStarted" , { ns : "views/live" } ) , {
295+ position : "top-center" ,
296+ } ) ;
297+ } else {
298+ toast . error ( t ( "snapshot.captureFailed" , { ns : "views/live" } ) , {
299+ position : "top-center" ,
300+ } ) ;
301+ }
302+ } , [ camera , config ?. ui ?. timezone , currentTime , getVideoTime , t , videoRef ] ) ;
303+
274304 return (
275305 < TransformWrapper
276306 minScale = { 1.0 }
@@ -294,6 +324,7 @@ export default function HlsVideoPlayer({
294324 seek : true ,
295325 playbackRate : true ,
296326 plusUpload : isAdmin && config ?. plus ?. enabled == true ,
327+ snapshot : supportsSnapshot ,
297328 fullscreen : supportsFullscreen ,
298329 } }
299330 setControlsOpen = { setControlsOpen }
@@ -320,7 +351,7 @@ export default function HlsVideoPlayer({
320351 onUploadFrame = { async ( ) => {
321352 const frameTime = getVideoTime ( ) ;
322353
323- if ( frameTime && onUploadFrame ) {
354+ if ( frameTime != undefined && onUploadFrame ) {
324355 const resp = await onUploadFrame ( frameTime ) ;
325356
326357 if ( resp && resp . status == 200 ) {
@@ -334,6 +365,8 @@ export default function HlsVideoPlayer({
334365 }
335366 }
336367 } }
368+ onSnapshot = { supportsSnapshot ? handleSnapshot : undefined }
369+ snapshotTitle = { t ( "snapshot.takeSnapshot" , { ns : "views/live" } ) }
337370 fullscreen = { fullscreen }
338371 toggleFullscreen = { toggleFullscreen }
339372 containerRef = { containerRef }
@@ -465,7 +498,7 @@ export default function HlsVideoPlayer({
465498
466499 const frameTime = getVideoTime ( ) ;
467500
468- if ( frameTime ) {
501+ if ( frameTime != undefined ) {
469502 onTimeUpdate ( frameTime ) ;
470503 }
471504 } }
0 commit comments