1#![allow(dead_code)]
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum FrameType {
7 I,
9 P,
11 B,
13 SP,
15 SI,
17}
18
19impl FrameType {
20 #[must_use]
22 pub fn is_reference(&self) -> bool {
23 matches!(self, Self::I | Self::P | Self::SP | Self::SI)
24 }
25
26 #[must_use]
28 pub fn is_intra(&self) -> bool {
29 matches!(self, Self::I | Self::SI)
30 }
31
32 #[must_use]
34 pub fn is_inter(&self) -> bool {
35 matches!(self, Self::P | Self::B | Self::SP)
36 }
37
38 #[must_use]
40 pub fn tag(&self) -> &'static str {
41 match self {
42 Self::I => "I",
43 Self::P => "P",
44 Self::B => "B",
45 Self::SP => "SP",
46 Self::SI => "SI",
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct FrameStat {
54 pub index: u64,
56 pub frame_type: FrameType,
58 pub size_bytes: u64,
60 pub width: u32,
62 pub height: u32,
64 pub qp: u8,
66}
67
68impl FrameStat {
69 #[must_use]
71 pub fn new(
72 index: u64,
73 frame_type: FrameType,
74 size_bytes: u64,
75 width: u32,
76 height: u32,
77 qp: u8,
78 ) -> Self {
79 Self {
80 index,
81 frame_type,
82 size_bytes,
83 width,
84 height,
85 qp,
86 }
87 }
88
89 #[allow(clippy::cast_precision_loss)]
91 #[must_use]
92 pub fn bits_per_pixel(&self) -> f64 {
93 let pixels = u64::from(self.width) * u64::from(self.height);
94 if pixels == 0 {
95 return 0.0;
96 }
97 (self.size_bytes * 8) as f64 / pixels as f64
98 }
99
100 #[must_use]
102 pub fn is_keyframe(&self) -> bool {
103 self.frame_type.is_intra()
104 }
105}
106
107#[derive(Debug, Default)]
109pub struct FrameStatsCollector {
110 frames: Vec<FrameStat>,
111}
112
113impl FrameStatsCollector {
114 #[must_use]
116 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn record(&mut self, stat: FrameStat) {
122 self.frames.push(stat);
123 }
124
125 #[must_use]
127 pub fn frame_count(&self) -> usize {
128 self.frames.len()
129 }
130
131 #[must_use]
133 pub fn i_frame_count(&self) -> usize {
134 self.frames
135 .iter()
136 .filter(|f| f.frame_type.is_intra())
137 .count()
138 }
139
140 #[must_use]
142 pub fn p_frame_count(&self) -> usize {
143 self.frames
144 .iter()
145 .filter(|f| matches!(f.frame_type, FrameType::P | FrameType::SP))
146 .count()
147 }
148
149 #[must_use]
151 pub fn b_frame_count(&self) -> usize {
152 self.frames
153 .iter()
154 .filter(|f| f.frame_type == FrameType::B)
155 .count()
156 }
157
158 #[allow(clippy::cast_precision_loss)]
160 #[must_use]
161 pub fn avg_bits_per_frame(&self) -> f64 {
162 if self.frames.is_empty() {
163 return 0.0;
164 }
165 let total_bytes: u64 = self.frames.iter().map(|f| f.size_bytes).sum();
166 (total_bytes * 8) as f64 / self.frames.len() as f64
167 }
168
169 #[must_use]
171 pub fn total_bytes(&self) -> u64 {
172 self.frames.iter().map(|f| f.size_bytes).sum()
173 }
174
175 #[must_use]
177 pub fn largest_frame(&self) -> Option<&FrameStat> {
178 self.frames.iter().max_by_key(|f| f.size_bytes)
179 }
180
181 #[allow(clippy::cast_precision_loss)]
183 #[must_use]
184 pub fn avg_qp(&self) -> f64 {
185 if self.frames.is_empty() {
186 return 0.0;
187 }
188 let sum: u64 = self.frames.iter().map(|f| u64::from(f.qp)).sum();
189 sum as f64 / self.frames.len() as f64
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 fn make_stat(idx: u64, ft: FrameType, size: u64) -> FrameStat {
198 FrameStat::new(idx, ft, size, 1920, 1080, 28)
199 }
200
201 #[test]
202 fn test_frame_type_is_reference_i() {
203 assert!(FrameType::I.is_reference());
204 }
205
206 #[test]
207 fn test_frame_type_is_reference_b() {
208 assert!(!FrameType::B.is_reference());
209 }
210
211 #[test]
212 fn test_frame_type_is_intra() {
213 assert!(FrameType::I.is_intra());
214 assert!(FrameType::SI.is_intra());
215 assert!(!FrameType::P.is_intra());
216 }
217
218 #[test]
219 fn test_frame_type_is_inter() {
220 assert!(FrameType::P.is_inter());
221 assert!(FrameType::B.is_inter());
222 assert!(!FrameType::I.is_inter());
223 }
224
225 #[test]
226 fn test_frame_type_tag() {
227 assert_eq!(FrameType::I.tag(), "I");
228 assert_eq!(FrameType::B.tag(), "B");
229 assert_eq!(FrameType::SP.tag(), "SP");
230 }
231
232 #[test]
233 fn test_frame_stat_bits_per_pixel() {
234 let s = FrameStat::new(0, FrameType::I, 100_000, 1920, 1080, 20);
236 let bpp = s.bits_per_pixel();
237 assert!((bpp - 800_000.0 / 2_073_600.0).abs() < 1e-6);
238 }
239
240 #[test]
241 fn test_frame_stat_bits_per_pixel_zero_dimension() {
242 let s = FrameStat::new(0, FrameType::I, 1000, 0, 0, 20);
243 assert_eq!(s.bits_per_pixel(), 0.0);
244 }
245
246 #[test]
247 fn test_frame_stat_is_keyframe() {
248 let i = FrameStat::new(0, FrameType::I, 0, 100, 100, 1);
249 assert!(i.is_keyframe());
250 let b = FrameStat::new(1, FrameType::B, 0, 100, 100, 1);
251 assert!(!b.is_keyframe());
252 }
253
254 #[test]
255 fn test_collector_i_frame_count() {
256 let mut c = FrameStatsCollector::new();
257 c.record(make_stat(0, FrameType::I, 50_000));
258 c.record(make_stat(1, FrameType::P, 10_000));
259 c.record(make_stat(2, FrameType::B, 5_000));
260 assert_eq!(c.i_frame_count(), 1);
261 }
262
263 #[test]
264 fn test_collector_b_frame_count() {
265 let mut c = FrameStatsCollector::new();
266 c.record(make_stat(0, FrameType::B, 5_000));
267 c.record(make_stat(1, FrameType::B, 6_000));
268 assert_eq!(c.b_frame_count(), 2);
269 }
270
271 #[test]
272 fn test_collector_avg_bits_per_frame() {
273 let mut c = FrameStatsCollector::new();
274 c.record(make_stat(0, FrameType::I, 100)); c.record(make_stat(1, FrameType::P, 100)); assert!((c.avg_bits_per_frame() - 800.0).abs() < 1e-9);
277 }
278
279 #[test]
280 fn test_collector_total_bytes() {
281 let mut c = FrameStatsCollector::new();
282 c.record(make_stat(0, FrameType::I, 1000));
283 c.record(make_stat(1, FrameType::P, 2000));
284 assert_eq!(c.total_bytes(), 3000);
285 }
286
287 #[test]
288 fn test_collector_largest_frame() {
289 let mut c = FrameStatsCollector::new();
290 c.record(make_stat(0, FrameType::I, 100_000));
291 c.record(make_stat(1, FrameType::P, 10_000));
292 let largest = c.largest_frame().expect("should succeed in test");
293 assert_eq!(largest.index, 0);
294 }
295
296 #[test]
297 fn test_collector_avg_qp() {
298 let mut c = FrameStatsCollector::new();
299 let mut f1 = make_stat(0, FrameType::I, 0);
300 f1.qp = 20;
301 let mut f2 = make_stat(1, FrameType::P, 0);
302 f2.qp = 30;
303 c.record(f1);
304 c.record(f2);
305 assert!((c.avg_qp() - 25.0).abs() < 1e-9);
306 }
307}