Skip to main content

rtmp_rs/session/
context.rs

1//! Handler context
2//!
3//! Context passed to handler callbacks containing session information
4//! and methods to interact with the connection.
5
6use std::net::SocketAddr;
7use std::sync::Arc;
8
9use crate::protocol::enhanced::EnhancedCapabilities;
10use crate::protocol::message::ConnectParams;
11use crate::protocol::quirks::EncoderType;
12use crate::stats::SessionStats;
13
14/// Context passed to RtmpHandler callbacks
15///
16/// Provides read-only access to session information. For operations
17/// that modify state, use the return values from handler methods.
18#[derive(Debug, Clone)]
19pub struct SessionContext {
20    /// Unique session ID
21    pub session_id: u64,
22
23    /// Remote peer address
24    pub peer_addr: SocketAddr,
25
26    /// Application name (from connect)
27    pub app: String,
28
29    /// Detected encoder type
30    pub encoder_type: EncoderType,
31
32    /// Connect parameters (if available)
33    pub connect_params: Option<Arc<ConnectParams>>,
34
35    /// Negotiated E-RTMP capabilities (if E-RTMP is active)
36    pub enhanced_capabilities: Option<EnhancedCapabilities>,
37
38    /// Current session statistics
39    pub stats: SessionStats,
40}
41
42impl SessionContext {
43    /// Create a new context
44    pub fn new(session_id: u64, peer_addr: SocketAddr) -> Self {
45        Self {
46            session_id,
47            peer_addr,
48            app: String::new(),
49            encoder_type: EncoderType::Unknown,
50            connect_params: None,
51            enhanced_capabilities: None,
52            stats: SessionStats::default(),
53        }
54    }
55
56    /// Update with connect parameters
57    pub fn with_connect(&mut self, params: ConnectParams, encoder_type: EncoderType) {
58        self.app = params.app.clone();
59        self.encoder_type = encoder_type;
60        self.connect_params = Some(Arc::new(params));
61    }
62
63    /// Set negotiated E-RTMP capabilities
64    pub fn with_enhanced_capabilities(&mut self, caps: EnhancedCapabilities) {
65        if caps.enabled {
66            self.enhanced_capabilities = Some(caps);
67        }
68    }
69
70    /// Check if E-RTMP is active for this session
71    pub fn is_enhanced_rtmp(&self) -> bool {
72        self.enhanced_capabilities
73            .as_ref()
74            .map(|c| c.enabled)
75            .unwrap_or(false)
76    }
77
78    /// Get the TC URL if available
79    pub fn tc_url(&self) -> Option<&str> {
80        self.connect_params
81            .as_ref()
82            .and_then(|p| p.tc_url.as_deref())
83    }
84
85    /// Get the page URL if available
86    pub fn page_url(&self) -> Option<&str> {
87        self.connect_params
88            .as_ref()
89            .and_then(|p| p.page_url.as_deref())
90    }
91
92    /// Get the flash version string if available
93    pub fn flash_ver(&self) -> Option<&str> {
94        self.connect_params
95            .as_ref()
96            .and_then(|p| p.flash_ver.as_deref())
97    }
98}
99
100/// Stream context passed to media callbacks
101#[derive(Debug, Clone)]
102pub struct StreamContext {
103    /// Parent session context
104    pub session: SessionContext,
105
106    /// Message stream ID
107    pub stream_id: u32,
108
109    /// Stream key
110    pub stream_key: String,
111
112    /// Whether this is a publishing or playing stream
113    pub is_publishing: bool,
114}
115
116impl StreamContext {
117    /// Create a new stream context
118    pub fn new(
119        session: SessionContext,
120        stream_id: u32,
121        stream_key: String,
122        is_publishing: bool,
123    ) -> Self {
124        Self {
125            session,
126            stream_id,
127            stream_key,
128            is_publishing,
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use std::net::{IpAddr, Ipv4Addr};
137
138    fn make_test_addr() -> SocketAddr {
139        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 54321)
140    }
141
142    #[test]
143    fn test_session_context_new() {
144        let addr = make_test_addr();
145        let ctx = SessionContext::new(42, addr);
146
147        assert_eq!(ctx.session_id, 42);
148        assert_eq!(ctx.peer_addr, addr);
149        assert_eq!(ctx.app, "");
150        assert_eq!(ctx.encoder_type, EncoderType::Unknown);
151        assert!(ctx.connect_params.is_none());
152    }
153
154    #[test]
155    fn test_session_context_with_connect() {
156        let addr = make_test_addr();
157        let mut ctx = SessionContext::new(1, addr);
158
159        let mut params = ConnectParams::default();
160        params.app = "live".to_string();
161        params.tc_url = Some("rtmp://localhost/live".to_string());
162        params.flash_ver = Some("FMLE/3.0".to_string());
163        params.page_url = Some("http://example.com".to_string());
164
165        ctx.with_connect(params, EncoderType::Obs);
166
167        assert_eq!(ctx.app, "live");
168        assert_eq!(ctx.encoder_type, EncoderType::Obs);
169        assert!(ctx.connect_params.is_some());
170    }
171
172    #[test]
173    fn test_session_context_tc_url() {
174        let addr = make_test_addr();
175        let mut ctx = SessionContext::new(1, addr);
176
177        // Before connect, tc_url should be None
178        assert!(ctx.tc_url().is_none());
179
180        // After connect with tc_url
181        let mut params = ConnectParams::default();
182        params.tc_url = Some("rtmp://server/app".to_string());
183        ctx.with_connect(params, EncoderType::Unknown);
184
185        assert_eq!(ctx.tc_url(), Some("rtmp://server/app"));
186    }
187
188    #[test]
189    fn test_session_context_page_url() {
190        let addr = make_test_addr();
191        let mut ctx = SessionContext::new(1, addr);
192
193        // Before connect
194        assert!(ctx.page_url().is_none());
195
196        // After connect with page_url
197        let mut params = ConnectParams::default();
198        params.page_url = Some("http://twitch.tv".to_string());
199        ctx.with_connect(params, EncoderType::Unknown);
200
201        assert_eq!(ctx.page_url(), Some("http://twitch.tv"));
202    }
203
204    #[test]
205    fn test_session_context_flash_ver() {
206        let addr = make_test_addr();
207        let mut ctx = SessionContext::new(1, addr);
208
209        // Before connect
210        assert!(ctx.flash_ver().is_none());
211
212        // After connect with flash_ver
213        let mut params = ConnectParams::default();
214        params.flash_ver = Some("OBS-Studio/29.1.3".to_string());
215        ctx.with_connect(params, EncoderType::Obs);
216
217        assert_eq!(ctx.flash_ver(), Some("OBS-Studio/29.1.3"));
218    }
219
220    #[test]
221    fn test_session_context_no_optional_params() {
222        let addr = make_test_addr();
223        let mut ctx = SessionContext::new(1, addr);
224
225        // Connect with minimal params (no tc_url, page_url, flash_ver)
226        let params = ConnectParams::default();
227        ctx.with_connect(params, EncoderType::Ffmpeg);
228
229        assert!(ctx.tc_url().is_none());
230        assert!(ctx.page_url().is_none());
231        assert!(ctx.flash_ver().is_none());
232    }
233
234    #[test]
235    fn test_stream_context_new() {
236        let addr = make_test_addr();
237        let session_ctx = SessionContext::new(1, addr);
238
239        let stream_ctx =
240            StreamContext::new(session_ctx.clone(), 5, "my_stream_key".to_string(), true);
241
242        assert_eq!(stream_ctx.stream_id, 5);
243        assert_eq!(stream_ctx.stream_key, "my_stream_key");
244        assert!(stream_ctx.is_publishing);
245        assert_eq!(stream_ctx.session.session_id, 1);
246    }
247
248    #[test]
249    fn test_stream_context_playing() {
250        let addr = make_test_addr();
251        let session_ctx = SessionContext::new(2, addr);
252
253        let stream_ctx = StreamContext::new(session_ctx, 10, "viewer_stream".to_string(), false);
254
255        assert_eq!(stream_ctx.stream_id, 10);
256        assert_eq!(stream_ctx.stream_key, "viewer_stream");
257        assert!(!stream_ctx.is_publishing);
258    }
259
260    #[test]
261    fn test_session_context_clone() {
262        let addr = make_test_addr();
263        let mut ctx = SessionContext::new(1, addr);
264
265        let mut params = ConnectParams::default();
266        params.app = "test".to_string();
267        ctx.with_connect(params, EncoderType::Wirecast);
268
269        let cloned = ctx.clone();
270
271        assert_eq!(cloned.session_id, ctx.session_id);
272        assert_eq!(cloned.app, ctx.app);
273        assert_eq!(cloned.encoder_type, ctx.encoder_type);
274    }
275}