Update
Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
parent
c23d7d06e0
commit
accbfdb1ea
2 changed files with 127 additions and 34 deletions
|
|
@ -105,8 +105,7 @@ lanshare -v
|
|||
│ Signaling Server │
|
||||
│ (Rust/Axum/WebSocket) │
|
||||
│ - Peer discovery │
|
||||
│ - Relays SDP offers/answers │
|
||||
│ - Relays ICE candidates │
|
||||
│ - Relays SDP offers/answers (with bundled candidates) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│ │
|
||||
│ WebSocket │ WebSocket
|
||||
|
|
@ -114,11 +113,15 @@ lanshare -v
|
|||
▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ Peer A │◄────────────────────►│ Peer B │
|
||||
│ (Browser) │ WebRTC P2P │ (Browser) │
|
||||
│ (Browser) │ Direct WebRTC │ (Browser) │
|
||||
└─────────────┘ (video/audio) └─────────────┘
|
||||
```
|
||||
|
||||
The server only handles signaling — the actual video/audio streams flow directly between browsers via WebRTC.
|
||||
**100% Local Network** — No external services, simple direct connections:
|
||||
- No STUN/TURN servers needed
|
||||
- No trickle ICE — candidates bundled directly in SDP
|
||||
- One offer + one answer = connected
|
||||
- Video/audio streams stay entirely on your LAN
|
||||
|
||||
## Browser Support
|
||||
|
||||
|
|
|
|||
|
|
@ -439,14 +439,25 @@
|
|||
maxReconnectAttempts: 5,
|
||||
};
|
||||
|
||||
// WebRTC config - using public STUN servers for ICE
|
||||
// WebRTC config - no STUN/TURN needed for LAN-only connections
|
||||
// WebRTC will use "host" ICE candidates (local IPs) which work directly on LAN
|
||||
// WebRTC config - LAN only, no STUN/TURN servers needed
|
||||
const rtcConfig = {
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||
]
|
||||
iceServers: [],
|
||||
};
|
||||
|
||||
// Toggle this to test bundled vs trickle ICE
|
||||
const USE_TRICKLE_ICE = true; // Set to false to test bundled approach
|
||||
|
||||
// Debug: Parse and log SDP candidates
|
||||
function extractCandidatesFromSDP(sdp) {
|
||||
const lines = sdp.split('\r\n');
|
||||
const candidates = lines.filter(line => line.startsWith('a=candidate:'));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
console.log(`ICE mode: ${USE_TRICKLE_ICE ? 'TRICKLE' : 'BUNDLED'}`);
|
||||
|
||||
// ==================== DOM Elements ====================
|
||||
const $ = id => document.getElementById(id);
|
||||
const connectingOverlay = $('connectingOverlay');
|
||||
|
|
@ -611,7 +622,7 @@
|
|||
break;
|
||||
|
||||
case 'ice_candidate':
|
||||
await handleIceCandidate(msg.from, msg.candidate);
|
||||
handleIceCandidate(msg.from, msg.candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -648,8 +659,30 @@
|
|||
function createPeerConnection(peerId) {
|
||||
const pc = new RTCPeerConnection(rtcConfig);
|
||||
|
||||
// Debug: Log ICE gathering state changes
|
||||
pc.onicegatheringstatechange = () => {
|
||||
console.log(`[${peerId.slice(0,8)}] 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:`);
|
||||
candidates.forEach(c => console.log(` ${c}`));
|
||||
}
|
||||
};
|
||||
|
||||
// Debug: Log ICE connection state
|
||||
pc.oniceconnectionstatechange = () => {
|
||||
console.log(`[${peerId.slice(0,8)}] 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(` → type: ${c.type}, protocol: ${c.protocol}, address: ${c.address}, port: ${c.port}`);
|
||||
|
||||
if (USE_TRICKLE_ICE) {
|
||||
send({
|
||||
type: 'ice_candidate',
|
||||
from: state.peerId,
|
||||
|
|
@ -657,10 +690,13 @@
|
|||
candidate: JSON.stringify(event.candidate),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(`[${peerId.slice(0,8)}] ICE candidate gathering complete (null candidate)`);
|
||||
}
|
||||
};
|
||||
|
||||
pc.ontrack = (event) => {
|
||||
console.log('Received track from', peerId);
|
||||
console.log(`[${peerId.slice(0,8)}] Received track:`, event.track.kind);
|
||||
const peer = state.peers.get(peerId);
|
||||
if (peer) {
|
||||
peer.stream = event.streams[0];
|
||||
|
|
@ -674,7 +710,7 @@
|
|||
};
|
||||
|
||||
pc.onconnectionstatechange = () => {
|
||||
console.log(`Connection state with ${peerId}: ${pc.connectionState}`);
|
||||
console.log(`[${peerId.slice(0,8)}] Connection state: ${pc.connectionState}`);
|
||||
};
|
||||
|
||||
return pc;
|
||||
|
|
@ -688,6 +724,34 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Wait for ICE gathering to complete (for bundled mode)
|
||||
function waitForIceGathering(pc, peerId) {
|
||||
return new Promise((resolve) => {
|
||||
if (pc.iceGatheringState === 'complete') {
|
||||
console.log(`[${peerId.slice(0,8)}] ICE already complete`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const checkState = () => {
|
||||
if (pc.iceGatheringState === 'complete') {
|
||||
pc.removeEventListener('icegatheringstatechange', checkState);
|
||||
console.log(`[${peerId.slice(0,8)}] ICE gathering finished`);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
pc.addEventListener('icegatheringstatechange', checkState);
|
||||
|
||||
// Timeout after 2s
|
||||
setTimeout(() => {
|
||||
pc.removeEventListener('icegatheringstatechange', checkState);
|
||||
console.log(`[${peerId.slice(0,8)}] ICE gathering timeout (state: ${pc.iceGatheringState})`);
|
||||
resolve();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
async function sendOfferTo(peerId) {
|
||||
const peer = state.peers.get(peerId);
|
||||
if (!peer) {
|
||||
|
|
@ -697,11 +761,11 @@
|
|||
|
||||
// Don't create a new connection if we already have one that's not closed
|
||||
if (peer.peerConnection && peer.peerConnection.connectionState !== 'closed') {
|
||||
console.log('sendOfferTo: already have connection to', peerId);
|
||||
console.log(`[${peerId.slice(0,8)}] sendOfferTo: already have connection`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('sendOfferTo: creating offer for', peerId);
|
||||
console.log(`[${peerId.slice(0,8)}] sendOfferTo: creating offer (${USE_TRICKLE_ICE ? 'trickle' : 'bundled'})`);
|
||||
const pc = createPeerConnection(peerId);
|
||||
peer.peerConnection = pc;
|
||||
|
||||
|
|
@ -715,11 +779,20 @@
|
|||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
|
||||
if (!USE_TRICKLE_ICE) {
|
||||
// Bundled mode: wait for all candidates to be in SDP
|
||||
await waitForIceGathering(pc, peerId);
|
||||
}
|
||||
|
||||
const sdpToSend = pc.localDescription;
|
||||
const candidates = extractCandidatesFromSDP(sdpToSend.sdp);
|
||||
console.log(`[${peerId.slice(0,8)}] Sending offer with ${candidates.length} candidates in SDP`);
|
||||
|
||||
send({
|
||||
type: 'offer',
|
||||
from: state.peerId,
|
||||
to: peerId,
|
||||
sdp: JSON.stringify(pc.localDescription),
|
||||
sdp: JSON.stringify(sdpToSend),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -736,55 +809,72 @@
|
|||
peer.peerConnection.close();
|
||||
}
|
||||
|
||||
console.log('handleOffer: creating answer for', fromPeerId);
|
||||
console.log(`[${fromPeerId.slice(0,8)}] handleOffer: creating answer (${USE_TRICKLE_ICE ? 'trickle' : 'bundled'})`);
|
||||
const pc = createPeerConnection(fromPeerId);
|
||||
peer.peerConnection = pc;
|
||||
|
||||
const sdp = JSON.parse(sdpJson);
|
||||
const remoteCandidates = extractCandidatesFromSDP(sdp.sdp);
|
||||
console.log(`[${fromPeerId.slice(0,8)}] Received offer with ${remoteCandidates.length} candidates in SDP:`);
|
||||
remoteCandidates.forEach(c => console.log(` ${c}`));
|
||||
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(sdp));
|
||||
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
|
||||
if (!USE_TRICKLE_ICE) {
|
||||
// Bundled mode: wait for all candidates to be in SDP
|
||||
await waitForIceGathering(pc, fromPeerId);
|
||||
}
|
||||
|
||||
const sdpToSend = pc.localDescription;
|
||||
const localCandidates = extractCandidatesFromSDP(sdpToSend.sdp);
|
||||
console.log(`[${fromPeerId.slice(0,8)}] Sending answer with ${localCandidates.length} candidates in SDP`);
|
||||
|
||||
send({
|
||||
type: 'answer',
|
||||
from: state.peerId,
|
||||
to: fromPeerId,
|
||||
sdp: JSON.stringify(pc.localDescription),
|
||||
sdp: JSON.stringify(sdpToSend),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAnswer(fromPeerId, sdpJson) {
|
||||
const peer = state.peers.get(fromPeerId);
|
||||
if (!peer || !peer.peerConnection) {
|
||||
console.warn('handleAnswer: no peer connection for', fromPeerId);
|
||||
console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: no peer connection`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only set remote description if we're in the right state
|
||||
if (peer.peerConnection.signalingState !== 'have-local-offer') {
|
||||
console.warn('handleAnswer: wrong state', peer.peerConnection.signalingState, 'for', fromPeerId);
|
||||
console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: wrong state: ${peer.peerConnection.signalingState}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('handleAnswer: setting remote description for', fromPeerId);
|
||||
const sdp = JSON.parse(sdpJson);
|
||||
const remoteCandidates = extractCandidatesFromSDP(sdp.sdp);
|
||||
console.log(`[${fromPeerId.slice(0,8)}] Received answer with ${remoteCandidates.length} candidates in SDP:`);
|
||||
remoteCandidates.forEach(c => console.log(` ${c}`));
|
||||
|
||||
console.log(`[${fromPeerId.slice(0,8)}] handleAnswer: setting remote description`);
|
||||
await peer.peerConnection.setRemoteDescription(new RTCSessionDescription(sdp));
|
||||
}
|
||||
|
||||
async function handleIceCandidate(fromPeerId, candidateJson) {
|
||||
function handleIceCandidate(fromPeerId, candidateJson) {
|
||||
const peer = state.peers.get(fromPeerId);
|
||||
if (!peer || !peer.peerConnection) {
|
||||
console.warn('handleIceCandidate: no peer connection for', fromPeerId);
|
||||
console.warn(`[${fromPeerId.slice(0,8)}] handleIceCandidate: no peer connection`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const candidate = JSON.parse(candidateJson);
|
||||
await peer.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
|
||||
} catch (err) {
|
||||
console.warn('handleIceCandidate: error adding candidate:', err);
|
||||
}
|
||||
console.log(`[${fromPeerId.slice(0,8)}] Remote candidate: ${candidate.candidate}`);
|
||||
|
||||
peer.peerConnection.addIceCandidate(new RTCIceCandidate(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));
|
||||
}
|
||||
|
||||
// ==================== Screen Sharing ====================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue