@@ -28,6 +28,7 @@ import { useScreenCapture, type CaptureQuality } from '@/hooks/useScreenCapture'
2828import { useRecording , formatDuration , type RecordingQuality } from '@/hooks/useRecording' ;
2929import { useWebRTCHost } from '@/hooks/useWebRTCHost' ;
3030import { useWebRTCHostSFU } from '@/hooks/useWebRTCHostSFU' ;
31+ import { useAudioMixer } from '@/hooks/useAudioMixer' ;
3132import type { SessionParticipant } from '@pairux/shared-types' ;
3233import { Logo } from '@/components/Logo' ;
3334
@@ -51,6 +52,12 @@ interface ApiResponse<T> {
5152 error ?: string ;
5253}
5354
55+ // Minimal viewer info needed for audio mixing
56+ interface HostedViewer {
57+ audioTrack : MediaStreamTrack | null ;
58+ isMuted : boolean ;
59+ }
60+
5461// Common type for both P2P and SFU host hooks (subset used by this page)
5562type HostHookFn = ( options : {
5663 sessionId : string ;
@@ -61,6 +68,7 @@ type HostHookFn = (options: {
6168} ) => {
6269 isHosting : boolean ;
6370 viewerCount : number ;
71+ viewers : Map < string , HostedViewer > ;
6472 error : string | null ;
6573 startHosting : ( ) => Promise < void > ;
6674 stopHosting : ( ) => void ;
@@ -205,6 +213,7 @@ function HostContent({
205213 const {
206214 isHosting,
207215 viewerCount,
216+ viewers : hostedViewers ,
208217 error : hostingError ,
209218 startHosting,
210219 stopHosting,
@@ -224,6 +233,56 @@ function HostContent({
224233 } ,
225234 } ) ;
226235
236+ // Audio mixer: combines host mic + all viewer audio into one stream for recording
237+ const {
238+ mixedStream,
239+ addTrack : mixerAddTrack ,
240+ removeTrack : mixerRemoveTrack ,
241+ setTrackMuted : mixerSetTrackMuted ,
242+ dispose : disposeMixer ,
243+ } = useAudioMixer ( ) ;
244+
245+ // Add host mic audio to the mixer
246+ useEffect ( ( ) => {
247+ if ( micStream ) {
248+ const firstAudioTrack = micStream . getAudioTracks ( ) [ 0 ] ;
249+ if ( firstAudioTrack ) {
250+ mixerAddTrack ( 'host-mic' , firstAudioTrack ) ;
251+ }
252+ }
253+ return ( ) => {
254+ mixerRemoveTrack ( 'host-mic' ) ;
255+ } ;
256+ } , [ micStream , mixerAddTrack , mixerRemoveTrack ] ) ;
257+
258+ // Sync viewer audio tracks into the mixer as viewers join/leave
259+ useEffect ( ( ) => {
260+ const currentViewerIds = new Set < string > ( ) ;
261+
262+ for ( const [ viewerId , viewer ] of hostedViewers . entries ( ) ) {
263+ if ( viewer . audioTrack ) {
264+ currentViewerIds . add ( viewerId ) ;
265+ mixerAddTrack ( viewerId , viewer . audioTrack ) ;
266+ mixerSetTrackMuted ( viewerId , viewer . isMuted ) ;
267+ }
268+ }
269+
270+ return ( ) => {
271+ for ( const viewerId of currentViewerIds ) {
272+ if ( ! hostedViewers . has ( viewerId ) ) {
273+ mixerRemoveTrack ( viewerId ) ;
274+ }
275+ }
276+ } ;
277+ } , [ hostedViewers , mixerAddTrack , mixerRemoveTrack , mixerSetTrackMuted ] ) ;
278+
279+ // Clean up mixer when component unmounts
280+ useEffect ( ( ) => {
281+ return ( ) => {
282+ disposeMixer ( ) ;
283+ } ;
284+ } , [ disposeMixer ] ) ;
285+
227286 // Start hosting when stream is available
228287 useEffect ( ( ) => {
229288 if ( stream && ! isHosting ) {
@@ -266,19 +325,27 @@ function HostContent({
266325 if ( ! stream ) return ;
267326 setRecordingBlob ( null ) ;
268327
269- // Create combined stream: screen video + mic audio
328+ // Build a recording stream that includes video + mixed audio (host mic + viewer audio).
329+ // The mixer's output is a live AudioContext graph, so viewers joining/leaving during
330+ // recording are automatically included without needing to restart MediaRecorder.
270331 const combinedStream = new MediaStream ( ) ;
271332 stream . getVideoTracks ( ) . forEach ( ( track ) => {
272333 combinedStream . addTrack ( track ) ;
273334 } ) ;
274- if ( micStream ) {
335+ if ( mixedStream ) {
336+ // Use mixer output (host mic + all viewer audio combined)
337+ mixedStream . getAudioTracks ( ) . forEach ( ( track ) => {
338+ combinedStream . addTrack ( track ) ;
339+ } ) ;
340+ } else if ( micStream ) {
341+ // Fallback: mixer not ready, use raw mic only
275342 micStream . getAudioTracks ( ) . forEach ( ( track ) => {
276343 combinedStream . addTrack ( track ) ;
277344 } ) ;
278345 }
279346
280347 startRecording ( combinedStream , { quality : recordingQuality } ) ;
281- } , [ stream , micStream , recordingQuality , startRecording ] ) ;
348+ } , [ stream , micStream , mixedStream , recordingQuality , startRecording ] ) ;
282349
283350 // Handle stop recording
284351 const handleStopRecording = useCallback ( ( ) => {
0 commit comments