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 let Some(target) = address.strip_prefix(P2P_SIGNAL_PREFIX) {
200 if !target.is_empty() && !target.contains('/') {
201 return Some(target);
202 }
203 }
204 None
205}
206
207pub fn signal_address(target_session_id: &str) -> String {
209 format!("{}{}", P2P_SIGNAL_PREFIX, target_session_id)
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_p2p_signal_serialization() {
218 let offer = P2PSignal::Offer {
219 from: "session-123".to_string(),
220 sdp: "v=0\r\n...".to_string(),
221 correlation_id: "conn-456".to_string(),
222 };
223
224 let json = serde_json::to_string(&offer).unwrap();
225 assert!(json.contains("\"type\":\"offer\""));
226 assert!(json.contains("\"from\":\"session-123\""));
227
228 let parsed: P2PSignal = serde_json::from_str(&json).unwrap();
229 assert_eq!(parsed, offer);
230 }
231
232 #[test]
233 fn test_is_p2p_address() {
234 assert!(is_p2p_address("/clasp/p2p/signal/abc"));
235 assert!(is_p2p_address("/clasp/p2p/announce"));
236 assert!(!is_p2p_address("/lumen/scene/0/opacity"));
237 assert!(!is_p2p_address("/clasp/other"));
238 }
239
240 #[test]
241 fn test_extract_target_session() {
242 assert_eq!(
243 extract_target_session("/clasp/p2p/signal/session-123"),
244 Some("session-123")
245 );
246 assert_eq!(extract_target_session("/clasp/p2p/signal/"), None);
247 assert_eq!(extract_target_session("/clasp/p2p/signal/a/b"), None);
248 assert_eq!(extract_target_session("/other/path"), None);
249 }
250
251 #[test]
252 fn test_signal_address() {
253 assert_eq!(
254 signal_address("session-123"),
255 "/clasp/p2p/signal/session-123"
256 );
257 }
258
259 #[test]
260 fn test_p2p_announce_serialization() {
261 let announce = P2PAnnounce {
262 session_id: "session-123".to_string(),
263 p2p_capable: true,
264 features: vec!["webrtc".to_string(), "reliable".to_string()],
265 };
266
267 let json = serde_json::to_string(&announce).unwrap();
268 let parsed: P2PAnnounce = serde_json::from_str(&json).unwrap();
269 assert_eq!(parsed, announce);
270 }
271}