Skip to main content

rtmp_rs/client/
config.rs

1//! Client configuration
2
3use std::time::Duration;
4
5use crate::media::fourcc::{AudioFourCc, VideoFourCc};
6use crate::protocol::enhanced::{CapsEx, EnhancedRtmpMode, FourCcCapability};
7
8/// Client configuration
9#[derive(Debug, Clone)]
10pub struct ClientConfig {
11    /// RTMP URL to connect to (rtmp://host[:port]/app/stream)
12    pub url: String,
13
14    /// Connection timeout
15    pub connect_timeout: Duration,
16
17    /// Read timeout
18    pub read_timeout: Duration,
19
20    /// Enable TCP_NODELAY
21    pub tcp_nodelay: bool,
22
23    /// Flash version string to send
24    pub flash_ver: String,
25
26    /// SWF URL to send
27    pub swf_url: Option<String>,
28
29    /// Page URL to send
30    pub page_url: Option<String>,
31
32    /// Receive buffer size in milliseconds
33    pub buffer_length: u32,
34
35    /// Enhanced RTMP mode (Auto, LegacyOnly, or EnhancedOnly)
36    pub enhanced_rtmp: EnhancedRtmpMode,
37
38    /// Enhanced RTMP client capabilities to advertise
39    pub enhanced_capabilities: EnhancedClientCapabilities,
40}
41
42/// Client-side Enhanced RTMP capabilities.
43///
44/// Configure which E-RTMP features and codecs the client supports.
45#[derive(Debug, Clone)]
46pub struct EnhancedClientCapabilities {
47    /// Support for NetConnection.Connect.ReconnectRequest
48    pub reconnect: bool,
49
50    /// Support for multitrack audio/video streams
51    pub multitrack: bool,
52
53    /// Support for ModEx signal parsing
54    pub modex: bool,
55
56    /// Video codecs supported with their capabilities
57    pub video_codecs: Vec<(VideoFourCc, FourCcCapability)>,
58
59    /// Audio codecs supported with their capabilities
60    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    /// Create capabilities with no codec support.
84    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    /// Add a video codec with specified capability.
95    pub fn with_video_codec(mut self, codec: VideoFourCc, cap: FourCcCapability) -> Self {
96        self.video_codecs.push((codec, cap));
97        self
98    }
99
100    /// Add an audio codec with specified capability.
101    pub fn with_audio_codec(mut self, codec: AudioFourCc, cap: FourCcCapability) -> Self {
102        self.audio_codecs.push((codec, cap));
103        self
104    }
105
106    /// Enable reconnect support.
107    pub fn with_reconnect(mut self) -> Self {
108        self.reconnect = true;
109        self
110    }
111
112    /// Enable multitrack support.
113    pub fn with_multitrack(mut self) -> Self {
114        self.multitrack = true;
115        self
116    }
117
118    /// Convert to CapsEx bitmask for protocol encoding.
119    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    /// Convert to EnhancedCapabilities for negotiation.
134    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    /// Create a new config with the given URL
176    pub fn new(url: impl Into<String>) -> Self {
177        Self {
178            url: url.into(),
179            ..Default::default()
180        }
181    }
182
183    /// Set Enhanced RTMP mode.
184    ///
185    /// - `Auto`: Negotiate E-RTMP if server supports it (default)
186    /// - `LegacyOnly`: Use legacy RTMP only
187    /// - `EnhancedOnly`: Require E-RTMP, fail if server doesn't support it
188    pub fn enhanced_rtmp(mut self, mode: EnhancedRtmpMode) -> Self {
189        self.enhanced_rtmp = mode;
190        self
191    }
192
193    /// Set Enhanced RTMP client capabilities.
194    pub fn enhanced_capabilities(mut self, caps: EnhancedClientCapabilities) -> Self {
195        self.enhanced_capabilities = caps;
196        self
197    }
198
199    /// Parse URL into components
200    pub fn parse_url(&self) -> Option<ParsedUrl> {
201        // rtmp://host[:port]/app[/stream]
202        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/// Parsed RTMP URL components
227#[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        // Check default video codecs - client defaults to decode capability
284        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}