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