1use std::time::Duration;
4
5use crate::media::fourcc::{AudioFourCc, VideoFourCc};
6use crate::protocol::enhanced::{CapsEx, EnhancedRtmpMode, FourCcCapability};
7
8#[derive(Debug, Clone)]
10pub struct ClientConfig {
11 pub url: String,
13
14 pub connect_timeout: Duration,
16
17 pub read_timeout: Duration,
19
20 pub tcp_nodelay: bool,
22
23 pub flash_ver: String,
25
26 pub swf_url: Option<String>,
28
29 pub page_url: Option<String>,
31
32 pub buffer_length: u32,
34
35 pub enhanced_rtmp: EnhancedRtmpMode,
37
38 pub enhanced_capabilities: EnhancedClientCapabilities,
40}
41
42#[derive(Debug, Clone)]
46pub struct EnhancedClientCapabilities {
47 pub reconnect: bool,
49
50 pub multitrack: bool,
52
53 pub modex: bool,
55
56 pub video_codecs: Vec<(VideoFourCc, FourCcCapability)>,
58
59 pub audio_codecs: Vec<(AudioFourCc, FourCcCapability)>,
61}
62
63impl Default for EnhancedClientCapabilities {
64 fn default() -> Self {
65 Self {
66 reconnect: false,
67 multitrack: false,
68 modex: true,
69 video_codecs: vec![
70 (VideoFourCc::Avc, FourCcCapability::decode()),
71 (VideoFourCc::Hevc, FourCcCapability::decode()),
72 (VideoFourCc::Av1, FourCcCapability::decode()),
73 ],
74 audio_codecs: vec![
75 (AudioFourCc::Aac, FourCcCapability::decode()),
76 (AudioFourCc::Opus, FourCcCapability::decode()),
77 ],
78 }
79 }
80}
81
82impl EnhancedClientCapabilities {
83 pub fn minimal() -> Self {
85 Self {
86 reconnect: false,
87 multitrack: false,
88 modex: true,
89 video_codecs: vec![],
90 audio_codecs: vec![],
91 }
92 }
93
94 pub fn with_video_codec(mut self, codec: VideoFourCc, cap: FourCcCapability) -> Self {
96 self.video_codecs.push((codec, cap));
97 self
98 }
99
100 pub fn with_audio_codec(mut self, codec: AudioFourCc, cap: FourCcCapability) -> Self {
102 self.audio_codecs.push((codec, cap));
103 self
104 }
105
106 pub fn with_reconnect(mut self) -> Self {
108 self.reconnect = true;
109 self
110 }
111
112 pub fn with_multitrack(mut self) -> Self {
114 self.multitrack = true;
115 self
116 }
117
118 pub fn to_caps_ex(&self) -> CapsEx {
120 let mut caps = CapsEx::empty();
121 if self.reconnect {
122 caps.insert(CapsEx::RECONNECT);
123 }
124 if self.multitrack {
125 caps.insert(CapsEx::MULTITRACK);
126 }
127 if self.modex {
128 caps.insert(CapsEx::MODEX);
129 }
130 caps
131 }
132
133 pub fn to_enhanced_capabilities(&self) -> crate::protocol::enhanced::EnhancedCapabilities {
135 use crate::protocol::enhanced::EnhancedCapabilities;
136
137 let mut caps = EnhancedCapabilities {
138 enabled: true,
139 caps_ex: self.to_caps_ex(),
140 video_codecs: std::collections::HashMap::new(),
141 audio_codecs: std::collections::HashMap::new(),
142 video_function: crate::protocol::enhanced::VideoFunctionFlags::empty(),
143 };
144
145 for (codec, capability) in &self.video_codecs {
146 caps.video_codecs.insert(*codec, *capability);
147 }
148
149 for (codec, capability) in &self.audio_codecs {
150 caps.audio_codecs.insert(*codec, *capability);
151 }
152
153 caps
154 }
155}
156
157impl Default for ClientConfig {
158 fn default() -> Self {
159 Self {
160 url: String::new(),
161 connect_timeout: Duration::from_secs(10),
162 read_timeout: Duration::from_secs(30),
163 tcp_nodelay: true,
164 flash_ver: "LNX 9,0,124,2".to_string(),
165 swf_url: None,
166 page_url: None,
167 buffer_length: 1000,
168 enhanced_rtmp: EnhancedRtmpMode::Auto,
169 enhanced_capabilities: EnhancedClientCapabilities::default(),
170 }
171 }
172}
173
174impl ClientConfig {
175 pub fn new(url: impl Into<String>) -> Self {
177 Self {
178 url: url.into(),
179 ..Default::default()
180 }
181 }
182
183 pub fn enhanced_rtmp(mut self, mode: EnhancedRtmpMode) -> Self {
189 self.enhanced_rtmp = mode;
190 self
191 }
192
193 pub fn enhanced_capabilities(mut self, caps: EnhancedClientCapabilities) -> Self {
195 self.enhanced_capabilities = caps;
196 self
197 }
198
199 pub fn parse_url(&self) -> Option<ParsedUrl> {
201 let url = self.url.strip_prefix("rtmp://")?;
203
204 let (host_port, path) = url.split_once('/')?;
205 let (host, port) = if let Some((h, p)) = host_port.split_once(':') {
206 (h.to_string(), p.parse().ok()?)
207 } else {
208 (host_port.to_string(), 1935)
209 };
210
211 let (app, stream_key) = if let Some((a, s)) = path.split_once('/') {
212 (a.to_string(), Some(s.to_string()))
213 } else {
214 (path.to_string(), None)
215 };
216
217 Some(ParsedUrl {
218 host,
219 port,
220 app,
221 stream_key,
222 })
223 }
224}
225
226#[derive(Debug, Clone)]
228pub struct ParsedUrl {
229 pub host: String,
230 pub port: u16,
231 pub app: String,
232 pub stream_key: Option<String>,
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_url_parsing() {
241 let config = ClientConfig::new("rtmp://localhost/live/test");
242 let parsed = config.parse_url().unwrap();
243 assert_eq!(parsed.host, "localhost");
244 assert_eq!(parsed.port, 1935);
245 assert_eq!(parsed.app, "live");
246 assert_eq!(parsed.stream_key, Some("test".into()));
247
248 let config = ClientConfig::new("rtmp://example.com:1936/app");
249 let parsed = config.parse_url().unwrap();
250 assert_eq!(parsed.host, "example.com");
251 assert_eq!(parsed.port, 1936);
252 assert_eq!(parsed.app, "app");
253 assert_eq!(parsed.stream_key, None);
254 }
255
256 #[test]
257 fn test_default_config_enhanced_rtmp() {
258 let config = ClientConfig::default();
259 assert_eq!(config.enhanced_rtmp, EnhancedRtmpMode::Auto);
260 }
261
262 #[test]
263 fn test_enhanced_rtmp_mode() {
264 let config = ClientConfig::new("rtmp://localhost/live/test")
265 .enhanced_rtmp(EnhancedRtmpMode::EnhancedOnly);
266 assert_eq!(config.enhanced_rtmp, EnhancedRtmpMode::EnhancedOnly);
267
268 let config = ClientConfig::new("rtmp://localhost/live/test")
269 .enhanced_rtmp(EnhancedRtmpMode::LegacyOnly);
270 assert_eq!(config.enhanced_rtmp, EnhancedRtmpMode::LegacyOnly);
271 }
272
273 #[test]
274 fn test_enhanced_client_capabilities_default() {
275 let caps = EnhancedClientCapabilities::default();
276
277 assert!(!caps.reconnect);
278 assert!(!caps.multitrack);
279 assert!(caps.modex);
280 assert!(!caps.video_codecs.is_empty());
281 assert!(!caps.audio_codecs.is_empty());
282
283 assert!(caps
285 .video_codecs
286 .iter()
287 .any(|(c, _)| *c == VideoFourCc::Avc));
288 assert!(caps
289 .video_codecs
290 .iter()
291 .any(|(c, _)| *c == VideoFourCc::Hevc));
292 }
293
294 #[test]
295 fn test_enhanced_client_capabilities_minimal() {
296 let caps = EnhancedClientCapabilities::minimal();
297
298 assert!(caps.video_codecs.is_empty());
299 assert!(caps.audio_codecs.is_empty());
300 }
301
302 #[test]
303 fn test_enhanced_client_capabilities_builder() {
304 let caps = EnhancedClientCapabilities::minimal()
305 .with_video_codec(VideoFourCc::Av1, FourCcCapability::decode())
306 .with_audio_codec(AudioFourCc::Opus, FourCcCapability::decode())
307 .with_reconnect()
308 .with_multitrack();
309
310 assert!(caps.reconnect);
311 assert!(caps.multitrack);
312 assert_eq!(caps.video_codecs.len(), 1);
313 assert_eq!(caps.audio_codecs.len(), 1);
314 }
315
316 #[test]
317 fn test_enhanced_client_capabilities_to_caps_ex() {
318 let caps = EnhancedClientCapabilities::default();
319 let caps_ex = caps.to_caps_ex();
320
321 assert!(!caps_ex.supports_reconnect());
322 assert!(!caps_ex.supports_multitrack());
323 assert!(caps_ex.supports_modex());
324
325 let caps_full = EnhancedClientCapabilities::default()
326 .with_reconnect()
327 .with_multitrack();
328 let caps_ex = caps_full.to_caps_ex();
329
330 assert!(caps_ex.supports_reconnect());
331 assert!(caps_ex.supports_multitrack());
332 }
333
334 #[test]
335 fn test_config_with_enhanced_capabilities() {
336 let caps = EnhancedClientCapabilities::minimal()
337 .with_video_codec(VideoFourCc::Hevc, FourCcCapability::decode());
338
339 let config = ClientConfig::new("rtmp://localhost/live/test")
340 .enhanced_rtmp(EnhancedRtmpMode::Auto)
341 .enhanced_capabilities(caps);
342
343 assert_eq!(config.enhanced_rtmp, EnhancedRtmpMode::Auto);
344 assert_eq!(config.enhanced_capabilities.video_codecs.len(), 1);
345 }
346}