Split peerConnection into separate outgoing/incoming connections
The root cause of both bugs was that each peer had a single peerConnection field used for both sending and receiving streams: 1. Starting a share while receiving another's stream failed because sendOfferTo() saw the existing incoming PC and bailed out, so the outgoing stream was never sent. 2. Stopping a share killed the other person's stream because stopSharing() closed ALL peer connections, including the incoming one used to receive their stream. Fix by splitting into outgoingPC (created via sendOfferTo, used for sending our stream) and incomingPC (created via handleOffer, used for receiving their stream). ICE candidates now embed a direction flag so the remote side routes them to the correct PC. https://claude.ai/code/session_01ALSwS4S8EHiP81i2KMsb9Y
This commit is contained in:
parent
4c223b47ed
commit
6ca45de838
1 changed files with 64 additions and 49 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue