diff --git a/internal/mesh/peer.go b/internal/mesh/peer.go index e2c1cf4..a5888df 100644 --- a/internal/mesh/peer.go +++ b/internal/mesh/peer.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" + "fmt" "log" "strings" "sync" @@ -94,6 +95,17 @@ func WireDataChannel( m.RemovePeer(peerID) pc.Close() }) + + pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { + m.Emit(proto.IpcMessage{ + Type: proto.EvtPeerStatus, + PeerID: &peerID, + ConnState: state.String(), + }) + if state == webrtc.PeerConnectionStateConnected { + go emitICEStats(pc, peerID, m) + } + }) } // WireCandidateTrickle seals and forwards each ICE candidate via the anchor as it arrives. @@ -257,6 +269,42 @@ func dispatchPeerMessage(msg proto.PeerMessage, from proto.PeerID, m *Mesh) { } } +// emitICEStats reads the active (nominated) ICE candidate pair from pion's stats +// and emits a peer_status event with the candidate type and remote address. +func emitICEStats(pc *webrtc.PeerConnection, peerID proto.PeerID, m *Mesh) { + stats := pc.GetStats() + candidateType := "unknown" + remoteAddress := "" + + // Find the nominated candidate pair and its remote candidate. + for _, s := range stats { + pair, ok := s.(webrtc.ICECandidatePairStats) + if !ok || !pair.Nominated { + continue + } + // Look up remote candidate by its ID. + if rc, ok2 := stats[pair.RemoteCandidateID]; ok2 { + remote, ok3 := rc.(webrtc.ICECandidateStats) + if ok3 { + candidateType = remote.CandidateType.String() + if remote.Port > 0 { + remoteAddress = fmt.Sprintf("%s:%d", remote.IP, remote.Port) + } else { + remoteAddress = remote.IP + } + } + } + break + } + + m.Emit(proto.IpcMessage{ + Type: proto.EvtPeerStatus, + PeerID: &peerID, + CandidateType: candidateType, + RemoteAddress: remoteAddress, + }) +} + // midOrRandom returns mid if non-empty, otherwise generates a random 16-byte hex string. // Ensures every stored message has a unique mid even from peers that don't send one. func midOrRandom(mid string) string { diff --git a/internal/proto/proto.go b/internal/proto/proto.go index 32b3d75..b56b263 100644 --- a/internal/proto/proto.go +++ b/internal/proto/proto.go @@ -228,6 +228,7 @@ const ( EvtPeerConnected IpcMsgType = "peer_connected" EvtPeerDisconnected IpcMsgType = "peer_disconnected" EvtSessionReady IpcMsgType = "session_ready" // DataChannel open + hello verified + EvtPeerStatus IpcMsgType = "peer_status" // ICE connection state + candidate type EvtIncomingFile IpcMsgType = "incoming_file" EvtFileProgress IpcMsgType = "file_progress" EvtStateSnapshot IpcMsgType = "state_snapshot" @@ -294,4 +295,8 @@ type IpcMessage struct { // export_identity / import_identity Passphrase string `json:"passphrase,omitempty"` // import only; never echoed back Backup string `json:"backup,omitempty"` // JSON backup blob + // peer_status — ICE connection quality + ConnState string `json:"conn_state,omitempty"` // pion PeerConnectionState string + CandidateType string `json:"candidate_type,omitempty"` // host | srflx | relay | unknown + RemoteAddress string `json:"remote_address,omitempty"` // remote IP:port of active candidate pair }