use serde::{Deserialize, Serialize};
pub const P2P_NAMESPACE: &str = "/clasp/p2p";
pub const P2P_SIGNAL_PREFIX: &str = "/clasp/p2p/signal/";
pub const P2P_ANNOUNCE: &str = "/clasp/p2p/announce";
pub const DEFAULT_CONNECTION_TIMEOUT_SECS: u64 = 30;
pub const DEFAULT_MAX_RETRIES: u32 = 3;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum P2PSignal {
Offer {
from: String,
sdp: String,
correlation_id: String,
},
Answer {
from: String,
sdp: String,
correlation_id: String,
},
IceCandidate {
from: String,
candidate: String,
correlation_id: String,
},
Connected {
from: String,
correlation_id: String,
},
Disconnected {
from: String,
correlation_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
}
impl P2PSignal {
pub fn from_session(&self) -> &str {
match self {
P2PSignal::Offer { from, .. } => from,
P2PSignal::Answer { from, .. } => from,
P2PSignal::IceCandidate { from, .. } => from,
P2PSignal::Connected { from, .. } => from,
P2PSignal::Disconnected { from, .. } => from,
}
}
pub fn correlation_id(&self) -> &str {
match self {
P2PSignal::Offer { correlation_id, .. } => correlation_id,
P2PSignal::Answer { correlation_id, .. } => correlation_id,
P2PSignal::IceCandidate { correlation_id, .. } => correlation_id,
P2PSignal::Connected { correlation_id, .. } => correlation_id,
P2PSignal::Disconnected { correlation_id, .. } => correlation_id,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct P2PAnnounce {
pub session_id: String,
pub p2p_capable: bool,
#[serde(default)]
pub features: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct P2PConfig {
pub ice_servers: Vec<String>,
pub turn_servers: Vec<TurnServer>,
pub connection_timeout_secs: u64,
pub max_retries: u32,
pub auto_fallback: bool,
}
impl Default for P2PConfig {
fn default() -> Self {
Self {
ice_servers: vec![
"stun:stun.l.google.com:19302".to_string(),
"stun:stun1.l.google.com:19302".to_string(),
],
turn_servers: Vec::new(),
connection_timeout_secs: DEFAULT_CONNECTION_TIMEOUT_SECS,
max_retries: DEFAULT_MAX_RETRIES,
auto_fallback: true,
}
}
}
#[derive(Debug, Clone)]
pub struct TurnServer {
pub url: String,
pub username: String,
pub credential: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum P2PConnectionState {
Disconnected,
Connecting,
GatheringCandidates,
Connected,
Failed,
Closed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RoutingMode {
ServerOnly,
P2POnly,
#[default]
PreferP2P,
}
pub fn is_p2p_address(address: &str) -> bool {
address.starts_with(P2P_NAMESPACE)
}
pub fn is_p2p_signal_address(address: &str) -> bool {
address.starts_with(P2P_SIGNAL_PREFIX)
}
pub fn extract_target_session(address: &str) -> Option<&str> {
if let Some(target) = address.strip_prefix(P2P_SIGNAL_PREFIX) {
if !target.is_empty() && !target.contains('/') {
return Some(target);
}
}
None
}
pub fn signal_address(target_session_id: &str) -> String {
format!("{}{}", P2P_SIGNAL_PREFIX, target_session_id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_p2p_signal_serialization() {
let offer = P2PSignal::Offer {
from: "session-123".to_string(),
sdp: "v=0\r\n...".to_string(),
correlation_id: "conn-456".to_string(),
};
let json = serde_json::to_string(&offer).unwrap();
assert!(json.contains("\"type\":\"offer\""));
assert!(json.contains("\"from\":\"session-123\""));
let parsed: P2PSignal = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, offer);
}
#[test]
fn test_is_p2p_address() {
assert!(is_p2p_address("/clasp/p2p/signal/abc"));
assert!(is_p2p_address("/clasp/p2p/announce"));
assert!(!is_p2p_address("/lumen/scene/0/opacity"));
assert!(!is_p2p_address("/clasp/other"));
}
#[test]
fn test_extract_target_session() {
assert_eq!(
extract_target_session("/clasp/p2p/signal/session-123"),
Some("session-123")
);
assert_eq!(extract_target_session("/clasp/p2p/signal/"), None);
assert_eq!(extract_target_session("/clasp/p2p/signal/a/b"), None);
assert_eq!(extract_target_session("/other/path"), None);
}
#[test]
fn test_signal_address() {
assert_eq!(
signal_address("session-123"),
"/clasp/p2p/signal/session-123"
);
}
#[test]
fn test_p2p_announce_serialization() {
let announce = P2PAnnounce {
session_id: "session-123".to_string(),
p2p_capable: true,
features: vec!["webrtc".to_string(), "reliable".to_string()],
};
let json = serde_json::to_string(&announce).unwrap();
let parsed: P2PAnnounce = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, announce);
}
}