Merge pull request #3 from TheNeikos/claude/fix-screenshare-stream-43ABB
Split peerConnection into separate outgoing/incoming connections
This commit is contained in:
commit
c432384aec
1 changed files with 64 additions and 49 deletions
|
|
@ -430,7 +430,7 @@
|
||||||
peerId: null,
|
peerId: null,
|
||||||
peerIp: null, // Our own IP as seen by the server
|
peerIp: null, // Our own IP as seen by the server
|
||||||
peerName: localStorage.getItem('lanshare_name') || null,
|
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,
|
localStream: null,
|
||||||
isSharing: false,
|
isSharing: false,
|
||||||
includeAudio: true,
|
includeAudio: true,
|
||||||
|
|
@ -601,9 +601,11 @@
|
||||||
peer.isSharing = false;
|
peer.isSharing = false;
|
||||||
peer.hasAudio = false;
|
peer.hasAudio = false;
|
||||||
peer.stream = null;
|
peer.stream = null;
|
||||||
if (peer.peerConnection) {
|
// Only close the incoming connection (their stream to us).
|
||||||
peer.peerConnection.close();
|
// Keep our outgoing connection if we're sharing to them.
|
||||||
peer.peerConnection = null;
|
if (peer.incomingPC) {
|
||||||
|
peer.incomingPC.close();
|
||||||
|
peer.incomingPC = null;
|
||||||
}
|
}
|
||||||
if (state.selectedPeerId === msg.peer_id) {
|
if (state.selectedPeerId === msg.peer_id) {
|
||||||
state.selectedPeerId = null;
|
state.selectedPeerId = null;
|
||||||
|
|
@ -648,16 +650,16 @@
|
||||||
isSharing,
|
isSharing,
|
||||||
hasAudio,
|
hasAudio,
|
||||||
stream: null,
|
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) {
|
function removePeer(peerId) {
|
||||||
const peer = state.peers.get(peerId);
|
const peer = state.peers.get(peerId);
|
||||||
if (peer) {
|
if (peer) {
|
||||||
if (peer.peerConnection) {
|
if (peer.outgoingPC) peer.outgoingPC.close();
|
||||||
peer.peerConnection.close();
|
if (peer.incomingPC) peer.incomingPC.close();
|
||||||
}
|
|
||||||
state.peers.delete(peerId);
|
state.peers.delete(peerId);
|
||||||
|
|
||||||
if (state.selectedPeerId === peerId) {
|
if (state.selectedPeerId === peerId) {
|
||||||
|
|
@ -668,45 +670,47 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== WebRTC ====================
|
// ==================== WebRTC ====================
|
||||||
function createPeerConnection(peerId) {
|
// direction: 'outgoing' (we send offer) or 'incoming' (we receive offer)
|
||||||
|
function createPeerConnection(peerId, direction) {
|
||||||
const pc = new RTCPeerConnection(rtcConfig);
|
const pc = new RTCPeerConnection(rtcConfig);
|
||||||
|
|
||||||
// Debug: Log ICE gathering state changes
|
// Debug: Log ICE gathering state changes
|
||||||
pc.onicegatheringstatechange = () => {
|
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') {
|
if (pc.iceGatheringState === 'complete') {
|
||||||
const sdp = pc.localDescription?.sdp || '';
|
const sdp = pc.localDescription?.sdp || '';
|
||||||
const candidates = extractCandidatesFromSDP(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}`));
|
candidates.forEach(c => console.log(` ${c}`));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debug: Log ICE connection state
|
// Debug: Log ICE connection state
|
||||||
pc.oniceconnectionstatechange = () => {
|
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
|
// Trickle ICE - send candidates as they're discovered
|
||||||
pc.onicecandidate = (event) => {
|
pc.onicecandidate = (event) => {
|
||||||
if (event.candidate) {
|
if (event.candidate) {
|
||||||
const c = 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}`);
|
console.log(` → type: ${c.type}, protocol: ${c.protocol}, address: ${c.address}, port: ${c.port}`);
|
||||||
|
|
||||||
if (USE_TRICKLE_ICE) {
|
if (USE_TRICKLE_ICE) {
|
||||||
// Replace mDNS .local addresses with our real IP for direct LAN connectivity
|
// Replace mDNS .local addresses with our real IP for direct LAN connectivity
|
||||||
const candidateObj = event.candidate.toJSON();
|
const candidateObj = event.candidate.toJSON();
|
||||||
candidateObj.candidate = replaceMdnsWithIp(candidateObj.candidate, state.peerIp);
|
candidateObj.candidate = replaceMdnsWithIp(candidateObj.candidate, state.peerIp);
|
||||||
|
// Embed direction so the remote side routes to the right PC
|
||||||
send({
|
send({
|
||||||
type: 'ice_candidate',
|
type: 'ice_candidate',
|
||||||
from: state.peerId,
|
from: state.peerId,
|
||||||
to: peerId,
|
to: peerId,
|
||||||
candidate: JSON.stringify(candidateObj),
|
candidate: JSON.stringify({ ...candidateObj, _dir: direction }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} 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 = () => {
|
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;
|
return pc;
|
||||||
|
|
@ -782,15 +786,15 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't create a new connection if we already have one that's not closed
|
// Don't create a new outgoing connection if we already have one
|
||||||
if (peer.peerConnection && peer.peerConnection.connectionState !== 'closed') {
|
if (peer.outgoingPC && peer.outgoingPC.connectionState !== 'closed') {
|
||||||
console.log(`[${peerId.slice(0,8)}] sendOfferTo: already have connection`);
|
console.log(`[${peerId.slice(0,8)}] sendOfferTo: already have outgoing connection`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[${peerId.slice(0,8)}] sendOfferTo: creating offer (${USE_TRICKLE_ICE ? 'trickle' : 'bundled'})`);
|
console.log(`[${peerId.slice(0,8)}] sendOfferTo: creating offer (${USE_TRICKLE_ICE ? 'trickle' : 'bundled'})`);
|
||||||
const pc = createPeerConnection(peerId);
|
const pc = createPeerConnection(peerId, 'outgoing');
|
||||||
peer.peerConnection = pc;
|
peer.outgoingPC = pc;
|
||||||
|
|
||||||
// Add local stream tracks
|
// Add local stream tracks
|
||||||
if (state.localStream) {
|
if (state.localStream) {
|
||||||
|
|
@ -828,14 +832,14 @@
|
||||||
|
|
||||||
const peer = state.peers.get(fromPeerId);
|
const peer = state.peers.get(fromPeerId);
|
||||||
|
|
||||||
// Close existing connection if any (new offer = renegotiation)
|
// Close existing incoming connection if any (new offer = renegotiation)
|
||||||
if (peer.peerConnection) {
|
if (peer.incomingPC) {
|
||||||
peer.peerConnection.close();
|
peer.incomingPC.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[${fromPeerId.slice(0,8)}] handleOffer: creating answer (${USE_TRICKLE_ICE ? 'trickle' : 'bundled'})`);
|
console.log(`[${fromPeerId.slice(0,8)}] handleOffer: creating answer (${USE_TRICKLE_ICE ? 'trickle' : 'bundled'})`);
|
||||||
const pc = createPeerConnection(fromPeerId);
|
const pc = createPeerConnection(fromPeerId, 'incoming');
|
||||||
peer.peerConnection = pc;
|
peer.incomingPC = pc;
|
||||||
|
|
||||||
// Replace any mDNS .local addresses in the remote SDP with the peer's real IP
|
// Replace any mDNS .local addresses in the remote SDP with the peer's real IP
|
||||||
const sdp = JSON.parse(sdpJson);
|
const sdp = JSON.parse(sdpJson);
|
||||||
|
|
@ -869,14 +873,14 @@
|
||||||
|
|
||||||
async function handleAnswer(fromPeerId, sdpJson) {
|
async function handleAnswer(fromPeerId, sdpJson) {
|
||||||
const peer = state.peers.get(fromPeerId);
|
const peer = state.peers.get(fromPeerId);
|
||||||
if (!peer || !peer.peerConnection) {
|
if (!peer || !peer.outgoingPC) {
|
||||||
console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: no peer connection`);
|
console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: no outgoing peer connection`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only set remote description if we're in the right state
|
// Only set remote description if we're in the right state
|
||||||
if (peer.peerConnection.signalingState !== 'have-local-offer') {
|
if (peer.outgoingPC.signalingState !== 'have-local-offer') {
|
||||||
console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: wrong state: ${peer.peerConnection.signalingState}`);
|
console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: wrong state: ${peer.outgoingPC.signalingState}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -888,22 +892,32 @@
|
||||||
remoteCandidates.forEach(c => console.log(` ${c}`));
|
remoteCandidates.forEach(c => console.log(` ${c}`));
|
||||||
|
|
||||||
console.log(`[${fromPeerId.slice(0,8)}] handleAnswer: setting remote description`);
|
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) {
|
function handleIceCandidate(fromPeerId, candidateJson) {
|
||||||
const peer = state.peers.get(fromPeerId);
|
const peer = state.peers.get(fromPeerId);
|
||||||
if (!peer || !peer.peerConnection) {
|
if (!peer) {
|
||||||
console.warn(`[${fromPeerId.slice(0,8)}] handleIceCandidate: no peer connection`);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace any mDNS .local addresses with the peer's real IP
|
// Replace any mDNS .local addresses with the peer's real IP
|
||||||
const candidate = JSON.parse(candidateJson);
|
data.candidate = replaceMdnsWithIp(data.candidate, peer.ip);
|
||||||
candidate.candidate = replaceMdnsWithIp(candidate.candidate, peer.ip);
|
console.log(`[${fromPeerId.slice(0,8)}:${dir}] Remote candidate: ${data.candidate}`);
|
||||||
console.log(`[${fromPeerId.slice(0,8)}] Remote candidate: ${candidate.candidate}`);
|
|
||||||
|
|
||||||
peer.peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
|
pc.addIceCandidate(new RTCIceCandidate(data))
|
||||||
.then(() => console.log(`[${fromPeerId.slice(0,8)}] Successfully added remote candidate`))
|
.then(() => console.log(`[${fromPeerId.slice(0,8)}] Successfully added remote candidate`))
|
||||||
.catch(err => console.error(`[${fromPeerId.slice(0,8)}] Error adding ICE candidate:`, err));
|
.catch(err => console.error(`[${fromPeerId.slice(0,8)}] Error adding ICE candidate:`, err));
|
||||||
}
|
}
|
||||||
|
|
@ -953,11 +967,12 @@
|
||||||
|
|
||||||
state.isSharing = false;
|
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) {
|
for (const [peerId, peer] of state.peers) {
|
||||||
if (peer.peerConnection) {
|
if (peer.outgoingPC) {
|
||||||
peer.peerConnection.close();
|
peer.outgoingPC.close();
|
||||||
peer.peerConnection = null;
|
peer.outgoingPC = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue