@@ -45,6 +45,7 @@ import {
45
45
FaMicrophoneSlash ,
46
46
} from "react-icons/fa" ;
47
47
import { GiSpeaker , GiSpeakerOff } from "react-icons/gi" ;
48
+ import { HiViewfinderCircle } from "react-icons/hi2" ;
48
49
import { IoMdArrowBack } from "react-icons/io" ;
49
50
import { LuEar , LuEarOff , LuVideo , LuVideoOff } from "react-icons/lu" ;
50
51
import {
@@ -82,6 +83,45 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
82
83
) ;
83
84
const { payload : audioState , send : sendAudio } = useAudioState ( camera . name ) ;
84
85
86
+ // click overlay for ptzs
87
+
88
+ const [ clickOverlay , setClickOverlay ] = useState ( false ) ;
89
+ const clickOverlayRef = useRef < HTMLDivElement > ( null ) ;
90
+ const { send : sendPtz } = usePtzCommand ( camera . name ) ;
91
+
92
+ const handleOverlayClick = useCallback (
93
+ (
94
+ e : React . MouseEvent < HTMLDivElement > | React . TouchEvent < HTMLDivElement > ,
95
+ ) => {
96
+ if ( ! clickOverlay ) {
97
+ return ;
98
+ }
99
+
100
+ let clientX ;
101
+ let clientY ;
102
+ if ( isMobile && e . nativeEvent instanceof TouchEvent ) {
103
+ clientX = e . nativeEvent . touches [ 0 ] . clientX ;
104
+ clientY = e . nativeEvent . touches [ 0 ] . clientY ;
105
+ } else if ( e . nativeEvent instanceof MouseEvent ) {
106
+ clientX = e . nativeEvent . clientX ;
107
+ clientY = e . nativeEvent . clientY ;
108
+ }
109
+
110
+ if ( clickOverlayRef . current && clientX && clientY ) {
111
+ const rect = clickOverlayRef . current . getBoundingClientRect ( ) ;
112
+
113
+ const normalizedX = ( clientX - rect . left ) / rect . width ;
114
+ const normalizedY = ( clientY - rect . top ) / rect . height ;
115
+
116
+ const pan = ( normalizedX - 0.5 ) * 2 ;
117
+ const tilt = ( 0.5 - normalizedY ) * 2 ;
118
+
119
+ sendPtz ( `move_relative_${ pan } _${ tilt } ` ) ;
120
+ }
121
+ } ,
122
+ [ clickOverlayRef , clickOverlay , sendPtz ] ,
123
+ ) ;
124
+
85
125
// fullscreen state
86
126
87
127
useEffect ( ( ) => {
@@ -277,6 +317,8 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
277
317
>
278
318
< div
279
319
className = { `flex flex-col justify-center items-center ${ growClassName } ` }
320
+ ref = { clickOverlayRef }
321
+ onClick = { handleOverlayClick }
280
322
style = { {
281
323
aspectRatio : aspectRatio ,
282
324
} }
@@ -293,14 +335,28 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
293
335
preferredLiveMode = { preferredLiveMode }
294
336
/>
295
337
</ div >
296
- { camera . onvif . host != "" && < PtzControlPanel camera = { camera . name } /> }
338
+ { camera . onvif . host != "" && (
339
+ < PtzControlPanel
340
+ camera = { camera . name }
341
+ clickOverlay = { clickOverlay }
342
+ setClickOverlay = { setClickOverlay }
343
+ />
344
+ ) }
297
345
</ TransformComponent >
298
346
</ div >
299
347
</ TransformWrapper >
300
348
) ;
301
349
}
302
350
303
- function PtzControlPanel ( { camera } : { camera : string } ) {
351
+ function PtzControlPanel ( {
352
+ camera,
353
+ clickOverlay,
354
+ setClickOverlay,
355
+ } : {
356
+ camera : string ;
357
+ clickOverlay : boolean ;
358
+ setClickOverlay : React . Dispatch < React . SetStateAction < boolean > > ;
359
+ } ) {
304
360
const { data : ptz } = useSWR < CameraPtzInfo > ( `${ camera } /ptz/info` ) ;
305
361
306
362
const { send : sendPtz } = usePtzCommand ( camera ) ;
@@ -442,6 +498,16 @@ function PtzControlPanel({ camera }: { camera: string }) {
442
498
</ Button >
443
499
</ >
444
500
) }
501
+ { ptz ?. features ?. includes ( "pt-r-fov" ) && (
502
+ < >
503
+ < Button
504
+ className = { `${ clickOverlay ? "text-selected" : "text-primary-foreground" } ` }
505
+ onClick = { ( ) => setClickOverlay ( ! clickOverlay ) }
506
+ >
507
+ < HiViewfinderCircle />
508
+ </ Button >
509
+ </ >
510
+ ) }
445
511
{ ( ptz ?. presets ?. length ?? 0 ) > 0 && (
446
512
< DropdownMenu >
447
513
< DropdownMenuTrigger asChild >
0 commit comments