Skip to content

Commit 4e71d97

Browse files
committed
Fix desktop viewer signaling auth and host live audio publish
1 parent 518ff8a commit 4e71d97

2 files changed

Lines changed: 79 additions & 18 deletions

File tree

apps/desktop/src/renderer/hooks/useWebRTCHostAPI.ts

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const DEFAULT_ICE_SERVERS: RTCIceServer[] = [
4545
export 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(() => {

apps/desktop/src/renderer/hooks/useWebRTCViewerAPI.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export function useWebRTCViewerAPI({
111111
const pendingCandidatesRef = useRef<RTCIceCandidateInit[]>([]);
112112
// Serialize signaling message processing to prevent race conditions
113113
const signalQueueRef = useRef<Promise<void>>(Promise.resolve());
114+
// Use the server-assigned subscriber ID for signaling POSTs (desktop token auth can differ
115+
// from the locally generated participantId used to open the SSE stream).
116+
const signalSenderIdRef = useRef(participantId);
114117

115118
// Callback refs to avoid circular dependencies
116119
const handleConnectionFailureRef = useRef<(() => Promise<void>) | undefined>(undefined);
@@ -123,6 +126,8 @@ export function useWebRTCViewerAPI({
123126
onCursorUpdateRef.current = onCursorUpdate;
124127
onKickedRef.current = onKicked;
125128

129+
const getSignalSenderId = useCallback(() => signalSenderIdRef.current, []);
130+
126131
// Calculate network quality from metrics
127132
const calculateNetworkQuality = useCallback((metrics: QualityMetrics): NetworkQuality => {
128133
const { packetLoss, roundTripTime } = metrics;
@@ -468,7 +473,7 @@ export function useWebRTCViewerAPI({
468473
await sendSignal({
469474
type: 'answer',
470475
sdp: answer.sdp,
471-
senderId: participantId,
476+
senderId: getSignalSenderId(),
472477
targetId: message.senderId,
473478
timestamp: Date.now(),
474479
});
@@ -505,7 +510,7 @@ export function useWebRTCViewerAPI({
505510
setError('Failed to process signaling message');
506511
}
507512
},
508-
[participantId, sendSignal]
513+
[getSignalSenderId, sendSignal]
509514
);
510515

511516
// Handle signaling messages from SSE — serialized via promise chain
@@ -536,7 +541,7 @@ export function useWebRTCViewerAPI({
536541
await sendSignal({
537542
type: 'offer',
538543
sdp: offer.sdp,
539-
senderId: participantId,
544+
senderId: getSignalSenderId(),
540545
timestamp: Date.now(),
541546
});
542547
}
@@ -549,7 +554,7 @@ export function useWebRTCViewerAPI({
549554
setConnectionState('failed');
550555
setError('Connection failed after multiple attempts');
551556
}
552-
}, [participantId, sendSignal]);
557+
}, [getSignalSenderId, sendSignal]);
553558

554559
handleConnectionFailureRef.current = handleConnectionFailure;
555560

@@ -644,7 +649,7 @@ export function useWebRTCViewerAPI({
644649
void sendSignal({
645650
type: 'ice-candidate',
646651
candidate: event.candidate.toJSON(),
647-
senderId: participantId,
652+
senderId: getSignalSenderId(),
648653
timestamp: Date.now(),
649654
});
650655
}
@@ -693,7 +698,7 @@ export function useWebRTCViewerAPI({
693698
};
694699

695700
return pc;
696-
}, [participantId, onStreamReady, onStreamEnded, sendSignal, setupDataChannel]);
701+
}, [getSignalSenderId, onStreamReady, onStreamEnded, sendSignal, setupDataChannel]);
697702

698703
// Disconnect and clean up
699704
const disconnect = useCallback(() => {
@@ -814,8 +819,20 @@ export function useWebRTCViewerAPI({
814819
// Use ICE servers from the server (includes TURN) if provided
815820
try {
816821
const data = JSON.parse(event.data as string) as {
822+
subscriberId?: string;
817823
iceServers?: RTCIceServer[];
818824
};
825+
if (data.subscriberId) {
826+
signalSenderIdRef.current = data.subscriberId;
827+
if (data.subscriberId !== participantId) {
828+
console.log(
829+
'[WebRTCViewer] Using server-assigned signaling senderId:',
830+
data.subscriberId
831+
);
832+
}
833+
} else {
834+
signalSenderIdRef.current = participantId;
835+
}
819836
if (data.iceServers && data.iceServers.length > 0) {
820837
iceServersRef.current = data.iceServers;
821838
console.log('[WebRTCViewer] Received ICE servers from server:', data.iceServers.length);

0 commit comments

Comments
 (0)