1- import { Flex , Text , Button } from '@radix-ui/themes'
1+ import { Flex , Text , Button , Spinner } from '@radix-ui/themes'
22import {
33 VideoIcon ,
44 ReloadIcon ,
55 EnterFullScreenIcon ,
66 SpeakerLoudIcon ,
77 SpeakerOffIcon ,
8+ ExclamationTriangleIcon ,
89} from '@radix-ui/react-icons'
9- import { useEntity , useWebRTC } from '~/hooks'
10+ import { useEntity , useWebRTC , useIsConnecting } from '~/hooks'
1011import { memo , useMemo , useState , useRef , useCallback } from 'react'
1112import { SkeletonCard , ErrorDisplay , FullscreenModal } from './ui'
1213import { GridCardWithComponents as GridCard } from './GridCard'
1314import { useDashboardStore } from '~/store'
1415import { KeepAlive } from './KeepAlive'
16+ import './CameraCard.css'
1517
1618interface CameraCardProps {
1719 entityId : string
@@ -43,6 +45,8 @@ function CameraControls({
4345 supportsStream,
4446 isEditMode,
4547 isMuted,
48+ isReconnecting,
49+ hasFrameWarning,
4650 handleToggleMute,
4751 handleVideoFullscreen,
4852 size,
@@ -57,6 +61,8 @@ function CameraControls({
5761 supportsStream : boolean
5862 isEditMode : boolean
5963 isMuted : boolean
64+ isReconnecting : boolean
65+ hasFrameWarning : boolean
6066 handleToggleMute : ( e : React . MouseEvent ) => void
6167 handleVideoFullscreen : ( e : React . MouseEvent ) => void
6268 size : 'small' | 'medium' | 'large'
@@ -100,17 +106,31 @@ function CameraControls({
100106 lineHeight : 1.2 ,
101107 textTransform : 'uppercase' ,
102108 fontWeight : 500 ,
109+ display : 'flex' ,
110+ alignItems : 'center' ,
111+ gap : `${ 0.4 * scaleFactor } em` ,
103112 } }
104113 >
114+ { isReconnecting || ( supportsStream && ! isStreaming && ! streamError ) ? (
115+ < Spinner size = "1" />
116+ ) : hasFrameWarning && ! streamError ? (
117+ < ExclamationTriangleIcon style = { { color : '#f59e0b' , width : '1em' , height : '1em' } } />
118+ ) : ( isRecording || isStreaming ) && ! streamError ? (
119+ < span className = "recording-dot" />
120+ ) : null }
105121 { streamError
106122 ? 'ERROR'
107- : isRecording
108- ? 'RECORDING'
109- : isStreaming
110- ? 'STREAMING'
111- : isIdle
112- ? 'IDLE'
113- : entity . state . toUpperCase ( ) }
123+ : isReconnecting || ( supportsStream && ! isStreaming && ! streamError )
124+ ? 'CONNECTING'
125+ : hasFrameWarning
126+ ? 'NO SIGNAL'
127+ : supportsStream && isStreaming && ( isRecording || entity . state === 'streaming' )
128+ ? 'RECORDING'
129+ : supportsStream && isStreaming
130+ ? 'STREAMING'
131+ : isIdle
132+ ? 'IDLE'
133+ : entity . state . toUpperCase ( ) }
114134 </ div >
115135 </ div >
116136
@@ -191,6 +211,7 @@ function CameraCardComponent({
191211 const { entity, isConnected, isStale, isLoading : isEntityLoading } = useEntity ( entityId )
192212 const { mode } = useDashboardStore ( )
193213 const isEditMode = mode === 'edit'
214+ const isReconnecting = useIsConnecting ( )
194215 const [ isFullscreen , setIsFullscreen ] = useState ( false )
195216 const [ isMuted , setIsMuted ] = useState ( true ) // Start muted by default
196217 const normalContainerRef = useRef < HTMLDivElement > ( null )
@@ -221,6 +242,7 @@ function CameraCardComponent({
221242 isStreaming,
222243 error : streamError ,
223244 retry : retryStream ,
245+ hasFrameWarning,
224246 } = useWebRTC ( {
225247 entityId,
226248 enabled : webRTCEnabled ,
@@ -297,19 +319,18 @@ function CameraCardComponent({
297319 isUnavailable = { isUnavailable }
298320 onSelect = { ( ) => onSelect ?.( ! isSelected ) }
299321 onDelete = { onDelete }
300- title = { streamError || ( isStale ? 'Entity data may be outdated' : undefined ) }
322+ title = { streamError || undefined }
301323 className = "camera-card"
302324 style = { {
303325 backgroundColor :
304326 ( isRecording || isStreaming_ ) && ! isSelected && ! streamError
305327 ? 'var(--blue-3)'
306328 : undefined ,
307329 borderColor :
308- ( isRecording || isStreaming_ ) && ! isSelected && ! streamError && ! isStale
330+ ( isRecording || isStreaming_ ) && ! isSelected && ! streamError
309331 ? 'var(--blue-6)'
310332 : undefined ,
311- borderWidth :
312- isSelected || streamError || isRecording || isStreaming_ || isStale ? '2px' : '1px' ,
333+ borderWidth : isSelected || streamError || isRecording || isStreaming_ ? '2px' : '1px' ,
313334 } }
314335 >
315336 < div style = { { width : '100%' , height : '100%' , position : 'relative' } } >
@@ -402,13 +423,24 @@ function CameraCardComponent({
402423 } }
403424 />
404425 </ KeepAlive >
426+ { ! isStreaming && (
427+ < Flex
428+ align = "center"
429+ justify = "center"
430+ style = { {
431+ position : 'absolute' ,
432+ top : 0 ,
433+ left : 0 ,
434+ width : '100%' ,
435+ height : '100%' ,
436+ backgroundColor : 'var(--gray-3)' ,
437+ } }
438+ >
439+ < Spinner size = "3" />
440+ </ Flex >
441+ ) }
405442 </ >
406443 ) }
407- { ! isStreaming && ! streamError && (
408- < Text size = "2" color = "gray" >
409- Connecting...
410- </ Text >
411- ) }
412444 </ div >
413445 ) : (
414446 < Flex
@@ -420,12 +452,8 @@ function CameraCardComponent({
420452 < GridCard . Icon >
421453 < VideoIcon
422454 style = { {
423- color : isStale
424- ? 'var(--orange-9)'
425- : isRecording || isStreaming_
426- ? 'var(--blue-9)'
427- : 'var(--gray-9)' ,
428- opacity : isStale ? 0.6 : 1 ,
455+ color : isRecording || isStreaming_ ? 'var(--blue-9)' : 'var(--gray-9)' ,
456+ opacity : 1 ,
429457 transition : 'opacity 0.2s ease' ,
430458 width : 20 ,
431459 height : 20 ,
@@ -454,6 +482,8 @@ function CameraCardComponent({
454482 supportsStream = { supportsStream }
455483 isEditMode = { isEditMode }
456484 isMuted = { isMuted }
485+ isReconnecting = { isReconnecting }
486+ hasFrameWarning = { hasFrameWarning }
457487 handleToggleMute = { handleToggleMute }
458488 handleVideoFullscreen = { handleVideoFullscreen }
459489 size = { size }
@@ -508,6 +538,8 @@ function CameraCardComponent({
508538 supportsStream = { supportsStream }
509539 isEditMode = { isEditMode }
510540 isMuted = { isMuted }
541+ isReconnecting = { isReconnecting }
542+ hasFrameWarning = { hasFrameWarning }
511543 handleToggleMute = { handleToggleMute }
512544 handleVideoFullscreen = { handleVideoFullscreen }
513545 size = "large"
0 commit comments