Skip to main content

rtmp_rs/protocol/
enhanced.rs

1//! Enhanced RTMP (E-RTMP) capability types
2//!
3//! This module defines the capability flags and structures used for
4//! E-RTMP capability negotiation during the connect handshake.
5//!
6//! Reference: E-RTMP v2 specification - "Enhancing NetConnection connect Command"
7
8use std::collections::HashMap;
9
10use crate::media::fourcc::{AudioFourCc, VideoFourCc};
11
12/// Extended capabilities bitmask (capsEx field in connect command).
13///
14/// These flags indicate support for various E-RTMP features.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub struct CapsEx(u32);
17
18impl CapsEx {
19    /// Support for NetConnection.Connect.ReconnectRequest
20    pub const RECONNECT: u32 = 0x01;
21    /// Support for multitrack audio/video
22    pub const MULTITRACK: u32 = 0x02;
23    /// Support for ModEx signal parsing
24    pub const MODEX: u32 = 0x04;
25    /// Support for nanosecond timestamp offsets
26    pub const TIMESTAMP_NANO_OFFSET: u32 = 0x08;
27
28    /// Create empty capabilities.
29    pub const fn empty() -> Self {
30        Self(0)
31    }
32
33    /// Create from raw u32 value.
34    pub const fn from_bits(bits: u32) -> Self {
35        Self(bits)
36    }
37
38    /// Get raw bits.
39    pub const fn bits(&self) -> u32 {
40        self.0
41    }
42
43    /// Check if a capability is set.
44    pub const fn contains(&self, flag: u32) -> bool {
45        (self.0 & flag) != 0
46    }
47
48    /// Set a capability flag.
49    pub fn insert(&mut self, flag: u32) {
50        self.0 |= flag;
51    }
52
53    /// Remove a capability flag.
54    pub fn remove(&mut self, flag: u32) {
55        self.0 &= !flag;
56    }
57
58    /// Compute intersection of two capability sets.
59    pub const fn intersection(&self, other: &Self) -> Self {
60        Self(self.0 & other.0)
61    }
62
63    /// Check if reconnect is supported.
64    pub const fn supports_reconnect(&self) -> bool {
65        self.contains(Self::RECONNECT)
66    }
67
68    /// Check if multitrack is supported.
69    pub const fn supports_multitrack(&self) -> bool {
70        self.contains(Self::MULTITRACK)
71    }
72
73    /// Check if ModEx signal parsing is supported.
74    pub const fn supports_modex(&self) -> bool {
75        self.contains(Self::MODEX)
76    }
77
78    /// Check if nanosecond timestamp offset is supported.
79    pub const fn supports_timestamp_nano_offset(&self) -> bool {
80        self.contains(Self::TIMESTAMP_NANO_OFFSET)
81    }
82}
83
84/// Codec capability flags for FOURCC info maps.
85///
86/// These flags indicate what operations a peer can perform with a codec.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
88pub struct FourCcCapability(u32);
89
90impl FourCcCapability {
91    /// Can decode this codec
92    pub const CAN_DECODE: u32 = 0x01;
93    /// Can encode this codec
94    pub const CAN_ENCODE: u32 = 0x02;
95    /// Can forward/relay this codec without transcoding
96    pub const CAN_FORWARD: u32 = 0x04;
97
98    /// Create empty capability.
99    pub const fn empty() -> Self {
100        Self(0)
101    }
102
103    /// Create from raw u32 value.
104    pub const fn from_bits(bits: u32) -> Self {
105        Self(bits)
106    }
107
108    /// Get raw bits.
109    pub const fn bits(&self) -> u32 {
110        self.0
111    }
112
113    /// Create capability indicating decode support.
114    pub const fn decode() -> Self {
115        Self(Self::CAN_DECODE)
116    }
117
118    /// Create capability indicating encode support.
119    pub const fn encode() -> Self {
120        Self(Self::CAN_ENCODE)
121    }
122
123    /// Create capability indicating forward/relay support.
124    pub const fn forward() -> Self {
125        Self(Self::CAN_FORWARD)
126    }
127
128    /// Create capability indicating full support (decode + encode + forward).
129    pub const fn full() -> Self {
130        Self(Self::CAN_DECODE | Self::CAN_ENCODE | Self::CAN_FORWARD)
131    }
132
133    /// Check if decode is supported.
134    pub const fn can_decode(&self) -> bool {
135        (self.0 & Self::CAN_DECODE) != 0
136    }
137
138    /// Check if encode is supported.
139    pub const fn can_encode(&self) -> bool {
140        (self.0 & Self::CAN_ENCODE) != 0
141    }
142
143    /// Check if forward is supported.
144    pub const fn can_forward(&self) -> bool {
145        (self.0 & Self::CAN_FORWARD) != 0
146    }
147}
148
149/// Video function flags (videoFunction field in connect command).
150///
151/// These flags indicate support for specific video features.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
153pub struct VideoFunctionFlags(u32);
154
155impl VideoFunctionFlags {
156    /// Client can perform frame-accurate seeks
157    pub const CLIENT_SEEK: u32 = 1;
158
159    /// Create empty flags.
160    pub const fn empty() -> Self {
161        Self(0)
162    }
163
164    /// Create from raw u32 value.
165    pub const fn from_bits(bits: u32) -> Self {
166        Self(bits)
167    }
168
169    /// Get raw bits.
170    pub const fn bits(&self) -> u32 {
171        self.0
172    }
173
174    /// Check if client seek is supported.
175    pub const fn supports_client_seek(&self) -> bool {
176        (self.0 & Self::CLIENT_SEEK) != 0
177    }
178}
179
180/// Negotiated E-RTMP capabilities for a session.
181///
182/// This structure holds the result of capability negotiation between
183/// client and server during the connect handshake.
184#[derive(Debug, Clone, Default)]
185pub struct EnhancedCapabilities {
186    /// Whether E-RTMP mode is enabled for this session.
187    pub enabled: bool,
188
189    /// Extended capabilities flags (intersection of client and server).
190    pub caps_ex: CapsEx,
191
192    /// Supported video codecs with their capabilities.
193    pub video_codecs: HashMap<VideoFourCc, FourCcCapability>,
194
195    /// Supported audio codecs with their capabilities.
196    pub audio_codecs: HashMap<AudioFourCc, FourCcCapability>,
197
198    /// Video function flags.
199    pub video_function: VideoFunctionFlags,
200}
201
202impl EnhancedCapabilities {
203    /// Create new empty capabilities (E-RTMP disabled).
204    pub fn new() -> Self {
205        Self::default()
206    }
207
208    /// Create capabilities with E-RTMP enabled and default codec support.
209    pub fn with_defaults() -> Self {
210        let mut caps = Self {
211            enabled: true,
212            caps_ex: CapsEx::from_bits(CapsEx::MODEX),
213            video_codecs: HashMap::new(),
214            audio_codecs: HashMap::new(),
215            video_function: VideoFunctionFlags::empty(),
216        };
217
218        // Default video codec support (forward-only for relay servers)
219        caps.video_codecs
220            .insert(VideoFourCc::Avc, FourCcCapability::forward());
221        caps.video_codecs
222            .insert(VideoFourCc::Hevc, FourCcCapability::forward());
223        caps.video_codecs
224            .insert(VideoFourCc::Av1, FourCcCapability::forward());
225        caps.video_codecs
226            .insert(VideoFourCc::Vp9, FourCcCapability::forward());
227
228        // Default audio codec support
229        caps.audio_codecs
230            .insert(AudioFourCc::Aac, FourCcCapability::forward());
231        caps.audio_codecs
232            .insert(AudioFourCc::Opus, FourCcCapability::forward());
233
234        caps
235    }
236
237    /// Check if a video codec is supported.
238    pub fn supports_video_codec(&self, codec: VideoFourCc) -> bool {
239        self.video_codecs.contains_key(&codec)
240    }
241
242    /// Check if an audio codec is supported.
243    pub fn supports_audio_codec(&self, codec: AudioFourCc) -> bool {
244        self.audio_codecs.contains_key(&codec)
245    }
246
247    /// Get capability for a video codec.
248    pub fn video_codec_capability(&self, codec: VideoFourCc) -> Option<FourCcCapability> {
249        self.video_codecs.get(&codec).copied()
250    }
251
252    /// Get capability for an audio codec.
253    pub fn audio_codec_capability(&self, codec: AudioFourCc) -> Option<FourCcCapability> {
254        self.audio_codecs.get(&codec).copied()
255    }
256
257    /// Check if multitrack is supported.
258    pub fn supports_multitrack(&self) -> bool {
259        self.enabled && self.caps_ex.supports_multitrack()
260    }
261
262    /// Check if reconnect is supported.
263    pub fn supports_reconnect(&self) -> bool {
264        self.enabled && self.caps_ex.supports_reconnect()
265    }
266
267    /// Compute intersection with another capability set.
268    ///
269    /// Used to negotiate common capabilities between client and server.
270    pub fn intersect(&self, other: &Self) -> Self {
271        if !self.enabled || !other.enabled {
272            return Self::new();
273        }
274
275        let mut result = Self {
276            enabled: true,
277            caps_ex: self.caps_ex.intersection(&other.caps_ex),
278            video_codecs: HashMap::new(),
279            audio_codecs: HashMap::new(),
280            video_function: VideoFunctionFlags::from_bits(
281                self.video_function.bits() & other.video_function.bits(),
282            ),
283        };
284
285        // Intersect video codecs
286        for (codec, self_cap) in &self.video_codecs {
287            if let Some(other_cap) = other.video_codecs.get(codec) {
288                let common = FourCcCapability::from_bits(self_cap.bits() & other_cap.bits());
289                if common.bits() != 0 {
290                    result.video_codecs.insert(*codec, common);
291                }
292            }
293        }
294
295        // Intersect audio codecs
296        for (codec, self_cap) in &self.audio_codecs {
297            if let Some(other_cap) = other.audio_codecs.get(codec) {
298                let common = FourCcCapability::from_bits(self_cap.bits() & other_cap.bits());
299                if common.bits() != 0 {
300                    result.audio_codecs.insert(*codec, common);
301                }
302            }
303        }
304
305        result
306    }
307}
308
309/// E-RTMP mode configuration.
310///
311/// Controls how the server/client handles E-RTMP capability negotiation.
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
313pub enum EnhancedRtmpMode {
314    /// Automatically negotiate E-RTMP if peer supports it (default).
315    #[default]
316    Auto,
317
318    /// Use legacy RTMP only, even if peer supports E-RTMP.
319    LegacyOnly,
320
321    /// Require E-RTMP; reject connections from legacy peers.
322    EnhancedOnly,
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_caps_ex_empty() {
331        let caps = CapsEx::empty();
332        assert_eq!(caps.bits(), 0);
333        assert!(!caps.supports_reconnect());
334        assert!(!caps.supports_multitrack());
335        assert!(!caps.supports_modex());
336        assert!(!caps.supports_timestamp_nano_offset());
337    }
338
339    #[test]
340    fn test_caps_ex_flags() {
341        let caps = CapsEx::from_bits(CapsEx::RECONNECT | CapsEx::MULTITRACK);
342        assert!(caps.supports_reconnect());
343        assert!(caps.supports_multitrack());
344        assert!(!caps.supports_modex());
345        assert!(!caps.supports_timestamp_nano_offset());
346    }
347
348    #[test]
349    fn test_caps_ex_insert_remove() {
350        let mut caps = CapsEx::empty();
351
352        caps.insert(CapsEx::RECONNECT);
353        assert!(caps.supports_reconnect());
354
355        caps.insert(CapsEx::MODEX);
356        assert!(caps.supports_modex());
357
358        caps.remove(CapsEx::RECONNECT);
359        assert!(!caps.supports_reconnect());
360        assert!(caps.supports_modex());
361    }
362
363    #[test]
364    fn test_caps_ex_intersection() {
365        let client = CapsEx::from_bits(CapsEx::RECONNECT | CapsEx::MULTITRACK | CapsEx::MODEX);
366        let server = CapsEx::from_bits(CapsEx::MULTITRACK | CapsEx::MODEX);
367
368        let common = client.intersection(&server);
369        assert!(!common.supports_reconnect());
370        assert!(common.supports_multitrack());
371        assert!(common.supports_modex());
372    }
373
374    #[test]
375    fn test_fourcc_capability_flags() {
376        let cap = FourCcCapability::full();
377        assert!(cap.can_decode());
378        assert!(cap.can_encode());
379        assert!(cap.can_forward());
380
381        let forward_only = FourCcCapability::forward();
382        assert!(!forward_only.can_decode());
383        assert!(!forward_only.can_encode());
384        assert!(forward_only.can_forward());
385    }
386
387    #[test]
388    fn test_fourcc_capability_from_bits() {
389        let cap = FourCcCapability::from_bits(
390            FourCcCapability::CAN_DECODE | FourCcCapability::CAN_FORWARD,
391        );
392        assert!(cap.can_decode());
393        assert!(!cap.can_encode());
394        assert!(cap.can_forward());
395    }
396
397    #[test]
398    fn test_enhanced_capabilities_default() {
399        let caps = EnhancedCapabilities::new();
400        assert!(!caps.enabled);
401        assert!(caps.video_codecs.is_empty());
402        assert!(caps.audio_codecs.is_empty());
403    }
404
405    #[test]
406    fn test_enhanced_capabilities_with_defaults() {
407        let caps = EnhancedCapabilities::with_defaults();
408        assert!(caps.enabled);
409        assert!(caps.supports_video_codec(VideoFourCc::Avc));
410        assert!(caps.supports_video_codec(VideoFourCc::Hevc));
411        assert!(caps.supports_video_codec(VideoFourCc::Av1));
412        assert!(caps.supports_audio_codec(AudioFourCc::Aac));
413        assert!(caps.supports_audio_codec(AudioFourCc::Opus));
414
415        // VP8 is also in defaults
416        assert!(caps.supports_video_codec(VideoFourCc::Vp9));
417    }
418
419    #[test]
420    fn test_enhanced_capabilities_codec_lookup() {
421        let caps = EnhancedCapabilities::with_defaults();
422
423        let avc_cap = caps.video_codec_capability(VideoFourCc::Avc).unwrap();
424        assert!(avc_cap.can_forward());
425
426        let vp8_cap = caps.video_codec_capability(VideoFourCc::Vp8);
427        // VP8 might not be in defaults, depending on implementation
428        assert!(vp8_cap.is_none() || vp8_cap.unwrap().can_forward());
429    }
430
431    #[test]
432    fn test_enhanced_capabilities_intersect() {
433        let mut client = EnhancedCapabilities::with_defaults();
434        client.caps_ex = CapsEx::from_bits(CapsEx::RECONNECT | CapsEx::MODEX);
435        client
436            .video_codecs
437            .insert(VideoFourCc::Avc, FourCcCapability::full());
438        client
439            .video_codecs
440            .insert(VideoFourCc::Vp8, FourCcCapability::decode());
441
442        let mut server = EnhancedCapabilities::with_defaults();
443        server.caps_ex = CapsEx::from_bits(CapsEx::MODEX);
444        server
445            .video_codecs
446            .insert(VideoFourCc::Avc, FourCcCapability::forward());
447        // Server doesn't support VP8
448
449        let common = client.intersect(&server);
450        assert!(common.enabled);
451        assert!(!common.caps_ex.supports_reconnect()); // Client only
452        assert!(common.caps_ex.supports_modex()); // Both
453
454        // AVC: intersection of full and forward = forward
455        let avc_cap = common.video_codec_capability(VideoFourCc::Avc).unwrap();
456        assert!(avc_cap.can_forward());
457        assert!(!avc_cap.can_encode()); // Server can't encode
458
459        // VP8: not in common (server doesn't support)
460        assert!(!common.supports_video_codec(VideoFourCc::Vp8));
461    }
462
463    #[test]
464    fn test_enhanced_capabilities_intersect_disabled() {
465        let client = EnhancedCapabilities::with_defaults();
466        let server = EnhancedCapabilities::new(); // disabled
467
468        let common = client.intersect(&server);
469        assert!(!common.enabled);
470    }
471
472    #[test]
473    fn test_enhanced_rtmp_mode_default() {
474        let mode = EnhancedRtmpMode::default();
475        assert_eq!(mode, EnhancedRtmpMode::Auto);
476    }
477
478    #[test]
479    fn test_video_function_flags() {
480        let flags = VideoFunctionFlags::from_bits(VideoFunctionFlags::CLIENT_SEEK);
481        assert!(flags.supports_client_seek());
482
483        let empty = VideoFunctionFlags::empty();
484        assert!(!empty.supports_client_seek());
485    }
486
487    #[test]
488    fn test_multitrack_support() {
489        let mut caps = EnhancedCapabilities::with_defaults();
490        assert!(!caps.supports_multitrack()); // Not enabled by default
491
492        caps.caps_ex.insert(CapsEx::MULTITRACK);
493        assert!(caps.supports_multitrack());
494
495        caps.enabled = false;
496        assert!(!caps.supports_multitrack()); // Disabled overall
497    }
498
499    #[test]
500    fn test_reconnect_support() {
501        let mut caps = EnhancedCapabilities::with_defaults();
502        assert!(!caps.supports_reconnect());
503
504        caps.caps_ex.insert(CapsEx::RECONNECT);
505        assert!(caps.supports_reconnect());
506    }
507}