Compare commits
No commits in common. "0df86e79ffd1fd0479fa50aa518a58c07ee0cca6" and "accbfdb1eafe5ebb4e1bceb2ba70cd1289772139" have entirely different histories.
0df86e79ff
...
accbfdb1ea
2 changed files with 98 additions and 250 deletions
|
|
@ -1,6 +1,5 @@
|
|||
use axum::{
|
||||
extract::{
|
||||
connect_info::ConnectInfo,
|
||||
ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
State,
|
||||
},
|
||||
|
|
@ -50,26 +49,11 @@ struct Args {
|
|||
#[arg(short, long, conflicts_with = "verbose")]
|
||||
quiet: bool,
|
||||
|
||||
/// TLS certificate file (PEM format)
|
||||
///
|
||||
/// When provided along with --key, the server will use HTTPS/WSS.
|
||||
/// A plain HTTP listener is also started on --http-port for clients
|
||||
/// that cannot validate the certificate (e.g. iOS with self-signed certs).
|
||||
/// If omitted, the server runs over plain HTTP only.
|
||||
#[arg(short, long)]
|
||||
certificate: Option<PathBuf>,
|
||||
certificate: PathBuf,
|
||||
|
||||
/// TLS private key file (PEM format)
|
||||
#[arg(short, long)]
|
||||
key: Option<PathBuf>,
|
||||
|
||||
/// Port for the plain HTTP listener (used alongside HTTPS)
|
||||
///
|
||||
/// When TLS is enabled, a second HTTP-only listener is started on this
|
||||
/// port so that iOS devices (which reject self-signed WSS certs) can
|
||||
/// connect via ws:// instead.
|
||||
#[arg(long, default_value_t = 8081, value_name = "PORT")]
|
||||
http_port: u16,
|
||||
key: PathBuf,
|
||||
}
|
||||
|
||||
/// Messages sent between peers via the signaling server
|
||||
|
|
@ -79,7 +63,6 @@ enum SignalMessage {
|
|||
/// Server assigns an ID to a new peer
|
||||
Welcome {
|
||||
peer_id: String,
|
||||
ip: String,
|
||||
peers: Vec<PeerInfo>,
|
||||
},
|
||||
/// Peer announces themselves (with optional name)
|
||||
|
|
@ -90,7 +73,6 @@ enum SignalMessage {
|
|||
PeerJoined {
|
||||
peer_id: String,
|
||||
name: String,
|
||||
ip: String,
|
||||
},
|
||||
/// Broadcast: a peer left
|
||||
PeerLeft {
|
||||
|
|
@ -138,7 +120,6 @@ enum SignalMessage {
|
|||
struct PeerInfo {
|
||||
peer_id: String,
|
||||
name: String,
|
||||
ip: String,
|
||||
is_sharing: bool,
|
||||
has_audio: bool,
|
||||
}
|
||||
|
|
@ -146,7 +127,6 @@ struct PeerInfo {
|
|||
/// State for a connected peer
|
||||
struct PeerState {
|
||||
name: String,
|
||||
ip: String,
|
||||
is_sharing: bool,
|
||||
has_audio: bool,
|
||||
tx: broadcast::Sender<SignalMessage>,
|
||||
|
|
@ -170,7 +150,6 @@ impl AppState {
|
|||
.map(|entry| PeerInfo {
|
||||
peer_id: entry.key().clone(),
|
||||
name: entry.value().name.clone(),
|
||||
ip: entry.value().ip.clone(),
|
||||
is_sharing: entry.value().is_sharing,
|
||||
has_audio: entry.value().has_audio,
|
||||
})
|
||||
|
|
@ -214,8 +193,6 @@ async fn main() {
|
|||
.parse()
|
||||
.expect("Invalid host:port combination");
|
||||
|
||||
let use_tls = args.certificate.is_some() && args.key.is_some();
|
||||
|
||||
println!();
|
||||
println!(" ╭─────────────────────────────────────────╮");
|
||||
println!(" │ \x1b[1;33mLAN Share\x1b[0m │");
|
||||
|
|
@ -223,27 +200,12 @@ async fn main() {
|
|||
println!(" ╰─────────────────────────────────────────╯");
|
||||
println!();
|
||||
println!(" \x1b[1mServer running at:\x1b[0m");
|
||||
|
||||
if use_tls {
|
||||
println!(" → https://{}", addr);
|
||||
let http_addr: SocketAddr = format!("{}:{}", args.host, args.http_port)
|
||||
.parse()
|
||||
.expect("Invalid host:http_port combination");
|
||||
println!(" → http://{} \x1b[90m(plain HTTP for iOS)\x1b[0m", http_addr);
|
||||
if args.host == "0.0.0.0" {
|
||||
if let Ok(local_ip) = local_ip_address::local_ip() {
|
||||
println!(" → https://{}:{}", local_ip, args.port);
|
||||
println!(" → http://{}:{} \x1b[90m(plain HTTP for iOS)\x1b[0m", local_ip, args.http_port);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(" → http://{}", addr);
|
||||
if args.host == "0.0.0.0" {
|
||||
if let Ok(local_ip) = local_ip_address::local_ip() {
|
||||
println!(" → http://{}:{}", local_ip, args.port);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
println!(" \x1b[90mOpen this URL in browsers on your local network.\x1b[0m");
|
||||
println!(" \x1b[90mPress Ctrl+C to stop the server.\x1b[0m");
|
||||
|
|
@ -251,53 +213,22 @@ async fn main() {
|
|||
|
||||
info!("Server starting on {}", addr);
|
||||
|
||||
if use_tls {
|
||||
let cert = args.certificate.unwrap();
|
||||
let key = args.key.unwrap();
|
||||
let config = RustlsConfig::from_pem_file(cert, key).await.unwrap();
|
||||
|
||||
// Start a plain HTTP listener alongside HTTPS so that iOS devices
|
||||
// (which silently reject self-signed WSS certificates) can connect
|
||||
// via ws:// instead.
|
||||
let http_addr: SocketAddr = format!("{}:{}", args.host, args.http_port)
|
||||
.parse()
|
||||
.expect("Invalid host:http_port combination");
|
||||
let http_app = app.clone();
|
||||
info!("Plain HTTP listener on {}", http_addr);
|
||||
tokio::spawn(async move {
|
||||
let listener = tokio::net::TcpListener::bind(http_addr).await.unwrap();
|
||||
axum::serve(listener, http_app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
let config = RustlsConfig::from_pem_file(args.certificate, args.key).await.unwrap();
|
||||
|
||||
axum_server::bind_rustls(addr, config)
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
axum_server::bind_rustls(addr, config).serve(app.into_make_service()).await.unwrap();
|
||||
}
|
||||
|
||||
async fn serve_index() -> impl IntoResponse {
|
||||
Html(include_str!("../static/index.html"))
|
||||
}
|
||||
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, state, addr))
|
||||
async fn ws_handler(ws: WebSocketUpgrade, State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, state))
|
||||
}
|
||||
|
||||
async fn handle_socket(socket: WebSocket, state: Arc<AppState>, addr: SocketAddr) {
|
||||
async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
let peer_id = Uuid::new_v4().to_string();
|
||||
let peer_ip = addr.ip().to_string();
|
||||
let (tx, _) = broadcast::channel::<SignalMessage>(64);
|
||||
|
||||
// Add peer to state with default name
|
||||
|
|
@ -305,7 +236,6 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, addr: SocketAddr
|
|||
peer_id.clone(),
|
||||
PeerState {
|
||||
name: format!("Peer {}", &peer_id[..8]),
|
||||
ip: peer_ip.clone(),
|
||||
is_sharing: false,
|
||||
has_audio: false,
|
||||
tx: tx.clone(),
|
||||
|
|
@ -320,7 +250,6 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, addr: SocketAddr
|
|||
// Send welcome message with current peer list
|
||||
let welcome = SignalMessage::Welcome {
|
||||
peer_id: peer_id.clone(),
|
||||
ip: peer_ip,
|
||||
peers: peers.into_iter().filter(|p| p.peer_id != peer_id).collect(),
|
||||
};
|
||||
|
||||
|
|
@ -390,11 +319,6 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, addr: SocketAddr
|
|||
async fn handle_signal(state: &AppState, from_peer: &str, signal: SignalMessage) {
|
||||
match signal {
|
||||
SignalMessage::Announce { name } => {
|
||||
let ip = state
|
||||
.peers
|
||||
.get(from_peer)
|
||||
.map(|p| p.ip.clone())
|
||||
.unwrap_or_default();
|
||||
if let Some(mut peer) = state.peers.get_mut(from_peer) {
|
||||
peer.name = name.clone();
|
||||
}
|
||||
|
|
@ -402,7 +326,6 @@ async fn handle_signal(state: &AppState, from_peer: &str, signal: SignalMessage)
|
|||
SignalMessage::PeerJoined {
|
||||
peer_id: from_peer.to_string(),
|
||||
name,
|
||||
ip,
|
||||
},
|
||||
Some(from_peer),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -428,9 +428,8 @@
|
|||
// ==================== State ====================
|
||||
const state = {
|
||||
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, outgoingPC, incomingPC }
|
||||
peers: new Map(), // peer_id -> { name, isSharing, hasAudio, stream, peerConnection }
|
||||
localStream: null,
|
||||
isSharing: false,
|
||||
includeAudio: true,
|
||||
|
|
@ -457,14 +456,6 @@
|
|||
return candidates;
|
||||
}
|
||||
|
||||
// Replace mDNS .local addresses with a real IP.
|
||||
// Modern browsers obfuscate local IPs as UUIDs ending in .local for privacy,
|
||||
// which breaks direct LAN connections when mDNS resolution isn't available.
|
||||
function replaceMdnsWithIp(str, realIp) {
|
||||
if (!str || !realIp) return str;
|
||||
return str.replace(/[a-f0-9-]+\.local/gi, realIp);
|
||||
}
|
||||
|
||||
console.log(`ICE mode: ${USE_TRICKLE_ICE ? 'TRICKLE' : 'BUNDLED'}`);
|
||||
|
||||
// ==================== DOM Elements ====================
|
||||
|
|
@ -504,25 +495,7 @@
|
|||
connectionStatus.textContent = 'Establishing WebSocket connection...';
|
||||
state.ws = new WebSocket(wsUrl);
|
||||
|
||||
// iOS Safari silently rejects wss:// connections to servers with
|
||||
// self-signed certificates — onopen never fires and the UI hangs.
|
||||
// Use a timeout to detect this and show a helpful message.
|
||||
const connectTimeout = setTimeout(() => {
|
||||
if (state.ws && state.ws.readyState !== WebSocket.OPEN) {
|
||||
console.warn('WebSocket connection timed out');
|
||||
state.ws.close();
|
||||
if (window.location.protocol === 'https:') {
|
||||
const httpPort = parseInt(window.location.port || '443', 10) + 1;
|
||||
const httpUrl = `http://${window.location.hostname}:${httpPort}`;
|
||||
connectionStatus.innerHTML =
|
||||
'Connection failed — iOS may not support self-signed WSS certificates.<br>' +
|
||||
'Try the plain HTTP URL instead: <a href="' + httpUrl + '" style="color:#60a5fa;text-decoration:underline">' + httpUrl + '</a>';
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
state.ws.onopen = () => {
|
||||
clearTimeout(connectTimeout);
|
||||
console.log('WebSocket connected');
|
||||
state.reconnectAttempts = 0;
|
||||
connectionStatus.textContent = 'Connected, waiting for welcome...';
|
||||
|
|
@ -534,7 +507,6 @@
|
|||
};
|
||||
|
||||
state.ws.onclose = () => {
|
||||
clearTimeout(connectTimeout);
|
||||
console.log('WebSocket disconnected');
|
||||
updateConnectionStatus(false);
|
||||
|
||||
|
|
@ -548,7 +520,6 @@
|
|||
};
|
||||
|
||||
state.ws.onerror = (err) => {
|
||||
clearTimeout(connectTimeout);
|
||||
console.error('WebSocket error:', err);
|
||||
};
|
||||
}
|
||||
|
|
@ -566,8 +537,6 @@
|
|||
switch (msg.type) {
|
||||
case 'welcome':
|
||||
state.peerId = msg.peer_id;
|
||||
state.peerIp = msg.ip;
|
||||
console.log(`Our IP as seen by server: ${msg.ip}`);
|
||||
connectingOverlay.style.display = 'none';
|
||||
updateConnectionStatus(true);
|
||||
|
||||
|
|
@ -581,7 +550,7 @@
|
|||
|
||||
// Add existing peers
|
||||
for (const peer of msg.peers) {
|
||||
addPeer(peer.peer_id, peer.name, peer.ip, peer.is_sharing, peer.has_audio);
|
||||
addPeer(peer.peer_id, peer.name, peer.is_sharing, peer.has_audio);
|
||||
// Request stream from peers that are already sharing
|
||||
if (peer.is_sharing) {
|
||||
requestStreamFrom(peer.peer_id);
|
||||
|
|
@ -591,7 +560,7 @@
|
|||
break;
|
||||
|
||||
case 'peer_joined':
|
||||
addPeer(msg.peer_id, msg.name, msg.ip, false, false);
|
||||
addPeer(msg.peer_id, msg.name, false, false);
|
||||
// If we're sharing, proactively send an offer to the new peer
|
||||
if (state.isSharing && state.localStream) {
|
||||
await sendOfferTo(msg.peer_id);
|
||||
|
|
@ -621,11 +590,9 @@
|
|||
peer.isSharing = false;
|
||||
peer.hasAudio = false;
|
||||
peer.stream = 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 (peer.peerConnection) {
|
||||
peer.peerConnection.close();
|
||||
peer.peerConnection = null;
|
||||
}
|
||||
if (state.selectedPeerId === msg.peer_id) {
|
||||
state.selectedPeerId = null;
|
||||
|
|
@ -640,7 +607,7 @@
|
|||
if (state.isSharing && state.localStream) {
|
||||
// Ensure we have this peer in our state (handles race condition)
|
||||
if (!state.peers.has(msg.from)) {
|
||||
addPeer(msg.from, `Peer ${msg.from.slice(0, 4)}`, null, false, false);
|
||||
addPeer(msg.from, `Peer ${msg.from.slice(0, 4)}`, false, false);
|
||||
}
|
||||
await sendOfferTo(msg.from);
|
||||
}
|
||||
|
|
@ -661,25 +628,24 @@
|
|||
}
|
||||
|
||||
// ==================== Peer Management ====================
|
||||
function addPeer(peerId, name, ip, isSharing, hasAudio) {
|
||||
function addPeer(peerId, name, isSharing, hasAudio) {
|
||||
if (peerId === state.peerId) return;
|
||||
|
||||
state.peers.set(peerId, {
|
||||
name,
|
||||
ip,
|
||||
isSharing,
|
||||
hasAudio,
|
||||
stream: null,
|
||||
outgoingPC: null, // PC where we send our stream to this peer
|
||||
incomingPC: null, // PC where we receive this peer's stream
|
||||
peerConnection: null,
|
||||
});
|
||||
}
|
||||
|
||||
function removePeer(peerId) {
|
||||
const peer = state.peers.get(peerId);
|
||||
if (peer) {
|
||||
if (peer.outgoingPC) peer.outgoingPC.close();
|
||||
if (peer.incomingPC) peer.incomingPC.close();
|
||||
if (peer.peerConnection) {
|
||||
peer.peerConnection.close();
|
||||
}
|
||||
state.peers.delete(peerId);
|
||||
|
||||
if (state.selectedPeerId === peerId) {
|
||||
|
|
@ -690,47 +656,42 @@
|
|||
}
|
||||
|
||||
// ==================== WebRTC ====================
|
||||
// direction: 'outgoing' (we send offer) or 'incoming' (we receive offer)
|
||||
function createPeerConnection(peerId, direction) {
|
||||
function createPeerConnection(peerId) {
|
||||
const pc = new RTCPeerConnection(rtcConfig);
|
||||
|
||||
// Debug: Log ICE gathering state changes
|
||||
pc.onicegatheringstatechange = () => {
|
||||
console.log(`[${peerId.slice(0,8)}:${direction}] ICE gathering state: ${pc.iceGatheringState}`);
|
||||
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)}:${direction}] Final SDP has ${candidates.length} candidates:`);
|
||||
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)}:${direction}] ICE connection state: ${pc.iceConnectionState}`);
|
||||
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)}:${direction}] Local candidate: ${c.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) {
|
||||
// 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, _dir: direction }),
|
||||
candidate: JSON.stringify(event.candidate),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(`[${peerId.slice(0,8)}:${direction}] ICE candidate gathering complete (null candidate)`);
|
||||
console.log(`[${peerId.slice(0,8)}] ICE candidate gathering complete (null candidate)`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -738,15 +699,7 @@
|
|||
console.log(`[${peerId.slice(0,8)}] Received track:`, event.track.kind);
|
||||
const peer = state.peers.get(peerId);
|
||||
if (peer) {
|
||||
// Use the associated stream, or build one from the track
|
||||
// (event.streams can be empty in some WebRTC edge cases)
|
||||
if (event.streams[0]) {
|
||||
peer.stream = event.streams[0];
|
||||
} else if (!peer.stream) {
|
||||
peer.stream = new MediaStream([event.track]);
|
||||
} else {
|
||||
peer.stream.addTrack(event.track);
|
||||
}
|
||||
updateUI();
|
||||
|
||||
// Auto-select if this is the first/only stream
|
||||
|
|
@ -757,7 +710,7 @@
|
|||
};
|
||||
|
||||
pc.onconnectionstatechange = () => {
|
||||
console.log(`[${peerId.slice(0,8)}:${direction}] Connection state: ${pc.connectionState}`);
|
||||
console.log(`[${peerId.slice(0,8)}] Connection state: ${pc.connectionState}`);
|
||||
};
|
||||
|
||||
return pc;
|
||||
|
|
@ -806,15 +759,15 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// 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`);
|
||||
// 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`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[${peerId.slice(0,8)}] sendOfferTo: creating offer (${USE_TRICKLE_ICE ? 'trickle' : 'bundled'})`);
|
||||
const pc = createPeerConnection(peerId, 'outgoing');
|
||||
peer.outgoingPC = pc;
|
||||
const pc = createPeerConnection(peerId);
|
||||
peer.peerConnection = pc;
|
||||
|
||||
// Add local stream tracks
|
||||
if (state.localStream) {
|
||||
|
|
@ -831,8 +784,7 @@
|
|||
await waitForIceGathering(pc, peerId);
|
||||
}
|
||||
|
||||
// Replace mDNS .local addresses with our real IP for direct LAN connectivity
|
||||
const sdpToSend = { type: pc.localDescription.type, sdp: replaceMdnsWithIp(pc.localDescription.sdp, state.peerIp) };
|
||||
const sdpToSend = pc.localDescription;
|
||||
const candidates = extractCandidatesFromSDP(sdpToSend.sdp);
|
||||
console.log(`[${peerId.slice(0,8)}] Sending offer with ${candidates.length} candidates in SDP`);
|
||||
|
||||
|
|
@ -847,23 +799,21 @@
|
|||
async function handleOffer(fromPeerId, sdpJson) {
|
||||
// Ensure we have this peer in state
|
||||
if (!state.peers.has(fromPeerId)) {
|
||||
addPeer(fromPeerId, `Peer ${fromPeerId.slice(0, 4)}`, null, true, false);
|
||||
addPeer(fromPeerId, `Peer ${fromPeerId.slice(0, 4)}`, true, false);
|
||||
}
|
||||
|
||||
const peer = state.peers.get(fromPeerId);
|
||||
|
||||
// Close existing incoming connection if any (new offer = renegotiation)
|
||||
if (peer.incomingPC) {
|
||||
peer.incomingPC.close();
|
||||
// Close existing connection if any (new offer = renegotiation)
|
||||
if (peer.peerConnection) {
|
||||
peer.peerConnection.close();
|
||||
}
|
||||
|
||||
console.log(`[${fromPeerId.slice(0,8)}] handleOffer: creating answer (${USE_TRICKLE_ICE ? 'trickle' : 'bundled'})`);
|
||||
const pc = createPeerConnection(fromPeerId, 'incoming');
|
||||
peer.incomingPC = pc;
|
||||
const pc = createPeerConnection(fromPeerId);
|
||||
peer.peerConnection = pc;
|
||||
|
||||
// Replace any mDNS .local addresses in the remote SDP with the peer's real IP
|
||||
const sdp = JSON.parse(sdpJson);
|
||||
sdp.sdp = replaceMdnsWithIp(sdp.sdp, peer.ip);
|
||||
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}`));
|
||||
|
|
@ -878,8 +828,7 @@
|
|||
await waitForIceGathering(pc, fromPeerId);
|
||||
}
|
||||
|
||||
// Replace mDNS .local addresses with our real IP for direct LAN connectivity
|
||||
const sdpToSend = { type: pc.localDescription.type, sdp: replaceMdnsWithIp(pc.localDescription.sdp, state.peerIp) };
|
||||
const sdpToSend = pc.localDescription;
|
||||
const localCandidates = extractCandidatesFromSDP(sdpToSend.sdp);
|
||||
console.log(`[${fromPeerId.slice(0,8)}] Sending answer with ${localCandidates.length} candidates in SDP`);
|
||||
|
||||
|
|
@ -893,51 +842,37 @@
|
|||
|
||||
async function handleAnswer(fromPeerId, sdpJson) {
|
||||
const peer = state.peers.get(fromPeerId);
|
||||
if (!peer || !peer.outgoingPC) {
|
||||
console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: no outgoing peer connection`);
|
||||
if (!peer || !peer.peerConnection) {
|
||||
console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: no peer connection`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only set remote description if we're in the right state
|
||||
if (peer.outgoingPC.signalingState !== 'have-local-offer') {
|
||||
console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: wrong state: ${peer.outgoingPC.signalingState}`);
|
||||
if (peer.peerConnection.signalingState !== 'have-local-offer') {
|
||||
console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: wrong state: ${peer.peerConnection.signalingState}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace any mDNS .local addresses in the remote SDP with the peer's real IP
|
||||
const sdp = JSON.parse(sdpJson);
|
||||
sdp.sdp = replaceMdnsWithIp(sdp.sdp, peer.ip);
|
||||
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.outgoingPC.setRemoteDescription(new RTCSessionDescription(sdp));
|
||||
await peer.peerConnection.setRemoteDescription(new RTCSessionDescription(sdp));
|
||||
}
|
||||
|
||||
function handleIceCandidate(fromPeerId, candidateJson) {
|
||||
const peer = state.peers.get(fromPeerId);
|
||||
if (!peer) {
|
||||
console.warn(`[${fromPeerId.slice(0,8)}] handleIceCandidate: peer not found`);
|
||||
if (!peer || !peer.peerConnection) {
|
||||
console.warn(`[${fromPeerId.slice(0,8)}] handleIceCandidate: no peer connection`);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(candidateJson);
|
||||
const dir = data._dir;
|
||||
delete data._dir;
|
||||
const candidate = JSON.parse(candidateJson);
|
||||
console.log(`[${fromPeerId.slice(0,8)}] Remote candidate: ${candidate.candidate}`);
|
||||
|
||||
// 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
|
||||
data.candidate = replaceMdnsWithIp(data.candidate, peer.ip);
|
||||
console.log(`[${fromPeerId.slice(0,8)}:${dir}] Remote candidate: ${data.candidate}`);
|
||||
|
||||
pc.addIceCandidate(new RTCIceCandidate(data))
|
||||
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));
|
||||
}
|
||||
|
|
@ -955,21 +890,13 @@
|
|||
stopSharing();
|
||||
};
|
||||
|
||||
// Notify server
|
||||
// Notify server - other peers will send request_stream in response
|
||||
send({
|
||||
type: 'started_sharing',
|
||||
peer_id: state.peerId,
|
||||
has_audio: stream.getAudioTracks().length > 0,
|
||||
});
|
||||
|
||||
// Proactively send offers to all connected peers so they
|
||||
// receive the stream immediately. Without this, we rely on
|
||||
// a request_stream round-trip that can silently fail because
|
||||
// handleSignal is async but never awaited by ws.onmessage.
|
||||
for (const [peerId] of state.peers) {
|
||||
sendOfferTo(peerId);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
} catch (err) {
|
||||
console.error('Error starting screen share:', err);
|
||||
|
|
@ -987,12 +914,11 @@
|
|||
|
||||
state.isSharing = false;
|
||||
|
||||
// Only close outgoing connections (where we were sending our stream).
|
||||
// Keep incoming connections alive so we can still view others' streams.
|
||||
// Close all peer connections we initiated
|
||||
for (const [peerId, peer] of state.peers) {
|
||||
if (peer.outgoingPC) {
|
||||
peer.outgoingPC.close();
|
||||
peer.outgoingPC = null;
|
||||
if (peer.peerConnection) {
|
||||
peer.peerConnection.close();
|
||||
peer.peerConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1024,9 +950,8 @@
|
|||
// Update peer count
|
||||
peerCount.textContent = state.peers.size;
|
||||
|
||||
// Update sharing buttons (hide entirely if browser lacks getDisplayMedia)
|
||||
const canShare = !!navigator.mediaDevices?.getDisplayMedia;
|
||||
startShareBtn.classList.toggle('hidden', state.isSharing || !canShare);
|
||||
// Update sharing buttons
|
||||
startShareBtn.classList.toggle('hidden', state.isSharing);
|
||||
stopShareBtn.classList.toggle('hidden', !state.isSharing);
|
||||
|
||||
// Update screen list
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue