@@ -114,6 +114,7 @@ const MoonlightStreamViewer: React.FC<MoonlightStreamViewerProps> = ({
114114 const [ isConnected , setIsConnected ] = useState ( false ) ;
115115 const [ error , setError ] = useState < string | null > ( null ) ;
116116 const [ status , setStatus ] = useState ( 'Initializing...' ) ;
117+ const [ isVisible , setIsVisible ] = useState ( false ) ; // Track if component is visible (for deferred connection)
117118 const [ isFullscreen , setIsFullscreen ] = useState ( false ) ;
118119 const [ audioEnabled , setAudioEnabled ] = useState ( true ) ;
119120 const [ pendingAutoJoin , setPendingAutoJoin ] = useState ( false ) ; // Wait for video before auto-join
@@ -129,7 +130,7 @@ const MoonlightStreamViewer: React.FC<MoonlightStreamViewerProps> = ({
129130 const manualBitrateSelectionTimeRef = useRef < number > ( 0 ) ; // Track when user manually selected bitrate (20s cooldown before auto-reduce)
130131 // Bandwidth recommendation state - instead of auto-switching, we show a recommendation popup
131132 const [ bitrateRecommendation , setBitrateRecommendation ] = useState < {
132- type : 'decrease' | 'increase' ;
133+ type : 'decrease' | 'increase' | 'screenshot' ;
133134 targetBitrate : number ;
134135 reason : string ;
135136 frameDrift ?: number ; // Current frame drift for decrease recommendations
@@ -1509,16 +1510,24 @@ const MoonlightStreamViewer: React.FC<MoonlightStreamViewerProps> = ({
15091510 return BITRATE_OPTIONS [ pessimisticIndex ] ;
15101511 } , [ ] ) ;
15111512
1512- // Auto-connect when wolfLobbyId becomes available
1513+ // Auto-connect when wolfLobbyId becomes available AND component is visible
15131514 // wolfLobbyId is fetched asynchronously from session data, so it's undefined on initial render
15141515 // If we connect before it's available, we use the wrong app_id (apps mode instead of lobbies mode)
1516+ // NEW: Wait for visibility before connecting (saves bandwidth when component not in view)
15151517 // NEW: Probe bandwidth FIRST, then connect at optimal bitrate (avoids reconnect on startup)
15161518 const hasConnectedRef = useRef ( false ) ;
15171519 const hasEverConnectedRef = useRef ( false ) ; // True after first successful connection (distinguishes initial vs reconnect)
15181520 useEffect ( ( ) => {
15191521 // Only auto-connect once
15201522 if ( hasConnectedRef . current ) return ;
15211523
1524+ // Wait for component to become visible before connecting
1525+ // This prevents wasting bandwidth on hidden tabs/components
1526+ if ( ! isVisible ) {
1527+ console . log ( '[MoonlightStreamViewer] Waiting for component to become visible before connecting...' ) ;
1528+ return ;
1529+ }
1530+
15221531 // If wolfLobbyId prop is expected but not yet loaded, wait for it
15231532 // We detect this by checking if sessionId is provided (external agent mode)
15241533 // In this mode, wolfLobbyId should be provided by the parent once session data loads
@@ -1553,7 +1562,7 @@ const MoonlightStreamViewer: React.FC<MoonlightStreamViewerProps> = ({
15531562
15541563 probeAndConnect ( ) ;
15551564 // eslint-disable-next-line react-hooks/exhaustive-deps
1556- } , [ wolfLobbyId , sessionId ] ) ; // Only trigger on props, not on function identity changes
1565+ } , [ wolfLobbyId , sessionId , isVisible ] ) ; // Only trigger on props and visibility , not on function identity changes
15571566
15581567 // Cleanup on unmount
15591568 useEffect ( ( ) => {
@@ -1898,6 +1907,31 @@ const MoonlightStreamViewer: React.FC<MoonlightStreamViewerProps> = ({
18981907 }
18991908 }
19001909 }
1910+ } else if ( congestionDetected && currentBitrate === MIN_BITRATE ) {
1911+ // Already at minimum bitrate but still experiencing congestion
1912+ // Recommend switching to screenshot mode for better reliability
1913+ congestionCheckCountRef . current ++ ;
1914+ stableCheckCountRef . current = 0 ;
1915+
1916+ if ( congestionCheckCountRef . current >= CONGESTION_CHECKS_FOR_REDUCE ) {
1917+ const timeSinceLastChange = now - lastBitrateChangeRef . current ;
1918+
1919+ if ( timeSinceLastChange > REDUCE_COOLDOWN_MS ) {
1920+ console . log ( `[AdaptiveBitrate] At minimum bitrate (${ MIN_BITRATE } Mbps) but still experiencing congestion (${ frameDrift . toFixed ( 0 ) } ms drift), recommending screenshot mode` ) ;
1921+
1922+ setBitrateRecommendation ( {
1923+ type : 'screenshot' ,
1924+ targetBitrate : MIN_BITRATE , // Keep same bitrate, just switch mode
1925+ reason : `Video streaming is struggling even at ${ MIN_BITRATE } Mbps` ,
1926+ frameDrift : frameDrift ,
1927+ } ) ;
1928+
1929+ lastBitrateChangeRef . current = now ;
1930+ stableCheckCountRef . current = 0 ;
1931+ congestionCheckCountRef . current = 0 ;
1932+ return ;
1933+ }
1934+ }
19011935 } else {
19021936 // Low frame drift - connection is stable at current bitrate
19031937 congestionCheckCountRef . current = 0 ; // Reset congestion counter on good sample
@@ -2010,6 +2044,28 @@ const MoonlightStreamViewer: React.FC<MoonlightStreamViewerProps> = ({
20102044 return ( ) => resizeObserver . disconnect ( ) ;
20112045 } , [ ] ) ;
20122046
2047+ // Track visibility for deferred connection - only connect when component is visible
2048+ // This saves bandwidth and avoids connection issues on high-latency networks
2049+ useEffect ( ( ) => {
2050+ const container = containerRef . current ;
2051+ if ( ! container ) return ;
2052+
2053+ const observer = new IntersectionObserver (
2054+ ( entries ) => {
2055+ const entry = entries [ 0 ] ;
2056+ if ( entry . isIntersecting && ! isVisible ) {
2057+ console . log ( '[MoonlightStreamViewer] Component became visible - will trigger connection' ) ;
2058+ setIsVisible ( true ) ;
2059+ }
2060+ // Note: we don't set isVisible=false when hidden - once connected, stay connected
2061+ } ,
2062+ { threshold : 0.1 } // Trigger when 10% visible
2063+ ) ;
2064+
2065+ observer . observe ( container ) ;
2066+ return ( ) => observer . disconnect ( ) ;
2067+ } , [ isVisible ] ) ;
2068+
20132069 // Calculate proper canvas display size to maintain aspect ratio
20142070 useEffect ( ( ) => {
20152071 if ( ! containerSize || ! canvasRef . current ) return ;
@@ -3062,35 +3118,48 @@ const MoonlightStreamViewer: React.FC<MoonlightStreamViewerProps> = ({
30623118 < Button
30633119 size = "small"
30643120 onClick = { ( ) => {
3065- setUserBitrate ( bitrateRecommendation . targetBitrate ) ;
3121+ if ( bitrateRecommendation . type === 'screenshot' ) {
3122+ // Switch to screenshot mode
3123+ setQualityMode ( 'low' ) ;
3124+ addChartEvent ( 'reduce' , 'User switched to screenshot mode' ) ;
3125+ } else {
3126+ // Change bitrate
3127+ setUserBitrate ( bitrateRecommendation . targetBitrate ) ;
3128+ addChartEvent (
3129+ bitrateRecommendation . type === 'decrease' ? 'reduce' : 'increase' ,
3130+ `User accepted: ${ userBitrate ?? requestedBitrate } →${ bitrateRecommendation . targetBitrate } Mbps`
3131+ ) ;
3132+ }
30663133 manualBitrateSelectionTimeRef . current = Date . now ( ) ;
3067- addChartEvent (
3068- bitrateRecommendation . type === 'decrease' ? 'reduce' : 'increase' ,
3069- `User accepted: ${ userBitrate ?? requestedBitrate } →${ bitrateRecommendation . targetBitrate } Mbps`
3070- ) ;
30713134 setBitrateRecommendation ( null ) ;
30723135 } }
30733136 sx = { {
3074- backgroundColor : bitrateRecommendation . type === 'decrease'
3075- ? 'rgba(255, 152, 0, 0.9)'
3076- : 'rgba(76, 175, 80, 0.9)' ,
3077- color : bitrateRecommendation . type === 'decrease' ? 'black' : 'white' ,
3137+ backgroundColor : bitrateRecommendation . type === 'screenshot'
3138+ ? 'rgba(244, 67, 54, 0.9)' // Red for screenshot recommendation
3139+ : bitrateRecommendation . type === 'decrease'
3140+ ? 'rgba(255, 152, 0, 0.9)'
3141+ : 'rgba(76, 175, 80, 0.9)' ,
3142+ color : 'white' ,
30783143 fontSize : '0.65rem' ,
30793144 px : 1 ,
30803145 py : 0.25 ,
30813146 minWidth : 'auto' ,
30823147 textTransform : 'none' ,
30833148 borderRadius : 1 ,
30843149 '&:hover' : {
3085- backgroundColor : bitrateRecommendation . type === 'decrease'
3086- ? 'rgba(255, 152, 0, 1)'
3087- : 'rgba(76, 175, 80, 1)' ,
3150+ backgroundColor : bitrateRecommendation . type === 'screenshot'
3151+ ? 'rgba(244, 67, 54, 1)'
3152+ : bitrateRecommendation . type === 'decrease'
3153+ ? 'rgba(255, 152, 0, 1)'
3154+ : 'rgba(76, 175, 80, 1)' ,
30883155 } ,
30893156 } }
30903157 >
3091- { bitrateRecommendation . type === 'decrease'
3092- ? `Slow connection · Try ${ bitrateRecommendation . targetBitrate } M`
3093- : `Improved · Try ${ bitrateRecommendation . targetBitrate } M` }
3158+ { bitrateRecommendation . type === 'screenshot'
3159+ ? 'Struggling · Try screenshots'
3160+ : bitrateRecommendation . type === 'decrease'
3161+ ? `Slow connection · Try ${ bitrateRecommendation . targetBitrate } M`
3162+ : `Improved · Try ${ bitrateRecommendation . targetBitrate } M` }
30943163 </ Button >
30953164 </ Tooltip >
30963165 ) }
0 commit comments