diff --git a/packages/web/src/components/voice/DmCallView.tsx b/packages/web/src/components/voice/DmCallView.tsx index 8d1c7c17..ebfb3447 100644 --- a/packages/web/src/components/voice/DmCallView.tsx +++ b/packages/web/src/components/voice/DmCallView.tsx @@ -89,12 +89,19 @@ export function DmCallView() { toggleCamera(); }; - const handleScreenShare = () => { + const handleScreenShare = async () => { const room = getActiveRoom(); - if (room) { - room.localParticipant.setScreenShareEnabled(!isScreenSharing); + if (!room) return; + try { + if (!isScreenSharing) { + await room.localParticipant.setScreenShareEnabled(true, { audio: true }); + } else { + await room.localParticipant.setScreenShareEnabled(false); + } + toggleScreenShare(); + } catch (err) { + console.error('[DmCallView] Failed to toggle screen share:', err); } - toggleScreenShare(); }; const handleEndCall = () => { diff --git a/packages/web/src/components/voice/VoiceControlBar.tsx b/packages/web/src/components/voice/VoiceControlBar.tsx index de298a7c..03f59038 100644 --- a/packages/web/src/components/voice/VoiceControlBar.tsx +++ b/packages/web/src/components/voice/VoiceControlBar.tsx @@ -96,7 +96,11 @@ export function VoiceControlBar() { const room = getActiveRoom(); if (!room) return; try { - await room.localParticipant.setScreenShareEnabled(!isScreenSharing); + if (!isScreenSharing) { + await room.localParticipant.setScreenShareEnabled(true, { audio: true }); + } else { + await room.localParticipant.setScreenShareEnabled(false); + } toggleScreenShare(); } catch (err) { console.error('[VoiceControlBar] Failed to toggle screen share:', err); diff --git a/packages/web/src/components/voice/VoiceControls.tsx b/packages/web/src/components/voice/VoiceControls.tsx index dc282e75..200e6101 100644 --- a/packages/web/src/components/voice/VoiceControls.tsx +++ b/packages/web/src/components/voice/VoiceControls.tsx @@ -42,7 +42,11 @@ export function VoiceControls() { const room = getActiveRoom(); if (!room) return; try { - await room.localParticipant.setScreenShareEnabled(!isScreenSharing); + if (!isScreenSharing) { + await room.localParticipant.setScreenShareEnabled(true, { audio: true }); + } else { + await room.localParticipant.setScreenShareEnabled(false); + } toggleScreenShare(); } catch (err) { console.error('[VoiceControls] Failed to toggle screen share:', err); diff --git a/packages/web/src/components/voice/VoiceUser.tsx b/packages/web/src/components/voice/VoiceUser.tsx index bb0bb861..bfe478e0 100644 --- a/packages/web/src/components/voice/VoiceUser.tsx +++ b/packages/web/src/components/voice/VoiceUser.tsx @@ -12,6 +12,7 @@ interface VoiceUserProps { export function VoiceUser({ participant, large }: VoiceUserProps) { const videoRef = useRef(null); const audioRef = useRef(null); + const screenAudioRef = useRef(null); const isDeafened = useVoiceStore((s) => s.isDeafened); const outputVolume = useVoiceStore((s) => s.outputVolume); @@ -128,6 +129,80 @@ export function VoiceUser({ participant, large }: VoiceUserProps) { }, [outputVolume, perUserVolume, isDeafened, isLocal, participant.audioTrack]); + // --- SCREEN SHARE AUDIO PIPELINE --- + + const screenBoostGainRef = useRef(null); + const screenBoostSourceRef = useRef(null); + + // Screen share audio: track attachment + useEffect(() => { + const audioEl = screenAudioRef.current; + if (isLocal || !audioEl || !participant.screenAudioTrack) { + if (audioEl) audioEl.srcObject = null; + return; + } + + const stream = new MediaStream([participant.screenAudioTrack]); + if ((audioEl.srcObject as MediaStream)?.id !== stream.id) { + audioEl.srcObject = stream; + audioEl.play().catch(() => {}); + } + }, [participant.screenAudioTrack, isLocal]); + + // Screen share audio: volume management (mirrors mic audio pipeline) + useEffect(() => { + const audioEl = screenAudioRef.current; + if (isLocal || !audioEl || !participant.screenAudioTrack) return; + + const globalScale = outputVolume / 100; + const userScale = perUserVolume / 100; + const finalVolume = globalScale * userScale; + + if (isDeafened) { + audioEl.muted = true; + return; + } + + const audioManager = AudioManager.getInstance(); + const ctx = audioManager.getContext(); + const isBoosting = finalVolume > 1.0; + const isContextReady = ctx && ctx.state === 'running'; + + if (isBoosting && isContextReady) { + if (!screenBoostGainRef.current && ctx) { + const gain = ctx.createGain(); + const source = ctx.createMediaStreamSource(new MediaStream([participant.screenAudioTrack])); + source.connect(gain); + gain.connect(ctx.destination); + screenBoostGainRef.current = gain; + screenBoostSourceRef.current = source; + } + if (screenBoostGainRef.current && ctx) { + screenBoostGainRef.current.gain.setTargetAtTime(finalVolume, ctx.currentTime, 0.01); + } + audioEl.muted = true; + } else { + if (screenBoostSourceRef.current) { + screenBoostSourceRef.current.disconnect(); + screenBoostSourceRef.current = null; + screenBoostGainRef.current = null; + } + audioEl.muted = false; + audioEl.volume = Math.min(finalVolume, 1.0); + if (audioEl.paused) { + audioEl.play().catch(() => {}); + } + } + + return () => { + if (screenBoostSourceRef.current) { + screenBoostSourceRef.current.disconnect(); + screenBoostSourceRef.current = null; + screenBoostGainRef.current = null; + } + }; + }, [outputVolume, perUserVolume, isDeafened, isLocal, participant.screenAudioTrack]); + // --- VIDEO & UI --- const liveScreen = participant.isScreenSharing && participant.screenTrack?.readyState === 'live' ? participant.screenTrack : null; @@ -188,6 +263,7 @@ export function VoiceUser({ participant, large }: VoiceUserProps) { - PlaysInline is critical for mobile */} {!isLocal &&