@@ -25,6 +25,13 @@ export default function WebRTCStreamer() {
2525 configRef . current = { webrtcUrl, providerType } ;
2626 } , [ webrtcUrl , providerType ] ) ;
2727
28+ // Keep the latest gameData accessible without re-running the connection-lifecycle
29+ // effect below on every NUI push (its identity changes on each message).
30+ const gameDataRef = useRef ( gameData ) ;
31+ useEffect ( ( ) => {
32+ gameDataRef . current = gameData ;
33+ } , [ gameData ] ) ;
34+
2835 useEffect ( ( ) => {
2936 let rendererInstance : any = null ;
3037
@@ -48,8 +55,10 @@ export default function WebRTCStreamer() {
4855 } ;
4956
5057 const initializeConnection = async ( targetViewerId : string ) => {
58+ let localPc : RTCPeerConnection | null = null ;
59+ let streamStarted = false ;
5160 try {
52- let myId = ( gameData as any ) . selfId ;
61+ let myId = ( gameDataRef . current as any ) . selfId ;
5362 if ( ! myId ) {
5463 try {
5564 const res = await sendNui ( 'getSelfId' , { } , 0 ) ;
@@ -70,6 +79,7 @@ export default function WebRTCStreamer() {
7079 const { default : renderer } = await import ( '@/utils/fivem-renderer' ) ;
7180 rendererInstance = renderer ;
7281 stream = ( renderer as any ) . startStream ( ) as MediaStream ;
82+ streamStarted = true ;
7383 } catch ( err ) {
7484 console . error ( '[WebRTC Streamer] Failed to load renderer:' , err ) ;
7585 return ;
@@ -78,6 +88,7 @@ export default function WebRTCStreamer() {
7888 if ( configRef . current . providerType === 'cloudflare-sfu' ) {
7989 const { publishToCF } = await import ( '@/utils/cf-sfu' ) ;
8090 const { pc, sessionId, trackName } = await publishToCF ( stream ! ) ;
91+ localPc = pc ;
8192 const signalingUnsub = ( ) => { } ;
8293 connectionsRef . current . set ( targetViewerId , { pc, signalingUnsub } ) ;
8394 signaling . send ( {
@@ -91,6 +102,7 @@ export default function WebRTCStreamer() {
91102 }
92103
93104 const pc = new RTCPeerConnection ( RTC_CONFIG ) ;
105+ localPc = pc ;
94106 stream ! . getTracks ( ) . forEach ( track => pc . addTrack ( track , stream ! ) ) ;
95107
96108 pc . onconnectionstatechange = ( ) => {
@@ -153,7 +165,16 @@ export default function WebRTCStreamer() {
153165 } ) ;
154166 } catch ( err ) {
155167 console . error ( '[WebRTC Streamer] Init failed:' , err ) ;
168+ // If the connection was already registered, cleanup() closes the pc
169+ // and stops the renderer stream via the map entry. If it failed
170+ // *before* connectionsRef.set(...), no map entry exists — tear down
171+ // the locally-created pc/renderer here so they don't leak.
172+ const hadEntry = connectionsRef . current . has ( targetViewerId ) ;
156173 cleanup ( targetViewerId ) ;
174+ if ( ! hadEntry ) {
175+ if ( localPc ) localPc . close ( ) ;
176+ if ( streamStarted && rendererInstance ) rendererInstance . stopStream ( ) ;
177+ }
157178 } finally {
158179 startingViewersRef . current . delete ( targetViewerId ) ;
159180 }
@@ -172,6 +193,10 @@ export default function WebRTCStreamer() {
172193 old . pc . close ( ) ;
173194 old . signalingUnsub ( ) ;
174195 connectionsRef . current . delete ( targetViewerId ) ;
196+ // Release the renderer watcher held by the previous connection
197+ // before initializeConnection() calls startStream() again — otherwise
198+ // the watcher counter leaks and the render loop never stops.
199+ if ( rendererInstance ) rendererInstance . stopStream ( ) ;
175200 }
176201 }
177202
@@ -192,7 +217,7 @@ export default function WebRTCStreamer() {
192217 off ( 'StartWebRTC' , startHandler ) ;
193218 off ( 'StopWebRTC' , stopHandler ) ;
194219 } ;
195- } , [ on , off , sendNui , gameData ] ) ;
220+ } , [ on , off , sendNui ] ) ;
196221
197222 return null ;
198223}
0 commit comments