Skip to main content

camgrab_core/rtsp/
codec.rs

1//! Codec detection and stream information
2//!
3//! This module handles codec identification from RTSP DESCRIBE responses (SDP)
4//! and provides structured information about video and audio streams.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// Video codec types supported by camgrab
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum CodecType {
13    /// H.264 / AVC
14    H264,
15    /// H.265 / HEVC
16    H265,
17    /// Motion JPEG
18    Mjpeg,
19    /// Unknown video codec
20    Unknown,
21}
22
23impl fmt::Display for CodecType {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            Self::H264 => write!(f, "H.264"),
27            Self::H265 => write!(f, "H.265"),
28            Self::Mjpeg => write!(f, "MJPEG"),
29            Self::Unknown => write!(f, "Unknown"),
30        }
31    }
32}
33
34impl CodecType {
35    /// Detects codec from MIME type or encoding name
36    pub fn from_encoding_name(name: &str) -> Self {
37        let name_lower = name.to_lowercase();
38        match name_lower.as_str() {
39            "h264" | "avc" | "h.264" => Self::H264,
40            "h265" | "hevc" | "h.265" => Self::H265,
41            "jpeg" | "mjpeg" | "motion-jpeg" => Self::Mjpeg,
42            _ => Self::Unknown,
43        }
44    }
45
46    /// Returns the file extension for this codec
47    pub fn extension(&self) -> &'static str {
48        match self {
49            Self::H264 => "h264",
50            Self::H265 => "h265",
51            Self::Mjpeg => "mjpeg",
52            Self::Unknown => "raw",
53        }
54    }
55}
56
57/// Audio codec types supported by camgrab
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "lowercase")]
60pub enum AudioCodec {
61    /// AAC audio
62    Aac,
63    /// G.711 A-law
64    Pcma,
65    /// G.711 μ-law
66    Pcmu,
67    /// Opus audio
68    Opus,
69    /// Unknown audio codec
70    Unknown,
71}
72
73impl fmt::Display for AudioCodec {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::Aac => write!(f, "AAC"),
77            Self::Pcma => write!(f, "PCMA"),
78            Self::Pcmu => write!(f, "PCMU"),
79            Self::Opus => write!(f, "Opus"),
80            Self::Unknown => write!(f, "Unknown"),
81        }
82    }
83}
84
85impl AudioCodec {
86    /// Detects audio codec from encoding name
87    pub fn from_encoding_name(name: &str) -> Self {
88        let name_lower = name.to_lowercase();
89        match name_lower.as_str() {
90            "aac" | "mpeg4-generic" => Self::Aac,
91            "pcma" => Self::Pcma,
92            "pcmu" => Self::Pcmu,
93            "opus" => Self::Opus,
94            _ => Self::Unknown,
95        }
96    }
97}
98
99/// Information about a media stream
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct StreamInfo {
102    /// Video codec
103    pub video_codec: CodecType,
104
105    /// Audio codec (if present)
106    pub audio_codec: Option<AudioCodec>,
107
108    /// Video width in pixels
109    pub width: Option<u32>,
110
111    /// Video height in pixels
112    pub height: Option<u32>,
113
114    /// Frames per second
115    pub fps: Option<f32>,
116
117    /// Audio sample rate (Hz)
118    pub sample_rate: Option<u32>,
119
120    /// Audio channels
121    pub audio_channels: Option<u8>,
122}
123
124impl Default for StreamInfo {
125    fn default() -> Self {
126        Self {
127            video_codec: CodecType::Unknown,
128            audio_codec: None,
129            width: None,
130            height: None,
131            fps: None,
132            sample_rate: None,
133            audio_channels: None,
134        }
135    }
136}
137
138impl StreamInfo {
139    /// Creates a new StreamInfo with the given video codec
140    pub fn new(video_codec: CodecType) -> Self {
141        Self {
142            video_codec,
143            ..Default::default()
144        }
145    }
146
147    /// Sets video dimensions
148    #[must_use]
149    pub fn with_dimensions(mut self, width: u32, height: u32) -> Self {
150        self.width = Some(width);
151        self.height = Some(height);
152        self
153    }
154
155    /// Sets frames per second
156    #[must_use]
157    pub fn with_fps(mut self, fps: f32) -> Self {
158        self.fps = Some(fps);
159        self
160    }
161
162    /// Sets audio codec
163    #[must_use]
164    pub fn with_audio_codec(mut self, codec: AudioCodec) -> Self {
165        self.audio_codec = Some(codec);
166        self
167    }
168
169    /// Sets audio parameters
170    #[must_use]
171    pub fn with_audio_params(mut self, sample_rate: u32, channels: u8) -> Self {
172        self.sample_rate = Some(sample_rate);
173        self.audio_channels = Some(channels);
174        self
175    }
176
177    /// Returns a human-readable description of the stream
178    pub fn description(&self) -> String {
179        let mut parts = vec![self.video_codec.to_string()];
180
181        if let (Some(w), Some(h)) = (self.width, self.height) {
182            parts.push(format!("{w}x{h}"));
183        }
184
185        if let Some(fps) = self.fps {
186            parts.push(format!("{fps:.1}fps"));
187        }
188
189        if let Some(audio) = self.audio_codec {
190            parts.push(format!("audio: {audio}"));
191        }
192
193        parts.join(", ")
194    }
195}
196
197/// Parses stream information from retina's session description
198///
199/// This function extracts codec information, dimensions, and other metadata
200/// from the RTSP session after DESCRIBE.
201///
202/// # Arguments
203///
204/// * `session` - The retina session containing stream metadata
205///
206/// # Returns
207///
208/// A `StreamInfo` struct with detected codec and stream parameters
209pub fn parse_stream_info<S: retina::client::State>(
210    session: &retina::client::Session<S>,
211) -> StreamInfo {
212    let mut info = StreamInfo::default();
213
214    // Iterate through the streams in the session
215    for (stream_id, stream) in session.streams().iter().enumerate() {
216        // Video stream detection
217        if let Some(params) = stream.parameters() {
218            // Match on the ParametersRef enum to detect codec types
219            match params {
220                retina::codec::ParametersRef::Video(video_params) => {
221                    // Extract codec information from the RFC 6381 codec string
222                    let codec_str = video_params.rfc6381_codec();
223
224                    if codec_str.starts_with("avc1") || codec_str.starts_with("avc3") {
225                        info.video_codec = CodecType::H264;
226                        tracing::debug!(
227                            stream = stream_id,
228                            codec = codec_str,
229                            "Detected H.264 stream"
230                        );
231                    } else if codec_str.starts_with("hvc1") || codec_str.starts_with("hev1") {
232                        info.video_codec = CodecType::H265;
233                        tracing::debug!(
234                            stream = stream_id,
235                            codec = codec_str,
236                            "Detected H.265 stream"
237                        );
238                    } else if codec_str.starts_with("mp4v") {
239                        info.video_codec = CodecType::Mjpeg;
240                        tracing::debug!(
241                            stream = stream_id,
242                            codec = codec_str,
243                            "Detected MJPEG stream"
244                        );
245                    }
246
247                    // Extract dimensions
248                    let (width, height) = video_params.pixel_dimensions();
249                    info.width = Some(width);
250                    info.height = Some(height);
251
252                    // Extract frame rate if available
253                    if let Some((num, denom)) = video_params.frame_rate() {
254                        if denom > 0 {
255                            info.fps = Some(num as f32 / denom as f32);
256                        }
257                    }
258                }
259                retina::codec::ParametersRef::Audio(audio_params) => {
260                    // Detect audio codec from RFC 6381 codec string
261                    if let Some(codec_str) = audio_params.rfc6381_codec() {
262                        if codec_str.starts_with("mp4a") {
263                            info.audio_codec = Some(AudioCodec::Aac);
264                        }
265                    }
266
267                    info.sample_rate = Some(audio_params.clock_rate());
268
269                    tracing::debug!(
270                        stream = stream_id,
271                        sample_rate = audio_params.clock_rate(),
272                        "Detected audio stream"
273                    );
274                }
275                retina::codec::ParametersRef::Message(_) => {
276                    // Message streams (like ONVIF metadata) - not currently handled
277                    tracing::debug!(stream = stream_id, "Detected message stream");
278                }
279            }
280        }
281    }
282
283    info
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_codec_from_encoding_name() {
292        assert_eq!(CodecType::from_encoding_name("H264"), CodecType::H264);
293        assert_eq!(CodecType::from_encoding_name("h264"), CodecType::H264);
294        assert_eq!(CodecType::from_encoding_name("AVC"), CodecType::H264);
295        assert_eq!(CodecType::from_encoding_name("H265"), CodecType::H265);
296        assert_eq!(CodecType::from_encoding_name("HEVC"), CodecType::H265);
297        assert_eq!(CodecType::from_encoding_name("MJPEG"), CodecType::Mjpeg);
298        assert_eq!(CodecType::from_encoding_name("jpeg"), CodecType::Mjpeg);
299        assert_eq!(CodecType::from_encoding_name("unknown"), CodecType::Unknown);
300    }
301
302    #[test]
303    fn test_audio_codec_from_encoding_name() {
304        assert_eq!(AudioCodec::from_encoding_name("AAC"), AudioCodec::Aac);
305        assert_eq!(AudioCodec::from_encoding_name("aac"), AudioCodec::Aac);
306        assert_eq!(
307            AudioCodec::from_encoding_name("MPEG4-GENERIC"),
308            AudioCodec::Aac
309        );
310        assert_eq!(AudioCodec::from_encoding_name("PCMA"), AudioCodec::Pcma);
311        assert_eq!(AudioCodec::from_encoding_name("PCMU"), AudioCodec::Pcmu);
312        assert_eq!(AudioCodec::from_encoding_name("Opus"), AudioCodec::Opus);
313        assert_eq!(
314            AudioCodec::from_encoding_name("unknown"),
315            AudioCodec::Unknown
316        );
317    }
318
319    #[test]
320    fn test_codec_extension() {
321        assert_eq!(CodecType::H264.extension(), "h264");
322        assert_eq!(CodecType::H265.extension(), "h265");
323        assert_eq!(CodecType::Mjpeg.extension(), "mjpeg");
324        assert_eq!(CodecType::Unknown.extension(), "raw");
325    }
326
327    #[test]
328    fn test_stream_info_builder() {
329        let info = StreamInfo::new(CodecType::H264)
330            .with_dimensions(1920, 1080)
331            .with_fps(30.0)
332            .with_audio_codec(AudioCodec::Aac)
333            .with_audio_params(48000, 2);
334
335        assert_eq!(info.video_codec, CodecType::H264);
336        assert_eq!(info.width, Some(1920));
337        assert_eq!(info.height, Some(1080));
338        assert_eq!(info.fps, Some(30.0));
339        assert_eq!(info.audio_codec, Some(AudioCodec::Aac));
340        assert_eq!(info.sample_rate, Some(48000));
341        assert_eq!(info.audio_channels, Some(2));
342    }
343
344    #[test]
345    fn test_stream_info_description() {
346        let info = StreamInfo::new(CodecType::H264)
347            .with_dimensions(1920, 1080)
348            .with_fps(30.0)
349            .with_audio_codec(AudioCodec::Aac);
350
351        let desc = info.description();
352        assert!(desc.contains("H.264"));
353        assert!(desc.contains("1920x1080"));
354        assert!(desc.contains("30.0fps"));
355        assert!(desc.contains("AAC"));
356    }
357
358    #[test]
359    fn test_codec_display() {
360        assert_eq!(format!("{}", CodecType::H264), "H.264");
361        assert_eq!(format!("{}", CodecType::H265), "H.265");
362        assert_eq!(format!("{}", CodecType::Mjpeg), "MJPEG");
363        assert_eq!(format!("{}", AudioCodec::Aac), "AAC");
364    }
365}