1use serde::{Deserialize, Serialize};
9
10pub const P2P_NAMESPACE: &str = "/clasp/p2p";
12
13pub const P2P_SIGNAL_PREFIX: &str = "/clasp/p2p/signal/";
16
17pub const P2P_ANNOUNCE: &str = "/clasp/p2p/announce";
19
20pub const DEFAULT_CONNECTION_TIMEOUT_SECS: u64 = 30;
22
23pub const DEFAULT_MAX_RETRIES: u32 = 3;
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31#[serde(tag = "type", rename_all = "snake_case")]
32pub enum P2PSignal {
33 Offer {
35 from: String,
37 sdp: String,
39 correlation_id: String,
41 },
42 Answer {
44 from: String,
46 sdp: String,
48 correlation_id: String,
50 },
51 IceCandidate {
53 from: String,
55 candidate: String,
57 correlation_id: String,
59 },
60 Connected {
62 from: String,
64 correlation_id: String,
66 },
67 Disconnected {
69 from: String,
71 correlation_id: String,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 reason: Option<String>,
76 },
77}
78
79impl P2PSignal {
80 pub fn from_session(&self) -> &str {
82 match self {
83 P2PSignal::Offer { from, .. } => from,
84 P2PSignal::Answer { from, .. } => from,
85 P2PSignal::IceCandidate { from, .. } => from,
86 P2PSignal::Connected { from, .. } => from,
87 P2PSignal::Disconnected { from, .. } => from,
88 }
89 }
90
91 pub fn correlation_id(&self) -> &str {
93 match self {
94 P2PSignal::Offer { correlation_id, .. } => correlation_id,
95 P2PSignal::Answer { correlation_id, .. } => correlation_id,
96 P2PSignal::IceCandidate { correlation_id, .. } => correlation_id,
97 P2PSignal::Connected { correlation_id, .. } => correlation_id,
98 P2PSignal::Disconnected { correlation_id, .. } => correlation_id,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
105pub struct P2PAnnounce {
106 pub session_id: String,
108 pub p2p_capable: bool,
110 #[serde(default)]
112 pub features: Vec<String>,
113}
114
115#[derive(Debug, Clone)]
117pub struct P2PConfig {
118 pub ice_servers: Vec<String>,
120 pub turn_servers: Vec<TurnServer>,
122 pub connection_timeout_secs: u64,
124 pub max_retries: u32,
126 pub auto_fallback: bool,
128}
129
130impl Default for P2PConfig {
131 fn default() -> Self {
132 Self {
133 ice_servers: vec![
134 "stun:stun.l.google.com:19302".to_string(),
135 "stun:stun1.l.google.com:19302".to_string(),
136 ],
137 turn_servers: Vec::new(),
138 connection_timeout_secs: DEFAULT_CONNECTION_TIMEOUT_SECS,
139 max_retries: DEFAULT_MAX_RETRIES,
140 auto_fallback: true,
141 }
142 }
143}
144
145#[derive(Debug, Clone)]
147pub struct TurnServer {
148 pub url: String,
150 pub username: String,
152 pub credential: String,
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum P2PConnectionState {
159 Disconnected,
161 Connecting,
163 GatheringCandidates,
165 Connected,
167 Failed,
169 Closed,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
175pub enum RoutingMode {
176 ServerOnly,
178 P2POnly,
180 #[default]
182 PreferP2P,
183}
184
185pub fn is_p2p_address(address: &str) -> bool {
187 address.starts_with(P2P_NAMESPACE)
188}
189
190pub fn is_p2p_signal_address(address: &str) -> bool {
192 address.starts_with(P2P_SIGNAL_PREFIX)
193}
194
195pub fn extract_target_session(address: &str) -> Option<&str> {
199 if address.starts_with(P2P_SIGNAL_PREFIX) {
200 let target = &address[P2P_SIGNAL_PREFIX.len()..];
201 if !target.is_empty() && !target.contains('/') {
202 return Some(target);
203 }
204 }
205 None
206}
207
208pub fn signal_address(target_session_id: &str) -> String {
210 format!("{}{}", P2P_SIGNAL_PREFIX, target_session_id)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_p2p_signal_serialization() {
219 let offer = P2PSignal::Offer {
220 from: "session-123".to_string(),
221 sdp: "v=0\r\n...".to_string(),
222 correlation_id: "conn-456".to_string(),
223 };
224
225 let json = serde_json::to_string(&offer).unwrap();
226 assert!(json.contains("\"type\":\"offer\""));
227 assert!(json.contains("\"from\":\"session-123\""));
228
229 let parsed: P2PSignal = serde_json::from_str(&json).unwrap();
230 assert_eq!(parsed, offer);
231 }
232
233 #[test]
234 fn test_is_p2p_address() {
235 assert!(is_p2p_address("/clasp/p2p/signal/abc"));
236 assert!(is_p2p_address("/clasp/p2p/announce"));
237 assert!(!is_p2p_address("/lumen/scene/0/opacity"));
238 assert!(!is_p2p_address("/clasp/other"));
239 }
240
241 #[test]
242 fn test_extract_target_session() {
243 assert_eq!(
244 extract_target_session("/clasp/p2p/signal/session-123"),
245 Some("session-123")
246 );
247 assert_eq!(extract_target_session("/clasp/p2p/signal/"), None);
248 assert_eq!(extract_target_session("/clasp/p2p/signal/a/b"), None);
249 assert_eq!(extract_target_session("/other/path"), None);
250 }
251
252 #[test]
253 fn test_signal_address() {
254 assert_eq!(
255 signal_address("session-123"),
256 "/clasp/p2p/signal/session-123"
257 );
258 }
259
260 #[test]
261 fn test_p2p_announce_serialization() {
262 let announce = P2PAnnounce {
263 session_id: "session-123".to_string(),
264 p2p_capable: true,
265 features: vec!["webrtc".to_string(), "reliable".to_string()],
266 };
267
268 let json = serde_json::to_string(&announce).unwrap();
269 let parsed: P2PAnnounce = serde_json::from_str(&json).unwrap();
270 assert_eq!(parsed, announce);
271 }
272}