@@ -33,8 +33,31 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) {
3333 }
3434 } , [ ] )
3535
36+ const checkServerActive = useCallback ( async ( ) : Promise < boolean > => {
37+ try {
38+ const headers : Record < string , string > = { }
39+ if ( token ) {
40+ headers . Authorization = `Bearer ${ token } `
41+ }
42+ const controller = new AbortController ( )
43+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 2000 )
44+ const response = await fetch ( "/api/host/status" , {
45+ headers,
46+ signal : controller . signal ,
47+ } )
48+ clearTimeout ( timeoutId )
49+ return response . ok
50+ } catch {
51+ return false
52+ }
53+ } , [ token ] )
54+
3655 const triggerRetry = useCallback ( ( ) => {
37- if ( trackActiveRef . current || retryTimerRef . current ) return
56+ if ( retryTimerRef . current ) return
57+
58+ setTrackActive ( false )
59+ setVideoStream ( null )
60+
3861 console . log (
3962 "[WebRTC] Startup/transient network failure, retrying automatically in 2 seconds..." ,
4063 )
@@ -45,6 +68,15 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) {
4568 } , 2000 )
4669 } , [ ] )
4770
71+ const handleNetworkFailure = useCallback ( async ( ) => {
72+ const isServerOnline = await checkServerActive ( )
73+ if ( isServerOnline ) {
74+ triggerRetry ( )
75+ } else {
76+ setError ( "Server has quit or is unreachable" )
77+ }
78+ } , [ checkServerActive , triggerRetry ] )
79+
4880 const reconnect = ( ) => {
4981 if ( retryTimerRef . current ) {
5082 clearTimeout ( retryTimerRef . current )
@@ -84,9 +116,9 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) {
84116 } )
85117 . catch ( ( err ) => {
86118 console . error ( "[WebRTC] Session init failed:" , err )
87- triggerRetry ( )
119+ handleNetworkFailure ( )
88120 } )
89- } , [ token , reconnectAttempt , triggerRetry ] )
121+ } , [ token , reconnectAttempt , handleNetworkFailure ] )
90122
91123 useEffect ( ( ) => {
92124 if ( ! activeSessionId ) return
@@ -123,7 +155,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) {
123155 } ) ,
124156 } ) . catch ( ( err ) => {
125157 console . error ( err )
126- triggerRetry ( )
158+ handleNetworkFailure ( )
127159 } )
128160 }
129161
@@ -132,7 +164,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) {
132164 videoPc . connectionState === "failed" ||
133165 videoPc . connectionState === "disconnected"
134166 ) {
135- triggerRetry ( )
167+ handleNetworkFailure ( )
136168 }
137169 }
138170
@@ -166,7 +198,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) {
166198 } ) ,
167199 } ) . catch ( ( err ) => {
168200 console . error ( err )
169- triggerRetry ( )
201+ handleNetworkFailure ( )
170202 } )
171203 }
172204
@@ -175,7 +207,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) {
175207 inputPc . connectionState === "failed" ||
176208 inputPc . connectionState === "disconnected"
177209 ) {
178- triggerRetry ( )
210+ handleNetworkFailure ( )
179211 }
180212 }
181213
@@ -191,7 +223,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) {
191223
192224 sse . onerror = ( event ) => {
193225 console . error ( "[WebRTC] SSE error:" , event )
194- triggerRetry ( )
226+ handleNetworkFailure ( )
195227 }
196228
197229 sse . addEventListener ( "stream-error" , ( event ) => {
@@ -232,7 +264,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) {
232264 }
233265 } catch ( err ) {
234266 console . error ( "[WebRTC] Video offer handling failed:" , err )
235- triggerRetry ( )
267+ handleNetworkFailure ( )
236268 }
237269 } )
238270
@@ -278,7 +310,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) {
278310 }
279311 } catch ( err ) {
280312 console . error ( "[WebRTC] Input answer failed:" , err )
281- triggerRetry ( )
313+ handleNetworkFailure ( )
282314 }
283315 } )
284316
@@ -319,17 +351,51 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) {
319351
320352 sendInputOffer ( ) . catch ( ( err ) => {
321353 console . error ( err )
322- triggerRetry ( )
354+ handleNetworkFailure ( )
323355 } )
324356
357+ // Video stream watchdog to automatically recover from silent stream freezes
358+ let lastBytesReceived = 0
359+ let lastBytesTime = Date . now ( )
360+
361+ const statsInterval = setInterval ( async ( ) => {
362+ if ( ! trackActiveRef . current ) return
363+ try {
364+ const stats = await videoPc . getStats ( )
365+ let videoInbound = null
366+ for ( const report of stats . values ( ) ) {
367+ if ( report . type === "inbound-rtp" && report . kind === "video" ) {
368+ videoInbound = report
369+ break
370+ }
371+ }
372+ if ( videoInbound ) {
373+ const bytes = videoInbound . bytesReceived
374+ const now = Date . now ( )
375+ if ( bytes > lastBytesReceived ) {
376+ lastBytesReceived = bytes
377+ lastBytesTime = now
378+ } else if ( now - lastBytesTime > 4000 ) {
379+ console . warn (
380+ "[WebRTC] Video stream freeze detected (no bytes received for 4s), reconnecting..." ,
381+ )
382+ handleNetworkFailure ( )
383+ }
384+ }
385+ } catch ( err ) {
386+ console . error ( "[WebRTC] Failed to fetch stats:" , err )
387+ }
388+ } , 2000 )
389+
325390 return ( ) => {
391+ clearInterval ( statsInterval )
326392 sse . close ( )
327393 videoPc . close ( )
328394 inputPc . close ( )
329395 setTrackActive ( false )
330396 setVideoStream ( null )
331397 }
332- } , [ activeSessionId , token , registerDataChannel , triggerRetry ] )
398+ } , [ activeSessionId , token , registerDataChannel , handleNetworkFailure ] )
333399
334400 return { trackActive, videoStream, error, reconnect, sendInputEvent }
335401}
0 commit comments