From a5af009ba84fb690cc167abd45fa31c9192b14c0 Mon Sep 17 00:00:00 2001 From: pavel Date: Tue, 24 Feb 2026 22:28:02 +0100 Subject: [PATCH] test --- desktop/app.js | 146 ++++++++++++++++++++++++------------------------- main.js | 35 +++++++++++- static/app.js | 146 ++++++++++++++++++++++++------------------------- 3 files changed, 176 insertions(+), 151 deletions(-) diff --git a/desktop/app.js b/desktop/app.js index ca0b334..584c5f2 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -17,8 +17,9 @@ const state = { rawStream: null, videoStream: null, screenStream: null, - audioContext: null, peerConnections: new Map(), + videoSenders: new Map(), + screenSenders: new Map(), muted: false, sharingVideo: false, sharingScreen: false, @@ -789,37 +790,37 @@ function ensurePeerConnection(peerId) { return state.voice.peerConnections.get(peerId); } - const pc = new RTCPeerConnection({ iceServers: state.voice.iceServers }); + const pc = new RTCPeerConnection({ + iceServers: state.voice.iceServers + }); + + pc.makingOffer = false; + pc.ignoreOffer = false; + pc.polite = !shouldInitiateOffer(peerId); - // Add transceivers in a fixed order to ensure stable SDP m-lines: [Audio, Camera, Screen] // 1. Audio if (state.voice.localStream) { - pc.addTransceiver(state.voice.localStream.getAudioTracks()[0], { - direction: 'sendrecv', - streams: [state.voice.localStream] + state.voice.localStream.getAudioTracks().forEach(track => { + pc.addTrack(track, state.voice.localStream); }); } else { - pc.addTransceiver('audio', { direction: 'sendrecv' }); + pc.addTransceiver('audio', { direction: 'recvonly' }); } // 2. Camera Video if (state.voice.videoStream) { - pc.addTransceiver(state.voice.videoStream.getVideoTracks()[0], { - direction: 'sendrecv', - streams: [state.voice.videoStream] + state.voice.videoStream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, state.voice.videoStream); + state.voice.videoSenders.set(peerId, sender); }); - } else { - pc.addTransceiver('video', { direction: 'sendrecv' }); } // 3. Screen Video if (state.voice.screenStream) { - pc.addTransceiver(state.voice.screenStream.getVideoTracks()[0], { - direction: 'sendrecv', - streams: [state.voice.screenStream] + state.voice.screenStream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, state.voice.screenStream); + state.voice.screenSenders.set(peerId, sender); }); - } else { - pc.addTransceiver('video', { direction: 'sendrecv' }); } pc.onicecandidate = (event) => { @@ -833,6 +834,7 @@ function ensurePeerConnection(peerId) { }; pc.ontrack = (event) => { + const stream = event.streams[0]; if (event.track.kind === "audio") { let audio = document.getElementById(`audio-${peerId}`); if (!audio) { @@ -842,24 +844,35 @@ function ensurePeerConnection(peerId) { audio.playsInline = true; document.body.appendChild(audio); } - audio.srcObject = event.streams[0]; + audio.srcObject = stream; } else if (event.track.kind === "video") { const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" }; - // Distinguish screen share from camera by checking track labels or signaling - // For simplicity, we can use the stream index or a dedicated track check. - // A more robust way is to have separate transceivers/signaling. - // Here we check if the label suggests a screen. - const isScreen = event.track.label.toLowerCase().includes('screen') || - event.track.label.toLowerCase().includes('monitor'); - renderVideo(peerId, peer.display_name, event.streams[0], isScreen ? 'screen' : 'camera'); + renderVideo(peerId, peer.display_name, stream, event.track.id); + + event.track.onmute = () => { + document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove(); + }; + event.track.onended = () => { + document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove(); + }; } }; pc.onnegotiationneeded = async () => { try { - await sendOffer(peerId); + pc.makingOffer = true; + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + state.voice.ws.send(JSON.stringify({ + type: "signal", + to_user_id: peerId, + kind: "offer", + data: pc.localDescription, + })); } catch (err) { console.error("negotiation failed", err); + } finally { + pc.makingOffer = false; } }; @@ -867,41 +880,23 @@ function ensurePeerConnection(peerId) { return pc; } -async function sendOffer(peerId) { - const pc = ensurePeerConnection(peerId); - const offer = await pc.createOffer(); - await pc.setLocalDescription(offer); - state.voice.ws.send(JSON.stringify({ - type: "signal", - to_user_id: peerId, - kind: "offer", - data: offer, - })); -} - async function handleSignal(fromPeerId, kind, data) { const pc = ensurePeerConnection(fromPeerId); - const polite = !shouldInitiateOffer(fromPeerId); try { if (kind === "offer") { - const offerCollision = (pc.signalingState !== "stable"); - if (offerCollision) { - if (!polite) return; - await Promise.all([ - pc.setLocalDescription({ type: "rollback" }), - pc.setRemoteDescription(new RTCSessionDescription(data)) - ]); - } else { - await pc.setRemoteDescription(new RTCSessionDescription(data)); - } + const offerCollision = pc.makingOffer || pc.signalingState !== "stable"; + pc.ignoreOffer = !pc.polite && offerCollision; + if (pc.ignoreOffer) return; + + await pc.setRemoteDescription(new RTCSessionDescription(data)); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); state.voice.ws.send(JSON.stringify({ type: "signal", to_user_id: fromPeerId, kind: "answer", - data: answer, + data: pc.localDescription, })); } else if (kind === "answer") { await pc.setRemoteDescription(new RTCSessionDescription(data)); @@ -909,9 +904,7 @@ async function handleSignal(fromPeerId, kind, data) { try { await pc.addIceCandidate(data ? new RTCIceCandidate(data) : null); } catch (err) { - if (pc.signalingState !== "stable") { - // Ignore ICE candidates during negotiation - } else { + if (!pc.ignoreOffer) { console.warn("failed to add ice candidate", err); } } @@ -952,11 +945,11 @@ async function joinVoice() { const msg = JSON.parse(event.data); if (msg.type === "peers") { for (const peer of msg.peers) { - if (shouldInitiateOffer(peer.user_id)) await sendOffer(peer.user_id); + ensurePeerConnection(peer.user_id); } } else if (msg.type === "peer_joined") { playSound('peer-join'); - if (shouldInitiateOffer(msg.user_id)) await sendOffer(msg.user_id); + ensurePeerConnection(msg.user_id); } else if (msg.type === "peer_left") { playSound('peer-leave'); const pc = state.voice.peerConnections.get(msg.user_id); @@ -996,6 +989,8 @@ async function joinVoice() { el.soundboard.classList.add("hidden"); for (const pc of state.voice.peerConnections.values()) pc.close(); state.voice.peerConnections.clear(); + state.voice.videoSenders.clear(); + state.voice.screenSenders.clear(); stopAndClearAudioPipeline(); state.voice.joinedChannelId = null; state.voice.ws = null; @@ -1025,11 +1020,11 @@ async function toggleVideo() { if (state.voice.ws) { state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: false })); } - for (const pc of state.voice.peerConnections.values()) { - const transceivers = pc.getTransceivers(); - const cameraTransceiver = transceivers.find(t => t.receiver.track.kind === 'video' && t.mid === transceivers[1].mid); - if (cameraTransceiver) { - cameraTransceiver.sender.replaceTrack(null); + for (const [peerId, pc] of state.voice.peerConnections) { + const sender = state.voice.videoSenders.get(peerId); + if (sender) { + pc.removeTrack(sender); + state.voice.videoSenders.delete(peerId); } } } else { @@ -1043,11 +1038,11 @@ async function toggleVideo() { state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: true })); } - for (const pc of state.voice.peerConnections.values()) { - const transceivers = pc.getTransceivers(); - if (transceivers[1]) { - transceivers[1].sender.replaceTrack(stream.getVideoTracks()[0]); - } + for (const [peerId, pc] of state.voice.peerConnections) { + stream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, stream); + state.voice.videoSenders.set(peerId, sender); + }); } } catch (err) { console.error("camera denied", err); @@ -1067,15 +1062,16 @@ async function toggleScreenShare() { if (state.voice.ws) { state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: false })); } - for (const pc of state.voice.peerConnections.values()) { - const transceivers = pc.getTransceivers(); - if (transceivers[2]) { - transceivers[2].sender.replaceTrack(null); + for (const [peerId, pc] of state.voice.peerConnections) { + const sender = state.voice.screenSenders.get(peerId); + if (sender) { + pc.removeTrack(sender); + state.voice.screenSenders.delete(peerId); } } } else { try { - const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }); + const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); state.voice.screenStream = stream; state.voice.sharingScreen = true; renderVideo(state.me.id, state.me.display_name, stream, 'screen'); @@ -1089,11 +1085,11 @@ async function toggleScreenShare() { state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: true })); } - for (const pc of state.voice.peerConnections.values()) { - const transceivers = pc.getTransceivers(); - if (transceivers[2]) { - transceivers[2].sender.replaceTrack(stream.getVideoTracks()[0]); - } + for (const [peerId, pc] of state.voice.peerConnections) { + stream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, stream); + state.voice.screenSenders.set(peerId, sender); + }); } } catch (err) { console.error("screen share denied", err); diff --git a/main.js b/main.js index 4517396..67053af 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, session } = require('electron'); +const { app, BrowserWindow, session, desktopCapturer } = require('electron'); const path = require('path'); const url = require('url'); @@ -18,6 +18,39 @@ function createWindow() { } }); + // Auto-approve media permissions (camera, microphone) + sess.setPermissionCheckHandler((webContents, permission) => { + if (permission === 'media') { + return true; + } + return false; + }); + + sess.setPermissionRequestHandler((webContents, permission, callback) => { + if (permission === 'media') { + callback(true); + } else { + callback(false); + } + }); + + // Handle screen share requests + sess.setDisplayMediaRequestHandler((request, callback) => { + desktopCapturer.getSources({ types: ['screen', 'window'] }).then((sources) => { + // Provide the first screen source by default, or implement a picker window here + if (sources && sources.length > 0) { + // We prefer a screen over a window if available, simple heuristic + const screenSource = sources.find(s => s.id.startsWith('screen')) || sources[0]; + callback({ video: screenSource, audio: 'loopback' }); + } else { + callback(null); // Reject safely + } + }).catch(err => { + console.error("Failed to get desktop sources for screen share", err); + callback(null); + }); + }); + const backendUrl = (process.env.CHATTZ_URL || 'https://discord.flegr.me').replace(/\/$/, ''); const indexPath = path.join(__dirname, 'desktop', 'index.html'); diff --git a/static/app.js b/static/app.js index c17ecee..e8e0058 100644 --- a/static/app.js +++ b/static/app.js @@ -17,8 +17,9 @@ const state = { rawStream: null, videoStream: null, screenStream: null, - audioContext: null, peerConnections: new Map(), + videoSenders: new Map(), + screenSenders: new Map(), muted: false, sharingVideo: false, sharingScreen: false, @@ -771,37 +772,37 @@ function ensurePeerConnection(peerId) { return state.voice.peerConnections.get(peerId); } - const pc = new RTCPeerConnection({ iceServers: state.voice.iceServers }); + const pc = new RTCPeerConnection({ + iceServers: state.voice.iceServers + }); + + pc.makingOffer = false; + pc.ignoreOffer = false; + pc.polite = !shouldInitiateOffer(peerId); - // Add transceivers in a fixed order to ensure stable SDP m-lines: [Audio, Camera, Screen] // 1. Audio if (state.voice.localStream) { - pc.addTransceiver(state.voice.localStream.getAudioTracks()[0], { - direction: 'sendrecv', - streams: [state.voice.localStream] + state.voice.localStream.getAudioTracks().forEach(track => { + pc.addTrack(track, state.voice.localStream); }); } else { - pc.addTransceiver('audio', { direction: 'sendrecv' }); + pc.addTransceiver('audio', { direction: 'recvonly' }); } // 2. Camera Video if (state.voice.videoStream) { - pc.addTransceiver(state.voice.videoStream.getVideoTracks()[0], { - direction: 'sendrecv', - streams: [state.voice.videoStream] + state.voice.videoStream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, state.voice.videoStream); + state.voice.videoSenders.set(peerId, sender); }); - } else { - pc.addTransceiver('video', { direction: 'sendrecv' }); } // 3. Screen Video if (state.voice.screenStream) { - pc.addTransceiver(state.voice.screenStream.getVideoTracks()[0], { - direction: 'sendrecv', - streams: [state.voice.screenStream] + state.voice.screenStream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, state.voice.screenStream); + state.voice.screenSenders.set(peerId, sender); }); - } else { - pc.addTransceiver('video', { direction: 'sendrecv' }); } pc.onicecandidate = (event) => { @@ -815,6 +816,7 @@ function ensurePeerConnection(peerId) { }; pc.ontrack = (event) => { + const stream = event.streams[0]; if (event.track.kind === "audio") { let audio = document.getElementById(`audio-${peerId}`); if (!audio) { @@ -824,24 +826,35 @@ function ensurePeerConnection(peerId) { audio.playsInline = true; document.body.appendChild(audio); } - audio.srcObject = event.streams[0]; + audio.srcObject = stream; } else if (event.track.kind === "video") { const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" }; - // Distinguish screen share from camera by checking track labels or signaling - // For simplicity, we can use the stream index or a dedicated track check. - // A more robust way is to have separate transceivers/signaling. - // Here we check if the label suggests a screen. - const isScreen = event.track.label.toLowerCase().includes('screen') || - event.track.label.toLowerCase().includes('monitor'); - renderVideo(peerId, peer.display_name, event.streams[0], isScreen ? 'screen' : 'camera'); + renderVideo(peerId, peer.display_name, stream, event.track.id); + + event.track.onmute = () => { + document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove(); + }; + event.track.onended = () => { + document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove(); + }; } }; pc.onnegotiationneeded = async () => { try { - await sendOffer(peerId); + pc.makingOffer = true; + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + state.voice.ws.send(JSON.stringify({ + type: "signal", + to_user_id: peerId, + kind: "offer", + data: pc.localDescription, + })); } catch (err) { console.error("negotiation failed", err); + } finally { + pc.makingOffer = false; } }; @@ -849,41 +862,23 @@ function ensurePeerConnection(peerId) { return pc; } -async function sendOffer(peerId) { - const pc = ensurePeerConnection(peerId); - const offer = await pc.createOffer(); - await pc.setLocalDescription(offer); - state.voice.ws.send(JSON.stringify({ - type: "signal", - to_user_id: peerId, - kind: "offer", - data: offer, - })); -} - async function handleSignal(fromPeerId, kind, data) { const pc = ensurePeerConnection(fromPeerId); - const polite = !shouldInitiateOffer(fromPeerId); try { if (kind === "offer") { - const offerCollision = (pc.signalingState !== "stable"); - if (offerCollision) { - if (!polite) return; - await Promise.all([ - pc.setLocalDescription({ type: "rollback" }), - pc.setRemoteDescription(new RTCSessionDescription(data)) - ]); - } else { - await pc.setRemoteDescription(new RTCSessionDescription(data)); - } + const offerCollision = pc.makingOffer || pc.signalingState !== "stable"; + pc.ignoreOffer = !pc.polite && offerCollision; + if (pc.ignoreOffer) return; + + await pc.setRemoteDescription(new RTCSessionDescription(data)); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); state.voice.ws.send(JSON.stringify({ type: "signal", to_user_id: fromPeerId, kind: "answer", - data: answer, + data: pc.localDescription, })); } else if (kind === "answer") { await pc.setRemoteDescription(new RTCSessionDescription(data)); @@ -891,9 +886,7 @@ async function handleSignal(fromPeerId, kind, data) { try { await pc.addIceCandidate(data ? new RTCIceCandidate(data) : null); } catch (err) { - if (pc.signalingState !== "stable") { - // Ignore ICE candidates during negotiation - } else { + if (!pc.ignoreOffer) { console.warn("failed to add ice candidate", err); } } @@ -934,11 +927,11 @@ async function joinVoice() { const msg = JSON.parse(event.data); if (msg.type === "peers") { for (const peer of msg.peers) { - if (shouldInitiateOffer(peer.user_id)) await sendOffer(peer.user_id); + ensurePeerConnection(peer.user_id); } } else if (msg.type === "peer_joined") { playSound('peer-join'); - if (shouldInitiateOffer(msg.user_id)) await sendOffer(msg.user_id); + ensurePeerConnection(msg.user_id); } else if (msg.type === "peer_left") { playSound('peer-leave'); const pc = state.voice.peerConnections.get(msg.user_id); @@ -978,6 +971,8 @@ async function joinVoice() { el.soundboard.classList.add("hidden"); for (const pc of state.voice.peerConnections.values()) pc.close(); state.voice.peerConnections.clear(); + state.voice.videoSenders.clear(); + state.voice.screenSenders.clear(); stopAndClearAudioPipeline(); state.voice.joinedChannelId = null; state.voice.ws = null; @@ -1007,11 +1002,11 @@ async function toggleVideo() { if (state.voice.ws) { state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: false })); } - for (const pc of state.voice.peerConnections.values()) { - const transceivers = pc.getTransceivers(); - const cameraTransceiver = transceivers.find(t => t.receiver.track.kind === 'video' && t.mid === transceivers[1].mid); - if (cameraTransceiver) { - cameraTransceiver.sender.replaceTrack(null); + for (const [peerId, pc] of state.voice.peerConnections) { + const sender = state.voice.videoSenders.get(peerId); + if (sender) { + pc.removeTrack(sender); + state.voice.videoSenders.delete(peerId); } } } else { @@ -1025,11 +1020,11 @@ async function toggleVideo() { state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: true })); } - for (const pc of state.voice.peerConnections.values()) { - const transceivers = pc.getTransceivers(); - if (transceivers[1]) { - transceivers[1].sender.replaceTrack(stream.getVideoTracks()[0]); - } + for (const [peerId, pc] of state.voice.peerConnections) { + stream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, stream); + state.voice.videoSenders.set(peerId, sender); + }); } } catch (err) { console.error("camera denied", err); @@ -1049,15 +1044,16 @@ async function toggleScreenShare() { if (state.voice.ws) { state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: false })); } - for (const pc of state.voice.peerConnections.values()) { - const transceivers = pc.getTransceivers(); - if (transceivers[2]) { - transceivers[2].sender.replaceTrack(null); + for (const [peerId, pc] of state.voice.peerConnections) { + const sender = state.voice.screenSenders.get(peerId); + if (sender) { + pc.removeTrack(sender); + state.voice.screenSenders.delete(peerId); } } } else { try { - const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }); + const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); state.voice.screenStream = stream; state.voice.sharingScreen = true; renderVideo(state.me.id, state.me.display_name, stream, 'screen'); @@ -1071,11 +1067,11 @@ async function toggleScreenShare() { state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: true })); } - for (const pc of state.voice.peerConnections.values()) { - const transceivers = pc.getTransceivers(); - if (transceivers[2]) { - transceivers[2].sender.replaceTrack(stream.getVideoTracks()[0]); - } + for (const [peerId, pc] of state.voice.peerConnections) { + stream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, stream); + state.voice.screenSenders.set(peerId, sender); + }); } } catch (err) { console.error("screen share denied", err);