1#![allow(dead_code)]
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ThumbnailFormat {
13 Jpeg,
15 Png,
17 Webp,
19}
20
21impl ThumbnailFormat {
22 #[must_use]
24 pub fn extension(&self) -> &'static str {
25 match self {
26 ThumbnailFormat::Jpeg => "jpg",
27 ThumbnailFormat::Png => "png",
28 ThumbnailFormat::Webp => "webp",
29 }
30 }
31
32 #[must_use]
34 pub fn mime_type(&self) -> &'static str {
35 match self {
36 ThumbnailFormat::Jpeg => "image/jpeg",
37 ThumbnailFormat::Png => "image/png",
38 ThumbnailFormat::Webp => "image/webp",
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
45pub enum ThumbnailStrategy {
46 FixedInterval,
48 SceneChange,
50 Uniform,
52 AtTimestamps(Vec<u64>),
54}
55
56#[derive(Debug, Clone)]
58pub struct ThumbnailConfig {
59 pub width: u32,
61 pub height: u32,
63 pub format: ThumbnailFormat,
65 pub quality: u8,
67 pub count: usize,
69 pub interval_strategy: ThumbnailStrategy,
71}
72
73impl ThumbnailConfig {
74 #[must_use]
76 pub fn default_web() -> Self {
77 Self {
78 width: 320,
79 height: 180,
80 format: ThumbnailFormat::Jpeg,
81 quality: 80,
82 count: 10,
83 interval_strategy: ThumbnailStrategy::Uniform,
84 }
85 }
86
87 #[must_use]
89 pub fn sprite_sheet(count: usize) -> Self {
90 Self {
91 width: 160,
92 height: 90,
93 format: ThumbnailFormat::Jpeg,
94 quality: 70,
95 count,
96 interval_strategy: ThumbnailStrategy::Uniform,
97 }
98 }
99
100 #[must_use]
102 pub fn is_valid(&self) -> bool {
103 self.width > 0 && self.height > 0 && self.count > 0
104 }
105}
106
107#[derive(Debug, Clone)]
109pub struct Thumbnail {
110 pub timestamp_ms: u64,
112 pub width: u32,
114 pub height: u32,
116 pub data: Vec<u8>,
118}
119
120impl Thumbnail {
121 #[must_use]
123 pub fn new(timestamp_ms: u64, width: u32, height: u32, data: Vec<u8>) -> Self {
124 Self {
125 timestamp_ms,
126 width,
127 height,
128 data,
129 }
130 }
131
132 #[must_use]
134 pub fn pixel_count(&self) -> usize {
135 (self.width * self.height) as usize
136 }
137
138 #[must_use]
140 pub fn expected_byte_len(&self) -> usize {
141 self.pixel_count() * 4
142 }
143}
144
145#[must_use]
154pub fn compute_thumbnail_timestamps(
155 duration_ms: u64,
156 strategy: &ThumbnailStrategy,
157 fps: f64,
158) -> Vec<u64> {
159 if duration_ms == 0 {
160 return Vec::new();
161 }
162
163 let snap = |ts: f64| -> u64 {
164 if fps > 0.0 {
165 let frame_ms = 1000.0 / fps;
166 ((ts / frame_ms).round() * frame_ms) as u64
167 } else {
168 ts as u64
169 }
170 };
171
172 match strategy {
173 ThumbnailStrategy::AtTimestamps(ts) => {
174 ts.iter().filter(|&&t| t <= duration_ms).copied().collect()
175 }
176
177 ThumbnailStrategy::Uniform => {
178 compute_uniform_timestamps(duration_ms, 10, fps)
182 }
183
184 ThumbnailStrategy::FixedInterval => {
185 let interval_ms = 10_000u64;
187 let mut ts = Vec::new();
188 let mut t = 0u64;
189 while t <= duration_ms {
190 ts.push(snap(t as f64));
191 t += interval_ms;
192 }
193 ts
194 }
195
196 ThumbnailStrategy::SceneChange => {
197 Vec::new()
200 }
201 }
202}
203
204#[must_use]
206pub fn compute_uniform_timestamps(duration_ms: u64, count: usize, fps: f64) -> Vec<u64> {
207 if count == 0 || duration_ms == 0 {
208 return Vec::new();
209 }
210
211 let snap = |ts: f64| -> u64 {
212 if fps > 0.0 {
213 let frame_ms = 1000.0 / fps;
214 ((ts / frame_ms).round() * frame_ms) as u64
215 } else {
216 ts as u64
217 }
218 };
219
220 if count == 1 {
221 return vec![snap(duration_ms as f64 / 2.0)];
222 }
223
224 (0..count)
225 .map(|i| {
226 let t = (duration_ms as f64 * i as f64) / (count - 1) as f64;
227 snap(t).min(duration_ms)
228 })
229 .collect()
230}
231
232#[allow(clippy::too_many_arguments)]
238#[must_use]
239pub fn scale_thumbnail(src: &[u8], src_w: u32, src_h: u32, dst_w: u32, dst_h: u32) -> Vec<u8> {
240 if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
241 return Vec::new();
242 }
243
244 let expected_len = (src_w * src_h * 4) as usize;
245 if src.len() < expected_len {
246 return Vec::new();
247 }
248
249 let mut dst = vec![0u8; (dst_w * dst_h * 4) as usize];
250
251 for dy in 0..dst_h {
252 for dx in 0..dst_w {
253 let sx = (f64::from(dx) * f64::from(src_w) / f64::from(dst_w)) as u32;
255 let sy = (f64::from(dy) * f64::from(src_h) / f64::from(dst_h)) as u32;
256
257 let src_idx = ((sy * src_w + sx) * 4) as usize;
258 let dst_idx = ((dy * dst_w + dx) * 4) as usize;
259
260 if src_idx + 3 < src.len() && dst_idx + 3 < dst.len() {
261 dst[dst_idx] = src[src_idx];
262 dst[dst_idx + 1] = src[src_idx + 1];
263 dst[dst_idx + 2] = src[src_idx + 2];
264 dst[dst_idx + 3] = src[src_idx + 3];
265 }
266 }
267 }
268
269 dst
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_thumbnail_format_extension() {
278 assert_eq!(ThumbnailFormat::Jpeg.extension(), "jpg");
279 assert_eq!(ThumbnailFormat::Png.extension(), "png");
280 assert_eq!(ThumbnailFormat::Webp.extension(), "webp");
281 }
282
283 #[test]
284 fn test_thumbnail_format_mime_type() {
285 assert_eq!(ThumbnailFormat::Jpeg.mime_type(), "image/jpeg");
286 assert_eq!(ThumbnailFormat::Png.mime_type(), "image/png");
287 assert_eq!(ThumbnailFormat::Webp.mime_type(), "image/webp");
288 }
289
290 #[test]
291 fn test_thumbnail_config_default_web() {
292 let cfg = ThumbnailConfig::default_web();
293 assert_eq!(cfg.width, 320);
294 assert_eq!(cfg.height, 180);
295 assert_eq!(cfg.format, ThumbnailFormat::Jpeg);
296 assert_eq!(cfg.count, 10);
297 assert!(cfg.is_valid());
298 }
299
300 #[test]
301 fn test_thumbnail_config_sprite_sheet() {
302 let cfg = ThumbnailConfig::sprite_sheet(20);
303 assert_eq!(cfg.width, 160);
304 assert_eq!(cfg.height, 90);
305 assert_eq!(cfg.count, 20);
306 assert!(cfg.is_valid());
307 }
308
309 #[test]
310 fn test_thumbnail_pixel_count() {
311 let thumb = Thumbnail::new(0, 160, 90, vec![0; 160 * 90 * 4]);
312 assert_eq!(thumb.pixel_count(), 14400);
313 assert_eq!(thumb.expected_byte_len(), 57600);
314 }
315
316 #[test]
317 fn test_compute_timestamps_at_timestamps() {
318 let strategy = ThumbnailStrategy::AtTimestamps(vec![1000, 2000, 3000]);
319 let ts = compute_thumbnail_timestamps(5000, &strategy, 0.0);
320 assert_eq!(ts, vec![1000, 2000, 3000]);
321 }
322
323 #[test]
324 fn test_compute_timestamps_at_timestamps_filters_out_of_range() {
325 let strategy = ThumbnailStrategy::AtTimestamps(vec![1000, 2000, 9999]);
326 let ts = compute_thumbnail_timestamps(5000, &strategy, 0.0);
327 assert_eq!(ts, vec![1000, 2000]);
328 }
329
330 #[test]
331 fn test_compute_timestamps_zero_duration() {
332 let ts = compute_thumbnail_timestamps(0, &ThumbnailStrategy::Uniform, 24.0);
333 assert!(ts.is_empty());
334 }
335
336 #[test]
337 fn test_compute_uniform_timestamps_count() {
338 let ts = compute_uniform_timestamps(60_000, 5, 0.0);
339 assert_eq!(ts.len(), 5);
340 assert_eq!(ts[0], 0);
342 assert_eq!(ts[4], 60_000);
343 }
344
345 #[test]
346 fn test_compute_uniform_timestamps_single() {
347 let ts = compute_uniform_timestamps(10_000, 1, 0.0);
348 assert_eq!(ts.len(), 1);
349 assert_eq!(ts[0], 5000);
350 }
351
352 #[test]
353 fn test_compute_fixed_interval_timestamps() {
354 let ts = compute_thumbnail_timestamps(30_000, &ThumbnailStrategy::FixedInterval, 0.0);
356 assert_eq!(ts, vec![0, 10_000, 20_000, 30_000]);
357 }
358
359 #[test]
360 fn test_scale_thumbnail_identity() {
361 let src = vec![
363 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, ];
368 let dst = scale_thumbnail(&src, 2, 2, 2, 2);
369 assert_eq!(dst, src);
370 }
371
372 #[test]
373 fn test_scale_thumbnail_upscale() {
374 let src = vec![100u8, 150, 200, 255];
376 let dst = scale_thumbnail(&src, 1, 1, 2, 2);
377 assert_eq!(dst.len(), 16);
378 assert_eq!(&dst[0..4], &[100, 150, 200, 255]);
380 assert_eq!(&dst[4..8], &[100, 150, 200, 255]);
381 }
382
383 #[test]
384 fn test_scale_thumbnail_zero_dimensions() {
385 let src = vec![255u8; 16];
386 assert!(scale_thumbnail(&src, 0, 2, 4, 4).is_empty());
387 assert!(scale_thumbnail(&src, 2, 2, 0, 4).is_empty());
388 }
389
390 #[test]
391 fn test_scale_thumbnail_undersized_src() {
392 let src = vec![255u8; 4]; let dst = scale_thumbnail(&src, 4, 4, 2, 2);
395 assert!(dst.is_empty());
396 }
397}