Merge pull request #3 from TheNeikos/claude/fix-screenshare-stream-43ABB

Split peerConnection into separate outgoing/incoming connections
This commit is contained in:
Marcel Müller 2026-02-09 10:32:55 +01:00 committed by GitHub
commit c432384aec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

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