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;
}
}