diff --git a/lanshare/static/index.html b/lanshare/static/index.html index cf21526..3221900 100644 --- a/lanshare/static/index.html +++ b/lanshare/static/index.html @@ -430,7 +430,7 @@ peerId: null, peerIp: null, // Our own IP as seen by the server peerName: localStorage.getItem('lanshare_name') || null, - peers: new Map(), // peer_id -> { name, ip, isSharing, hasAudio, stream, peerConnection } + peers: new Map(), // peer_id -> { name, ip, isSharing, hasAudio, stream, outgoingPC, incomingPC } localStream: null, isSharing: false, includeAudio: true, @@ -601,9 +601,11 @@ peer.isSharing = false; peer.hasAudio = false; peer.stream = null; - if (peer.peerConnection) { - peer.peerConnection.close(); - peer.peerConnection = null; + // Only close the incoming connection (their stream to us). + // Keep our outgoing connection if we're sharing to them. + if (peer.incomingPC) { + peer.incomingPC.close(); + peer.incomingPC = null; } if (state.selectedPeerId === msg.peer_id) { state.selectedPeerId = null; @@ -648,18 +650,18 @@ isSharing, hasAudio, stream: null, - peerConnection: null, + outgoingPC: null, // PC where we send our stream to this peer + incomingPC: null, // PC where we receive this peer's stream }); } function removePeer(peerId) { const peer = state.peers.get(peerId); if (peer) { - if (peer.peerConnection) { - peer.peerConnection.close(); - } + if (peer.outgoingPC) peer.outgoingPC.close(); + if (peer.incomingPC) peer.incomingPC.close(); state.peers.delete(peerId); - + if (state.selectedPeerId === peerId) { state.selectedPeerId = null; mainVideo.srcObject = null; @@ -668,45 +670,47 @@ } // ==================== WebRTC ==================== - function createPeerConnection(peerId) { + // direction: 'outgoing' (we send offer) or 'incoming' (we receive offer) + function createPeerConnection(peerId, direction) { const pc = new RTCPeerConnection(rtcConfig); - + // Debug: Log ICE gathering state changes pc.onicegatheringstatechange = () => { - console.log(`[${peerId.slice(0,8)}] ICE gathering state: ${pc.iceGatheringState}`); + console.log(`[${peerId.slice(0,8)}:${direction}] ICE gathering state: ${pc.iceGatheringState}`); if (pc.iceGatheringState === 'complete') { const sdp = pc.localDescription?.sdp || ''; const candidates = extractCandidatesFromSDP(sdp); - console.log(`[${peerId.slice(0,8)}] Final SDP has ${candidates.length} candidates:`); + console.log(`[${peerId.slice(0,8)}:${direction}] Final SDP has ${candidates.length} candidates:`); candidates.forEach(c => console.log(` ${c}`)); } }; - + // Debug: Log ICE connection state pc.oniceconnectionstatechange = () => { - console.log(`[${peerId.slice(0,8)}] ICE connection state: ${pc.iceConnectionState}`); + console.log(`[${peerId.slice(0,8)}:${direction}] ICE connection state: ${pc.iceConnectionState}`); }; - + // Trickle ICE - send candidates as they're discovered pc.onicecandidate = (event) => { if (event.candidate) { const c = event.candidate; - console.log(`[${peerId.slice(0,8)}] Local candidate: ${c.candidate}`); + console.log(`[${peerId.slice(0,8)}:${direction}] Local candidate: ${c.candidate}`); console.log(` → type: ${c.type}, protocol: ${c.protocol}, address: ${c.address}, port: ${c.port}`); if (USE_TRICKLE_ICE) { // Replace mDNS .local addresses with our real IP for direct LAN connectivity const candidateObj = event.candidate.toJSON(); candidateObj.candidate = replaceMdnsWithIp(candidateObj.candidate, state.peerIp); + // Embed direction so the remote side routes to the right PC send({ type: 'ice_candidate', from: state.peerId, to: peerId, - candidate: JSON.stringify(candidateObj), + candidate: JSON.stringify({ ...candidateObj, _dir: direction }), }); } } else { - console.log(`[${peerId.slice(0,8)}] ICE candidate gathering complete (null candidate)`); + console.log(`[${peerId.slice(0,8)}:${direction}] ICE candidate gathering complete (null candidate)`); } }; @@ -733,7 +737,7 @@ }; pc.onconnectionstatechange = () => { - console.log(`[${peerId.slice(0,8)}] Connection state: ${pc.connectionState}`); + console.log(`[${peerId.slice(0,8)}:${direction}] Connection state: ${pc.connectionState}`); }; return pc; @@ -781,16 +785,16 @@ console.warn('sendOfferTo: peer not found:', peerId); return; } - - // Don't create a new connection if we already have one that's not closed - if (peer.peerConnection && peer.peerConnection.connectionState !== 'closed') { - console.log(`[${peerId.slice(0,8)}] sendOfferTo: already have connection`); + + // Don't create a new outgoing connection if we already have one + if (peer.outgoingPC && peer.outgoingPC.connectionState !== 'closed') { + console.log(`[${peerId.slice(0,8)}] sendOfferTo: already have outgoing connection`); return; } - + console.log(`[${peerId.slice(0,8)}] sendOfferTo: creating offer (${USE_TRICKLE_ICE ? 'trickle' : 'bundled'})`); - const pc = createPeerConnection(peerId); - peer.peerConnection = pc; + const pc = createPeerConnection(peerId, 'outgoing'); + peer.outgoingPC = pc; // Add local stream tracks if (state.localStream) { @@ -828,14 +832,14 @@ const peer = state.peers.get(fromPeerId); - // Close existing connection if any (new offer = renegotiation) - if (peer.peerConnection) { - peer.peerConnection.close(); + // Close existing incoming connection if any (new offer = renegotiation) + if (peer.incomingPC) { + peer.incomingPC.close(); } console.log(`[${fromPeerId.slice(0,8)}] handleOffer: creating answer (${USE_TRICKLE_ICE ? 'trickle' : 'bundled'})`); - const pc = createPeerConnection(fromPeerId); - peer.peerConnection = pc; + const pc = createPeerConnection(fromPeerId, 'incoming'); + peer.incomingPC = pc; // Replace any mDNS .local addresses in the remote SDP with the peer's real IP const sdp = JSON.parse(sdpJson); @@ -869,14 +873,14 @@ async function handleAnswer(fromPeerId, sdpJson) { const peer = state.peers.get(fromPeerId); - if (!peer || !peer.peerConnection) { - console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: no peer connection`); + if (!peer || !peer.outgoingPC) { + console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: no outgoing peer connection`); return; } // Only set remote description if we're in the right state - if (peer.peerConnection.signalingState !== 'have-local-offer') { - console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: wrong state: ${peer.peerConnection.signalingState}`); + if (peer.outgoingPC.signalingState !== 'have-local-offer') { + console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: wrong state: ${peer.outgoingPC.signalingState}`); return; } @@ -888,22 +892,32 @@ remoteCandidates.forEach(c => console.log(` ${c}`)); console.log(`[${fromPeerId.slice(0,8)}] handleAnswer: setting remote description`); - await peer.peerConnection.setRemoteDescription(new RTCSessionDescription(sdp)); + await peer.outgoingPC.setRemoteDescription(new RTCSessionDescription(sdp)); } function handleIceCandidate(fromPeerId, candidateJson) { const peer = state.peers.get(fromPeerId); - if (!peer || !peer.peerConnection) { - console.warn(`[${fromPeerId.slice(0,8)}] handleIceCandidate: no peer connection`); + if (!peer) { + console.warn(`[${fromPeerId.slice(0,8)}] handleIceCandidate: peer not found`); + return; + } + + const data = JSON.parse(candidateJson); + const dir = data._dir; + delete data._dir; + + // Their outgoing PC corresponds to our incoming PC (and vice versa) + const pc = dir === 'outgoing' ? peer.incomingPC : peer.outgoingPC; + if (!pc) { + console.warn(`[${fromPeerId.slice(0,8)}] handleIceCandidate: no matching PC for direction ${dir}`); return; } // Replace any mDNS .local addresses with the peer's real IP - const candidate = JSON.parse(candidateJson); - candidate.candidate = replaceMdnsWithIp(candidate.candidate, peer.ip); - console.log(`[${fromPeerId.slice(0,8)}] Remote candidate: ${candidate.candidate}`); + data.candidate = replaceMdnsWithIp(data.candidate, peer.ip); + console.log(`[${fromPeerId.slice(0,8)}:${dir}] Remote candidate: ${data.candidate}`); - peer.peerConnection.addIceCandidate(new RTCIceCandidate(candidate)) + pc.addIceCandidate(new RTCIceCandidate(data)) .then(() => console.log(`[${fromPeerId.slice(0,8)}] Successfully added remote candidate`)) .catch(err => console.error(`[${fromPeerId.slice(0,8)}] Error adding ICE candidate:`, err)); } @@ -950,14 +964,15 @@ state.localStream.getTracks().forEach(track => track.stop()); state.localStream = null; } - + state.isSharing = false; - - // Close all peer connections we initiated + + // Only close outgoing connections (where we were sending our stream). + // Keep incoming connections alive so we can still view others' streams. for (const [peerId, peer] of state.peers) { - if (peer.peerConnection) { - peer.peerConnection.close(); - peer.peerConnection = null; + if (peer.outgoingPC) { + peer.outgoingPC.close(); + peer.outgoingPC = null; } }