@@ -45,6 +45,7 @@ const DEFAULT_ICE_SERVERS: RTCIceServer[] = [
4545export interface ViewerConnection {
4646 id : string ;
4747 peerConnection : RTCPeerConnection ;
48+ hostAudioSender : RTCRtpSender | null ;
4849 dataChannel : RTCDataChannel | null ;
4950 connectionState : ConnectionState ;
5051 controlState : 'view-only' | 'requested' | 'granted' ;
@@ -136,6 +137,36 @@ export function useWebRTCHostAPI({
136137 onInputReceivedRef . current = onInputReceived ;
137138 onCursorUpdateRef . current = onCursorUpdate ;
138139
140+ const getPreferredHostAudioTrack = useCallback (
141+ ( streamOverride ?: MediaStream | null ) : MediaStreamTrack | null => {
142+ const stream = streamOverride ?? localStreamRef . current ;
143+ const streamAudioTrack = stream ?. getAudioTracks ( ) [ 0 ] ?? null ;
144+ if ( streamAudioTrack ) return streamAudioTrack ;
145+ return hostMicStreamRef . current ?. getAudioTracks ( ) [ 0 ] ?? null ;
146+ } ,
147+ [ ]
148+ ) ;
149+
150+ const syncHostAudioSender = useCallback (
151+ async ( viewer : ViewerConnection , preferredTrack : MediaStreamTrack | null ) => {
152+ const pc = viewer . peerConnection ;
153+
154+ if ( preferredTrack ) {
155+ if ( viewer . hostAudioSender ) {
156+ if ( viewer . hostAudioSender . track ?. id !== preferredTrack . id ) {
157+ await viewer . hostAudioSender . replaceTrack ( preferredTrack ) ;
158+ }
159+ } else {
160+ viewer . hostAudioSender = pc . addTrack ( preferredTrack , new MediaStream ( [ preferredTrack ] ) ) ;
161+ }
162+ } else if ( viewer . hostAudioSender ) {
163+ pc . removeTrack ( viewer . hostAudioSender ) ;
164+ viewer . hostAudioSender = null ;
165+ }
166+ } ,
167+ [ ]
168+ ) ;
169+
139170 // Send signal via API
140171 const sendSignal = useCallback (
141172 async ( signal : SignalMessage ) => {
@@ -365,7 +396,7 @@ export function useWebRTCHostAPI({
365396
366397 // Create peer connection for a viewer
367398 const createPeerConnection = useCallback (
368- ( viewerId : string ) : RTCPeerConnection => {
399+ ( viewerId : string ) : { pc : RTCPeerConnection ; hostAudioSender : RTCRtpSender | null } => {
369400 console . log ( '[WebRTCHost] Creating peer connection for viewer:' , viewerId ) ;
370401
371402 const pc = new RTCPeerConnection ( {
@@ -406,11 +437,13 @@ export function useWebRTCHostAPI({
406437 }
407438
408439 // Add host mic track so the viewer can hear the host
409- const hostMic = hostMicStreamRef . current ;
410- if ( hostMic ) {
411- hostMic . getAudioTracks ( ) . forEach ( ( track ) => {
412- pc . addTrack ( track , hostMic ) ;
413- } ) ;
440+ let hostAudioSender : RTCRtpSender | null = null ;
441+ const preferredHostAudioTrack = getPreferredHostAudioTrack ( currentStream ) ;
442+ if ( preferredHostAudioTrack ) {
443+ hostAudioSender = pc . addTrack (
444+ preferredHostAudioTrack ,
445+ new MediaStream ( [ preferredHostAudioTrack ] )
446+ ) ;
414447 }
415448
416449 // Handle incoming tracks from viewer (their mic audio)
@@ -502,9 +535,15 @@ export function useWebRTCHostAPI({
502535 handleDataChannelMessage ( viewerId , event ) ;
503536 } ;
504537
505- return pc ;
538+ return { pc , hostAudioSender } ;
506539 } ,
507- [ hostId , handleDataChannelMessage , sendSignal , relayAudioToOtherViewers ]
540+ [
541+ getPreferredHostAudioTrack ,
542+ hostId ,
543+ handleDataChannelMessage ,
544+ sendSignal ,
545+ relayAudioToOtherViewers ,
546+ ]
508547 ) ;
509548
510549 // Remove a viewer
@@ -538,11 +577,12 @@ export function useWebRTCHostAPI({
538577
539578 console . log ( '[WebRTCHost] Viewer joining:' , viewerId ) ;
540579
541- const pc = createPeerConnection ( viewerId ) ;
580+ const { pc , hostAudioSender } = createPeerConnection ( viewerId ) ;
542581
543582 const viewer : ViewerConnection = {
544583 id : viewerId ,
545584 peerConnection : pc ,
585+ hostAudioSender,
546586 dataChannel : null ,
547587 connectionState : 'connecting' ,
548588 controlState : 'view-only' ,
@@ -813,6 +853,7 @@ export function useWebRTCHostAPI({
813853 async ( stream : MediaStream ) => {
814854 localStreamRef . current = stream ;
815855 const videoTracks = stream . getVideoTracks ( ) ;
856+ const preferredHostAudioTrack = getPreferredHostAudioTrack ( stream ) ;
816857 console . log ( '[WebRTCHost] Publishing stream:' , {
817858 videoTracks : videoTracks . length ,
818859 audioTracks : stream . getAudioTracks ( ) . length ,
@@ -848,6 +889,7 @@ export function useWebRTCHostAPI({
848889
849890 try {
850891 const pc = viewer . peerConnection ;
892+ await syncHostAudioSender ( viewer , preferredHostAudioTrack ) ;
851893 const existingVideoSenders = pc
852894 . getSenders ( )
853895 . filter ( ( sender ) => sender . track ?. kind === 'video' ) ;
@@ -886,7 +928,7 @@ export function useWebRTCHostAPI({
886928 }
887929 }
888930 } ,
889- [ hostId , sendSignal ]
931+ [ getPreferredHostAudioTrack , hostId , sendSignal , syncHostAudioSender ]
890932 ) ;
891933
892934 // Unpublish the screen share stream (remove video tracks) without closing connections
@@ -898,14 +940,16 @@ export function useWebRTCHostAPI({
898940 continue ;
899941
900942 try {
901- // Remove video senders (keep audio -- mic stays )
943+ // Remove video senders (keep audio path active )
902944 const senders = viewer . peerConnection . getSenders ( ) ;
903945 for ( const sender of senders ) {
904946 if ( sender . track ?. kind === 'video' ) {
905947 viewer . peerConnection . removeTrack ( sender ) ;
906948 }
907949 }
908950
951+ await syncHostAudioSender ( viewer , getPreferredHostAudioTrack ( null ) ) ;
952+
909953 // Renegotiate so viewer sees track removal
910954 const offer = await viewer . peerConnection . createOffer ( ) ;
911955 await viewer . peerConnection . setLocalDescription ( offer ) ;
@@ -923,7 +967,7 @@ export function useWebRTCHostAPI({
923967 console . error ( `[WebRTCHost] Failed to unpublish stream from ${ viewer . id } :` , err ) ;
924968 }
925969 }
926- } , [ hostId , sendSignal ] ) ;
970+ } , [ getPreferredHostAudioTrack , hostId , sendSignal , syncHostAudioSender ] ) ;
927971
928972 // Cleanup on unmount
929973 useEffect ( ( ) => {
0 commit comments