@@ -81,7 +81,7 @@ async function fetchLiveRuntimeState(force = false) {
8181
8282function getLiveRuntimePollMs ( ) {
8383 const carrotVisible = document . body ?. dataset ?. page === "carrot" && ! document . hidden ;
84- return carrotVisible ? 350 : 1500 ;
84+ return carrotVisible ? 1000 : 3000 ;
8585}
8686
8787function scheduleLiveRuntimeStateFetch ( ms = getLiveRuntimePollMs ( ) ) {
@@ -101,6 +101,7 @@ scheduleLiveRuntimeStateFetch(1500);
101101let RTC_PC = null ;
102102let RTC_RETRY_T = null ;
103103let RTC_WAIT_TRACK_T = null ;
104+ let RTC_FAIL_COUNT = 0 ;
104105function rtcHasLiveTrack ( ) {
105106 const video = document . getElementById ( "rtcVideo" ) ;
106107 return Boolean ( video && video . srcObject ) ;
@@ -133,10 +134,12 @@ async function rtcDisconnect() {
133134
134135function rtcScheduleRetry ( ms = 2000 ) {
135136 rtcCancelRetry ( ) ;
137+ const backoff = Math . min ( ms * Math . pow ( 1.5 , RTC_FAIL_COUNT ) , 30000 ) ;
138+ RTC_FAIL_COUNT = Math . min ( RTC_FAIL_COUNT + 1 , 20 ) ;
136139 RTC_RETRY_T = setTimeout ( async ( ) => {
137140 RTC_RETRY_T = null ;
138141 await rtcConnectOnce ( ) . catch ( ( ) => { } ) ;
139- } , ms ) ;
142+ } , backoff ) ;
140143}
141144
142145function rtcArmTrackTimeout ( ms = 5000 ) {
@@ -145,7 +148,7 @@ function rtcArmTrackTimeout(ms = 5000) {
145148 RTC_WAIT_TRACK_T = null ;
146149 rtcStatusSet ( "no track, retry..." ) ;
147150 await rtcDisconnect ( ) ;
148- rtcScheduleRetry ( 1000 ) ;
151+ rtcScheduleRetry ( 2000 ) ;
149152 } , ms ) ;
150153}
151154
@@ -171,9 +174,13 @@ async function waitIceComplete(pc, timeoutMs = 8000) {
171174 } ) ;
172175}
173176
177+ let _rtcConnecting = false ;
178+
174179async function rtcConnectOnce ( ) {
180+ if ( _rtcConnecting ) return ;
175181 if ( RTC_PC && ( RTC_PC . connectionState === "connected" || RTC_PC . connectionState === "connecting" ) ) return ;
176182
183+ _rtcConnecting = true ;
177184 try {
178185 await rtcDisconnect ( ) ;
179186 rtcStatusSet ( "connecting..." ) ;
@@ -207,12 +214,13 @@ async function rtcConnectOnce() {
207214 try { await videoEl . play ( ) ; } catch ( e ) { console . log ( "[RTC] play() failed" , e ) ; }
208215 rtcStatusSet ( "track: " + ev . track . kind ) ;
209216 rtcDisarmTrackTimeout ( ) ;
217+ RTC_FAIL_COUNT = 0 ;
210218 } ;
211219
212220 pc . onconnectionstatechange = ( ) => {
213221 const state = pc . connectionState ;
214- console . log ( "[RTC] connectionState:" , state ) ;
215222 rtcStatusSet ( "conn: " + state ) ;
223+ if ( state === "connected" ) RTC_FAIL_COUNT = 0 ;
216224 if ( state === "failed" || state === "disconnected" || state === "closed" ) {
217225 rtcDisconnect ( ) ;
218226 rtcScheduleRetry ( 2000 ) ;
@@ -221,7 +229,6 @@ async function rtcConnectOnce() {
221229
222230 pc . oniceconnectionstatechange = ( ) => {
223231 const state = pc . iceConnectionState ;
224- console . log ( "[RTC] iceConnectionState:" , state ) ;
225232 rtcStatusSet ( "ice: " + state ) ;
226233 if ( state === "failed" || state === "disconnected" || state === "closed" ) {
227234 rtcDisconnect ( ) ;
@@ -261,15 +268,15 @@ async function rtcConnectOnce() {
261268 rtcStatusSet ( "error: " + e . message ) ;
262269 await rtcDisconnect ( ) ;
263270 rtcScheduleRetry ( 2000 ) ;
264- throw e ;
271+ } finally {
272+ _rtcConnecting = false ;
265273 }
266274}
267275
268276async function waitServerReady ( timeoutMs = 8000 ) {
269277 const t0 = Date . now ( ) ;
270278 while ( Date . now ( ) - t0 < timeoutMs ) {
271279 try {
272- // ���� ����ִ����� Ȯ�� (������ API)
273280 const r = await fetch ( "/api/settings" , { cache : "no-store" } ) ;
274281 if ( r . ok ) return true ;
275282 } catch { }
@@ -278,15 +285,30 @@ async function waitServerReady(timeoutMs = 8000) {
278285 return false ;
279286}
280287
288+ window . CARROT_VISION_ACTIVE = false ;
289+
290+ window . CarrotVisionStart = async function ( ) {
291+ if ( window . CARROT_VISION_ACTIVE ) return ;
292+ window . CARROT_VISION_ACTIVE = true ;
293+
294+ const btn = document . getElementById ( "visionStartOverlay" ) ;
295+ if ( btn ) btn . style . display = "none" ;
296+
297+ rtcStatusSet ( "waiting server..." ) ;
298+ await waitServerReady ( 8000 ) ;
299+ await rtcConnectOnce ( ) . catch ( ( ) => { } ) ;
300+ } ;
301+
281302function rtcInitAuto ( ) {
282- ( async ( ) => {
283- rtcStatusSet ( "waiting server..." ) ;
284- await waitServerReady ( 8000 ) ; // �����ص� ��� ����
285- await rtcConnectOnce ( ) . catch ( ( ) => { } ) ;
286- } ) ( ) ;
303+ const btn = document . getElementById ( "btnStartVision" ) ;
304+ if ( btn ) btn . onclick = window . CarrotVisionStart ;
287305
288306 document . addEventListener ( "visibilitychange" , ( ) => {
289- if ( ! document . hidden ) rtcConnectOnce ( ) . catch ( ( ) => { } ) ;
307+ if ( ! document . hidden && ! _rtcConnecting && window . CARROT_VISION_ACTIVE ) {
308+ rtcCancelRetry ( ) ;
309+ RTC_FAIL_COUNT = 0 ;
310+ rtcConnectOnce ( ) . catch ( ( ) => { } ) ;
311+ }
290312 } ) ;
291313}
292314
@@ -539,6 +561,33 @@ function startAll() {
539561 rawOverlayConnectAll ( ) ;
540562}
541563
564+ /* ── Phase 1: visibilitychange tab pause + WS release ── */
565+ let _visibilityPaused = false ;
566+
567+ function _onVisibilityChange ( ) {
568+ if ( document . hidden ) {
569+ if ( _visibilityPaused ) return ;
570+ _visibilityPaused = true ;
571+ console . log ( "[perf] tab hidden → pausing" ) ;
572+ _hudStopRenderLoop ( ) ;
573+ rawOverlayDisconnectAll ( ) ;
574+ if ( LIVE_RUNTIME_FETCH_T ) {
575+ clearTimeout ( LIVE_RUNTIME_FETCH_T ) ;
576+ LIVE_RUNTIME_FETCH_T = null ;
577+ }
578+ } else {
579+ if ( ! _visibilityPaused ) return ;
580+ _visibilityPaused = false ;
581+ console . log ( "[perf] tab visible → resuming" ) ;
582+ _hudStartRenderLoop ( ) ;
583+ _hudMarkDirty ( ) ;
584+ rawOverlayConnectAll ( ) ;
585+ scheduleLiveRuntimeStateFetch ( 100 ) ;
586+ }
587+ }
588+
589+ document . addEventListener ( "visibilitychange" , _onVisibilityChange ) ;
590+
542591
543592
544593if ( document . readyState === "loading" ) {
0 commit comments