1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum CodecType {
13 H264,
15 H265,
17 Mjpeg,
19 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "lowercase")]
60pub enum AudioCodec {
61 Aac,
63 Pcma,
65 Pcmu,
67 Opus,
69 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct StreamInfo {
102 pub video_codec: CodecType,
104
105 pub audio_codec: Option<AudioCodec>,
107
108 pub width: Option<u32>,
110
111 pub height: Option<u32>,
113
114 pub fps: Option<f32>,
116
117 pub sample_rate: Option<u32>,
119
120 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 pub fn new(video_codec: CodecType) -> Self {
141 Self {
142 video_codec,
143 ..Default::default()
144 }
145 }
146
147 #[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 #[must_use]
157 pub fn with_fps(mut self, fps: f32) -> Self {
158 self.fps = Some(fps);
159 self
160 }
161
162 #[must_use]
164 pub fn with_audio_codec(mut self, codec: AudioCodec) -> Self {
165 self.audio_codec = Some(codec);
166 self
167 }
168
169 #[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 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
197pub fn parse_stream_info<S: retina::client::State>(
210 session: &retina::client::Session<S>,
211) -> StreamInfo {
212 let mut info = StreamInfo::default();
213
214 for (stream_id, stream) in session.streams().iter().enumerate() {
216 if let Some(params) = stream.parameters() {
218 match params {
220 retina::codec::ParametersRef::Video(video_params) => {
221 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 let (width, height) = video_params.pixel_dimensions();
249 info.width = Some(width);
250 info.height = Some(height);
251
252 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 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 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}