1use crate::driver::Screenshot;
15use crate::result::{ProbarError, ProbarResult};
16use image::{DynamicImage, ImageFormat};
17use serde::{Deserialize, Serialize};
18use std::io::{Cursor, Write};
19use std::path::Path;
20use std::time::{Duration, Instant};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum VideoCodec {
25 Mjpeg,
27 Raw,
29}
30
31impl Default for VideoCodec {
32 fn default() -> Self {
33 Self::Mjpeg
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum RecordingState {
40 Idle,
42 Recording,
44 Stopped,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct VideoConfig {
51 pub fps: u8,
53 pub width: u32,
55 pub height: u32,
57 pub bitrate: u32,
59 pub codec: VideoCodec,
61 pub max_duration_secs: u32,
63 pub jpeg_quality: u8,
65}
66
67impl Default for VideoConfig {
68 fn default() -> Self {
69 Self {
70 fps: 30,
71 width: 1280,
72 height: 720,
73 bitrate: 5000,
74 codec: VideoCodec::Mjpeg,
75 max_duration_secs: 300, jpeg_quality: 85,
77 }
78 }
79}
80
81impl VideoConfig {
82 #[must_use]
84 pub fn new(width: u32, height: u32) -> Self {
85 Self {
86 width,
87 height,
88 ..Default::default()
89 }
90 }
91
92 #[must_use]
94 pub fn with_fps(mut self, fps: u8) -> Self {
95 self.fps = fps.clamp(1, 60);
96 self
97 }
98
99 #[must_use]
101 pub fn with_bitrate(mut self, bitrate: u32) -> Self {
102 self.bitrate = bitrate;
103 self
104 }
105
106 #[must_use]
108 pub fn with_codec(mut self, codec: VideoCodec) -> Self {
109 self.codec = codec;
110 self
111 }
112
113 #[must_use]
115 pub fn with_max_duration(mut self, secs: u32) -> Self {
116 self.max_duration_secs = secs;
117 self
118 }
119
120 #[must_use]
122 pub fn with_jpeg_quality(mut self, quality: u8) -> Self {
123 self.jpeg_quality = quality.clamp(1, 100);
124 self
125 }
126
127 #[must_use]
129 pub fn frame_duration(&self) -> Duration {
130 Duration::from_millis(1000 / u64::from(self.fps.max(1)))
131 }
132
133 #[must_use]
135 pub fn timescale(&self) -> u32 {
136 u32::from(self.fps) * 100
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct EncodedFrame {
143 pub data: Vec<u8>,
145 pub timestamp_ms: u64,
147 pub duration_ms: u64,
149}
150
151#[derive(Debug)]
174pub struct VideoRecorder {
175 config: VideoConfig,
176 frames: Vec<EncodedFrame>,
177 state: RecordingState,
178 start_time: Option<Instant>,
179 last_frame_time: Option<Instant>,
180}
181
182impl VideoRecorder {
183 #[must_use]
185 pub fn new(config: VideoConfig) -> Self {
186 Self {
187 config,
188 frames: Vec::new(),
189 state: RecordingState::Idle,
190 start_time: None,
191 last_frame_time: None,
192 }
193 }
194
195 #[must_use]
197 pub fn state(&self) -> RecordingState {
198 self.state
199 }
200
201 #[must_use]
203 pub fn frame_count(&self) -> usize {
204 self.frames.len()
205 }
206
207 #[must_use]
209 pub fn config(&self) -> &VideoConfig {
210 &self.config
211 }
212
213 pub fn start(&mut self) -> ProbarResult<()> {
215 if self.state == RecordingState::Recording {
216 return Err(ProbarError::VideoRecording {
217 message: "Recording already in progress".to_string(),
218 });
219 }
220
221 self.frames.clear();
222 self.state = RecordingState::Recording;
223 self.start_time = Some(Instant::now());
224 self.last_frame_time = None;
225
226 Ok(())
227 }
228
229 pub fn capture_frame(&mut self, screenshot: &Screenshot) -> ProbarResult<()> {
231 if self.state != RecordingState::Recording {
232 return Err(ProbarError::VideoRecording {
233 message: "Recording not started".to_string(),
234 });
235 }
236
237 let start_time = self.start_time.ok_or_else(|| ProbarError::VideoRecording {
238 message: "Recording start time not set".to_string(),
239 })?;
240
241 let elapsed = start_time.elapsed();
243 if self.config.max_duration_secs > 0
244 && elapsed.as_secs() > u64::from(self.config.max_duration_secs)
245 {
246 return Err(ProbarError::VideoRecording {
247 message: format!(
248 "Maximum recording duration of {} seconds exceeded",
249 self.config.max_duration_secs
250 ),
251 });
252 }
253
254 let frame_duration = self.config.frame_duration();
256 if let Some(last_time) = self.last_frame_time {
257 let since_last = last_time.elapsed();
258 if since_last < frame_duration {
259 return Ok(());
261 }
262 }
263
264 let encoded = self.encode_frame(screenshot)?;
266 let timestamp_ms = elapsed.as_millis() as u64;
267
268 self.frames.push(EncodedFrame {
269 data: encoded,
270 timestamp_ms,
271 duration_ms: frame_duration.as_millis() as u64,
272 });
273
274 self.last_frame_time = Some(Instant::now());
275 Ok(())
276 }
277
278 pub fn capture_raw_frame(&mut self, data: &[u8], width: u32, height: u32) -> ProbarResult<()> {
280 if self.state != RecordingState::Recording {
281 return Err(ProbarError::VideoRecording {
282 message: "Recording not started".to_string(),
283 });
284 }
285
286 let start_time = self.start_time.ok_or_else(|| ProbarError::VideoRecording {
287 message: "Recording start time not set".to_string(),
288 })?;
289
290 let elapsed = start_time.elapsed();
292 if self.config.max_duration_secs > 0
293 && elapsed.as_secs() > u64::from(self.config.max_duration_secs)
294 {
295 return Err(ProbarError::VideoRecording {
296 message: format!(
297 "Maximum recording duration of {} seconds exceeded",
298 self.config.max_duration_secs
299 ),
300 });
301 }
302
303 let frame_duration = self.config.frame_duration();
305 if let Some(last_time) = self.last_frame_time {
306 if last_time.elapsed() < frame_duration {
307 return Ok(());
308 }
309 }
310
311 let encoded = self.encode_raw_frame(data, width, height)?;
313 let timestamp_ms = elapsed.as_millis() as u64;
314
315 self.frames.push(EncodedFrame {
316 data: encoded,
317 timestamp_ms,
318 duration_ms: frame_duration.as_millis() as u64,
319 });
320
321 self.last_frame_time = Some(Instant::now());
322 Ok(())
323 }
324
325 pub fn stop(&mut self) -> ProbarResult<Vec<u8>> {
327 if self.state != RecordingState::Recording {
328 return Err(ProbarError::VideoRecording {
329 message: "Recording not in progress".to_string(),
330 });
331 }
332
333 self.state = RecordingState::Stopped;
334
335 if self.frames.is_empty() {
336 return Err(ProbarError::VideoRecording {
337 message: "No frames captured".to_string(),
338 });
339 }
340
341 self.generate_mp4()
342 }
343
344 pub fn save(&self, path: &Path) -> ProbarResult<()> {
346 if self.state != RecordingState::Stopped {
347 return Err(ProbarError::VideoRecording {
348 message: "Recording must be stopped before saving".to_string(),
349 });
350 }
351
352 if self.frames.is_empty() {
353 return Err(ProbarError::VideoRecording {
354 message: "No frames to save".to_string(),
355 });
356 }
357
358 let video_data = self.generate_mp4()?;
359 std::fs::write(path, video_data)?;
360 Ok(())
361 }
362
363 fn encode_frame(&self, screenshot: &Screenshot) -> ProbarResult<Vec<u8>> {
365 let cursor = Cursor::new(&screenshot.data);
367 let img =
368 image::load(cursor, ImageFormat::Png).map_err(|e| ProbarError::VideoRecording {
369 message: format!("Failed to decode screenshot: {e}"),
370 })?;
371
372 let img = if img.width() != self.config.width || img.height() != self.config.height {
374 img.resize_exact(
375 self.config.width,
376 self.config.height,
377 image::imageops::FilterType::Lanczos3,
378 )
379 } else {
380 img
381 };
382
383 self.encode_image(&img)
384 }
385
386 fn encode_raw_frame(&self, data: &[u8], width: u32, height: u32) -> ProbarResult<Vec<u8>> {
388 let img = image::RgbaImage::from_raw(width, height, data.to_vec()).ok_or_else(|| {
389 ProbarError::VideoRecording {
390 message: "Invalid raw frame dimensions".to_string(),
391 }
392 })?;
393
394 let img = DynamicImage::ImageRgba8(img);
395
396 let img = if width != self.config.width || height != self.config.height {
398 img.resize_exact(
399 self.config.width,
400 self.config.height,
401 image::imageops::FilterType::Lanczos3,
402 )
403 } else {
404 img
405 };
406
407 self.encode_image(&img)
408 }
409
410 fn encode_image(&self, img: &DynamicImage) -> ProbarResult<Vec<u8>> {
412 match self.config.codec {
413 VideoCodec::Mjpeg => {
414 let rgb = img.to_rgb8();
415 let mut buffer = Cursor::new(Vec::new());
416 let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(
417 &mut buffer,
418 self.config.jpeg_quality,
419 );
420 encoder
421 .encode(
422 rgb.as_raw(),
423 self.config.width,
424 self.config.height,
425 image::ExtendedColorType::Rgb8,
426 )
427 .map_err(|e| ProbarError::VideoRecording {
428 message: format!("JPEG encoding failed: {e}"),
429 })?;
430 Ok(buffer.into_inner())
431 }
432 VideoCodec::Raw => {
433 Ok(img.to_rgb8().into_raw())
435 }
436 }
437 }
438
439 fn generate_mp4(&self) -> ProbarResult<Vec<u8>> {
441 let mut output = Vec::new();
445
446 self.write_ftyp_box(&mut output)?;
448
449 let frames_size: usize = self.frames.iter().map(|f| f.data.len()).sum();
451
452 self.write_mdat_box(&mut output, frames_size)?;
454
455 self.write_moov_box(&mut output)?;
457
458 Ok(output)
459 }
460
461 fn write_ftyp_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
463 let brand = b"isom";
464 let minor_version: u32 = 512;
465 let compatible_brands = [b"isom", b"iso2", b"mp41"];
466
467 let size = 8 + 4 + 4 + (compatible_brands.len() * 4);
468 self.write_box_header(out, size as u32, b"ftyp")?;
469 out.write_all(brand)?;
470 out.write_all(&minor_version.to_be_bytes())?;
471 for brand in &compatible_brands {
472 out.write_all(*brand)?;
473 }
474 Ok(())
475 }
476
477 fn write_mdat_box(&self, out: &mut Vec<u8>, data_size: usize) -> ProbarResult<()> {
479 let box_size = 8 + data_size;
480 self.write_box_header(out, box_size as u32, b"mdat")?;
481 for frame in &self.frames {
482 out.write_all(&frame.data)?;
483 }
484 Ok(())
485 }
486
487 fn write_moov_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
489 let mut moov_contents = Vec::new();
491
492 self.write_mvhd_box(&mut moov_contents)?;
494
495 self.write_trak_box(&mut moov_contents)?;
497
498 let moov_size = 8 + moov_contents.len();
499 self.write_box_header(out, moov_size as u32, b"moov")?;
500 out.write_all(&moov_contents)?;
501 Ok(())
502 }
503
504 fn write_mvhd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
506 let timescale = self.config.timescale();
507 let duration = self.calculate_duration();
508
509 let mut content = Vec::new();
510 content.write_all(&[0, 0, 0, 0])?;
512 content.write_all(&0u32.to_be_bytes())?;
514 content.write_all(&0u32.to_be_bytes())?;
516 content.write_all(×cale.to_be_bytes())?;
518 content.write_all(&duration.to_be_bytes())?;
520 content.write_all(&0x00010000u32.to_be_bytes())?;
522 content.write_all(&[0x01, 0x00])?;
524 content.write_all(&[0u8; 10])?;
526 let matrix: [u32; 9] = [0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000];
528 for val in &matrix {
529 content.write_all(&val.to_be_bytes())?;
530 }
531 content.write_all(&[0u8; 24])?;
533 content.write_all(&2u32.to_be_bytes())?;
535
536 let size = 8 + content.len();
537 self.write_box_header(out, size as u32, b"mvhd")?;
538 out.write_all(&content)?;
539 Ok(())
540 }
541
542 fn write_trak_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
544 let mut trak_contents = Vec::new();
545
546 self.write_tkhd_box(&mut trak_contents)?;
548
549 self.write_mdia_box(&mut trak_contents)?;
551
552 let trak_size = 8 + trak_contents.len();
553 self.write_box_header(out, trak_size as u32, b"trak")?;
554 out.write_all(&trak_contents)?;
555 Ok(())
556 }
557
558 fn write_tkhd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
560 let duration = self.calculate_duration();
561
562 let mut content = Vec::new();
563 content.write_all(&[0, 0, 0, 3])?;
565 content.write_all(&0u32.to_be_bytes())?;
567 content.write_all(&0u32.to_be_bytes())?;
569 content.write_all(&1u32.to_be_bytes())?;
571 content.write_all(&0u32.to_be_bytes())?;
573 content.write_all(&duration.to_be_bytes())?;
575 content.write_all(&[0u8; 8])?;
577 content.write_all(&0u16.to_be_bytes())?;
579 content.write_all(&0u16.to_be_bytes())?;
581 content.write_all(&0u16.to_be_bytes())?;
583 content.write_all(&0u16.to_be_bytes())?;
585 let matrix: [u32; 9] = [0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000];
587 for val in &matrix {
588 content.write_all(&val.to_be_bytes())?;
589 }
590 content.write_all(&(self.config.width << 16).to_be_bytes())?;
592 content.write_all(&(self.config.height << 16).to_be_bytes())?;
594
595 let size = 8 + content.len();
596 self.write_box_header(out, size as u32, b"tkhd")?;
597 out.write_all(&content)?;
598 Ok(())
599 }
600
601 fn write_mdia_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
603 let mut mdia_contents = Vec::new();
604
605 self.write_mdhd_box(&mut mdia_contents)?;
607
608 self.write_hdlr_box(&mut mdia_contents)?;
610
611 self.write_minf_box(&mut mdia_contents)?;
613
614 let mdia_size = 8 + mdia_contents.len();
615 self.write_box_header(out, mdia_size as u32, b"mdia")?;
616 out.write_all(&mdia_contents)?;
617 Ok(())
618 }
619
620 fn write_mdhd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
622 let timescale = self.config.timescale();
623 let duration = self.calculate_duration();
624
625 let mut content = Vec::new();
626 content.write_all(&[0, 0, 0, 0])?;
628 content.write_all(&0u32.to_be_bytes())?;
630 content.write_all(&0u32.to_be_bytes())?;
632 content.write_all(×cale.to_be_bytes())?;
634 content.write_all(&duration.to_be_bytes())?;
636 content.write_all(&0x55c4u16.to_be_bytes())?;
638 content.write_all(&0u16.to_be_bytes())?;
640
641 let size = 8 + content.len();
642 self.write_box_header(out, size as u32, b"mdhd")?;
643 out.write_all(&content)?;
644 Ok(())
645 }
646
647 fn write_hdlr_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
649 let mut content = Vec::new();
650 content.write_all(&[0, 0, 0, 0])?;
652 content.write_all(&0u32.to_be_bytes())?;
654 content.write_all(b"vide")?;
656 content.write_all(&[0u8; 12])?;
658 content.write_all(b"Probar Video Handler\0")?;
660
661 let size = 8 + content.len();
662 self.write_box_header(out, size as u32, b"hdlr")?;
663 out.write_all(&content)?;
664 Ok(())
665 }
666
667 fn write_minf_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
669 let mut minf_contents = Vec::new();
670
671 self.write_vmhd_box(&mut minf_contents)?;
673
674 self.write_dinf_box(&mut minf_contents)?;
676
677 self.write_stbl_box(&mut minf_contents)?;
679
680 let minf_size = 8 + minf_contents.len();
681 self.write_box_header(out, minf_size as u32, b"minf")?;
682 out.write_all(&minf_contents)?;
683 Ok(())
684 }
685
686 fn write_vmhd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
688 let mut content = Vec::new();
689 content.write_all(&[0, 0, 0, 1])?;
691 content.write_all(&0u16.to_be_bytes())?;
693 content.write_all(&[0u8; 6])?;
695
696 let size = 8 + content.len();
697 self.write_box_header(out, size as u32, b"vmhd")?;
698 out.write_all(&content)?;
699 Ok(())
700 }
701
702 fn write_dinf_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
704 let mut dinf_contents = Vec::new();
705
706 self.write_dref_box(&mut dinf_contents)?;
708
709 let dinf_size = 8 + dinf_contents.len();
710 self.write_box_header(out, dinf_size as u32, b"dinf")?;
711 out.write_all(&dinf_contents)?;
712 Ok(())
713 }
714
715 fn write_dref_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
717 let mut content = Vec::new();
718 content.write_all(&[0, 0, 0, 0])?;
720 content.write_all(&1u32.to_be_bytes())?;
722
723 content.write_all(&12u32.to_be_bytes())?; content.write_all(b"url ")?;
726 content.write_all(&[0, 0, 0, 1])?; let size = 8 + content.len();
729 self.write_box_header(out, size as u32, b"dref")?;
730 out.write_all(&content)?;
731 Ok(())
732 }
733
734 fn write_stbl_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
736 let mut stbl_contents = Vec::new();
737
738 self.write_stsd_box(&mut stbl_contents)?;
740
741 self.write_stts_box(&mut stbl_contents)?;
743
744 self.write_stsc_box(&mut stbl_contents)?;
746
747 self.write_stsz_box(&mut stbl_contents)?;
749
750 self.write_stco_box(&mut stbl_contents)?;
752
753 let stbl_size = 8 + stbl_contents.len();
754 self.write_box_header(out, stbl_size as u32, b"stbl")?;
755 out.write_all(&stbl_contents)?;
756 Ok(())
757 }
758
759 fn write_stsd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
761 let mut content = Vec::new();
762 content.write_all(&[0, 0, 0, 0])?;
764 content.write_all(&1u32.to_be_bytes())?;
766
767 let codec_tag = match self.config.codec {
769 VideoCodec::Mjpeg => b"jpeg",
770 VideoCodec::Raw => b"raw ",
771 };
772
773 let mut entry = Vec::new();
775 entry.write_all(&[0u8; 6])?;
777 entry.write_all(&1u16.to_be_bytes())?;
779 entry.write_all(&0u16.to_be_bytes())?;
781 entry.write_all(&0u16.to_be_bytes())?;
783 entry.write_all(&[0u8; 12])?;
785 entry.write_all(&(self.config.width as u16).to_be_bytes())?;
787 entry.write_all(&(self.config.height as u16).to_be_bytes())?;
789 entry.write_all(&0x00480000u32.to_be_bytes())?;
791 entry.write_all(&0x00480000u32.to_be_bytes())?;
793 entry.write_all(&0u32.to_be_bytes())?;
795 entry.write_all(&1u16.to_be_bytes())?;
797 let mut compressor_name = [0u8; 32];
799 let name = b"Probar Video";
800 compressor_name[0] = name.len() as u8;
801 compressor_name[1..1 + name.len()].copy_from_slice(name);
802 entry.write_all(&compressor_name)?;
803 entry.write_all(&24u16.to_be_bytes())?;
805 entry.write_all(&(-1i16).to_be_bytes())?;
807
808 let entry_size = 8 + entry.len();
809 content.write_all(&(entry_size as u32).to_be_bytes())?;
810 content.write_all(codec_tag)?;
811 content.write_all(&entry)?;
812
813 let size = 8 + content.len();
814 self.write_box_header(out, size as u32, b"stsd")?;
815 out.write_all(&content)?;
816 Ok(())
817 }
818
819 fn write_stts_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
821 let frame_duration_ticks = self.config.timescale() / u32::from(self.config.fps);
822
823 let mut content = Vec::new();
824 content.write_all(&[0, 0, 0, 0])?;
826 content.write_all(&1u32.to_be_bytes())?;
828 content.write_all(&(self.frames.len() as u32).to_be_bytes())?;
830 content.write_all(&frame_duration_ticks.to_be_bytes())?;
832
833 let size = 8 + content.len();
834 self.write_box_header(out, size as u32, b"stts")?;
835 out.write_all(&content)?;
836 Ok(())
837 }
838
839 fn write_stsc_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
841 let mut content = Vec::new();
842 content.write_all(&[0, 0, 0, 0])?;
844 content.write_all(&1u32.to_be_bytes())?;
846 content.write_all(&1u32.to_be_bytes())?;
848 content.write_all(&(self.frames.len() as u32).to_be_bytes())?;
850 content.write_all(&1u32.to_be_bytes())?;
852
853 let size = 8 + content.len();
854 self.write_box_header(out, size as u32, b"stsc")?;
855 out.write_all(&content)?;
856 Ok(())
857 }
858
859 fn write_stsz_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
861 let mut content = Vec::new();
862 content.write_all(&[0, 0, 0, 0])?;
864 content.write_all(&0u32.to_be_bytes())?;
866 content.write_all(&(self.frames.len() as u32).to_be_bytes())?;
868 for frame in &self.frames {
870 content.write_all(&(frame.data.len() as u32).to_be_bytes())?;
871 }
872
873 let size = 8 + content.len();
874 self.write_box_header(out, size as u32, b"stsz")?;
875 out.write_all(&content)?;
876 Ok(())
877 }
878
879 fn write_stco_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
881 let ftyp_size = 8 + 4 + 4 + 12; let mdat_header_size = 8;
884 let mdat_offset = ftyp_size + mdat_header_size;
885
886 let mut content = Vec::new();
887 content.write_all(&[0, 0, 0, 0])?;
889 content.write_all(&1u32.to_be_bytes())?;
891 content.write_all(&(mdat_offset as u32).to_be_bytes())?;
893
894 let size = 8 + content.len();
895 self.write_box_header(out, size as u32, b"stco")?;
896 out.write_all(&content)?;
897 Ok(())
898 }
899
900 fn write_box_header(
902 &self,
903 out: &mut Vec<u8>,
904 size: u32,
905 box_type: &[u8; 4],
906 ) -> ProbarResult<()> {
907 out.write_all(&size.to_be_bytes())?;
908 out.write_all(box_type)?;
909 Ok(())
910 }
911
912 fn calculate_duration(&self) -> u32 {
914 let frame_count = self.frames.len() as u32;
915 let frame_duration_ticks = self.config.timescale() / u32::from(self.config.fps);
916 frame_count * frame_duration_ticks
917 }
918}
919
920#[cfg(test)]
921#[allow(clippy::unwrap_used, clippy::expect_used)]
922mod tests {
923 use super::*;
924
925 mod video_config_tests {
926 use super::*;
927
928 #[test]
929 fn test_default_config() {
930 let config = VideoConfig::default();
931 assert_eq!(config.fps, 30);
932 assert_eq!(config.width, 1280);
933 assert_eq!(config.height, 720);
934 assert_eq!(config.bitrate, 5000);
935 assert_eq!(config.codec, VideoCodec::Mjpeg);
936 assert_eq!(config.max_duration_secs, 300);
937 assert_eq!(config.jpeg_quality, 85);
938 }
939
940 #[test]
941 fn test_config_new() {
942 let config = VideoConfig::new(1920, 1080);
943 assert_eq!(config.width, 1920);
944 assert_eq!(config.height, 1080);
945 }
946
947 #[test]
948 fn test_config_builder() {
949 let config = VideoConfig::new(800, 600)
950 .with_fps(60)
951 .with_bitrate(10000)
952 .with_codec(VideoCodec::Raw)
953 .with_max_duration(600)
954 .with_jpeg_quality(95);
955
956 assert_eq!(config.fps, 60);
957 assert_eq!(config.bitrate, 10000);
958 assert_eq!(config.codec, VideoCodec::Raw);
959 assert_eq!(config.max_duration_secs, 600);
960 assert_eq!(config.jpeg_quality, 95);
961 }
962
963 #[test]
964 fn test_fps_clamping() {
965 let config = VideoConfig::default().with_fps(0);
966 assert_eq!(config.fps, 1);
967
968 let config = VideoConfig::default().with_fps(100);
969 assert_eq!(config.fps, 60);
970 }
971
972 #[test]
973 fn test_jpeg_quality_clamping() {
974 let config = VideoConfig::default().with_jpeg_quality(0);
975 assert_eq!(config.jpeg_quality, 1);
976
977 let config = VideoConfig::default().with_jpeg_quality(200);
978 assert_eq!(config.jpeg_quality, 100);
979 }
980
981 #[test]
982 fn test_frame_duration() {
983 let config = VideoConfig::default().with_fps(30);
984 let duration = config.frame_duration();
985 assert_eq!(duration.as_millis(), 33);
986
987 let config = VideoConfig::default().with_fps(60);
988 let duration = config.frame_duration();
989 assert_eq!(duration.as_millis(), 16);
990 }
991
992 #[test]
993 fn test_timescale() {
994 let config = VideoConfig::default().with_fps(30);
995 assert_eq!(config.timescale(), 3000);
996
997 let config = VideoConfig::default().with_fps(60);
998 assert_eq!(config.timescale(), 6000);
999 }
1000 }
1001
1002 mod video_codec_tests {
1003 use super::*;
1004
1005 #[test]
1006 fn test_default_codec() {
1007 let codec = VideoCodec::default();
1008 assert_eq!(codec, VideoCodec::Mjpeg);
1009 }
1010
1011 #[test]
1012 fn test_codec_equality() {
1013 assert_eq!(VideoCodec::Mjpeg, VideoCodec::Mjpeg);
1014 assert_eq!(VideoCodec::Raw, VideoCodec::Raw);
1015 assert_ne!(VideoCodec::Mjpeg, VideoCodec::Raw);
1016 }
1017 }
1018
1019 mod recording_state_tests {
1020 use super::*;
1021
1022 #[test]
1023 fn test_state_equality() {
1024 assert_eq!(RecordingState::Idle, RecordingState::Idle);
1025 assert_eq!(RecordingState::Recording, RecordingState::Recording);
1026 assert_eq!(RecordingState::Stopped, RecordingState::Stopped);
1027 assert_ne!(RecordingState::Idle, RecordingState::Recording);
1028 }
1029 }
1030
1031 mod video_recorder_tests {
1032 use super::*;
1033
1034 #[test]
1035 fn test_new_recorder() {
1036 let config = VideoConfig::default();
1037 let recorder = VideoRecorder::new(config);
1038 assert_eq!(recorder.state(), RecordingState::Idle);
1039 assert_eq!(recorder.frame_count(), 0);
1040 }
1041
1042 #[test]
1043 fn test_start_recording() {
1044 let config = VideoConfig::default();
1045 let mut recorder = VideoRecorder::new(config);
1046
1047 recorder.start().expect("Failed to start recording");
1048 assert_eq!(recorder.state(), RecordingState::Recording);
1049 }
1050
1051 #[test]
1052 fn test_double_start_error() {
1053 let config = VideoConfig::default();
1054 let mut recorder = VideoRecorder::new(config);
1055
1056 recorder.start().expect("Failed to start recording");
1057 let result = recorder.start();
1058 assert!(result.is_err());
1059 }
1060
1061 #[test]
1062 fn test_capture_without_start_error() {
1063 let config = VideoConfig::default();
1064 let mut recorder = VideoRecorder::new(config);
1065
1066 let data = vec![255u8; 800 * 600 * 4];
1067 let result = recorder.capture_raw_frame(&data, 800, 600);
1068 assert!(result.is_err());
1069 }
1070
1071 #[test]
1072 fn test_stop_without_start_error() {
1073 let config = VideoConfig::default();
1074 let mut recorder = VideoRecorder::new(config);
1075
1076 let result = recorder.stop();
1077 assert!(result.is_err());
1078 }
1079
1080 #[test]
1081 fn test_stop_without_frames_error() {
1082 let config = VideoConfig::default();
1083 let mut recorder = VideoRecorder::new(config);
1084
1085 recorder.start().expect("Failed to start recording");
1086 let result = recorder.stop();
1087 assert!(result.is_err());
1088 }
1089
1090 #[test]
1091 fn test_capture_raw_frame() {
1092 let config = VideoConfig::new(10, 10).with_fps(1);
1093 let mut recorder = VideoRecorder::new(config);
1094
1095 recorder.start().expect("Failed to start recording");
1096
1097 let data = vec![255, 0, 0, 255].repeat(100); recorder
1100 .capture_raw_frame(&data, 10, 10)
1101 .expect("Failed to capture frame");
1102
1103 assert_eq!(recorder.frame_count(), 1);
1104 }
1105
1106 #[test]
1107 fn test_full_recording_cycle() {
1108 let config = VideoConfig::new(10, 10).with_fps(1);
1109 let mut recorder = VideoRecorder::new(config);
1110
1111 recorder.start().expect("Failed to start recording");
1112
1113 for _ in 0..3 {
1115 let data = vec![255, 0, 0, 255].repeat(100);
1116 recorder
1117 .capture_raw_frame(&data, 10, 10)
1118 .expect("Failed to capture frame");
1119 std::thread::sleep(std::time::Duration::from_millis(1100));
1121 }
1122
1123 let video_data = recorder.stop().expect("Failed to stop recording");
1124 assert!(!video_data.is_empty());
1125
1126 assert!(video_data.len() >= 8);
1128 assert_eq!(&video_data[4..8], b"ftyp");
1129 }
1130
1131 #[test]
1132 fn test_config_accessor() {
1133 let config = VideoConfig::new(1920, 1080).with_fps(60);
1134 let recorder = VideoRecorder::new(config);
1135
1136 assert_eq!(recorder.config().width, 1920);
1137 assert_eq!(recorder.config().height, 1080);
1138 assert_eq!(recorder.config().fps, 60);
1139 }
1140 }
1141
1142 mod encoded_frame_tests {
1143 use super::*;
1144
1145 #[test]
1146 fn test_encoded_frame_creation() {
1147 let frame = EncodedFrame {
1148 data: vec![1, 2, 3, 4],
1149 timestamp_ms: 100,
1150 duration_ms: 33,
1151 };
1152
1153 assert_eq!(frame.data.len(), 4);
1154 assert_eq!(frame.timestamp_ms, 100);
1155 assert_eq!(frame.duration_ms, 33);
1156 }
1157 }
1158
1159 mod mp4_generation_tests {
1160 use super::*;
1161
1162 #[test]
1163 fn test_mp4_has_correct_structure() {
1164 let config = VideoConfig::new(10, 10).with_fps(1);
1165 let mut recorder = VideoRecorder::new(config);
1166
1167 recorder.start().expect("Failed to start");
1168 let data = vec![255, 0, 0, 255].repeat(100);
1169 recorder
1170 .capture_raw_frame(&data, 10, 10)
1171 .expect("Failed to capture");
1172
1173 let video = recorder.stop().expect("Failed to stop");
1174
1175 assert!(find_box(&video, b"ftyp").is_some());
1177
1178 assert!(find_box(&video, b"mdat").is_some());
1180
1181 assert!(find_box(&video, b"moov").is_some());
1183 }
1184 }
1185
1186 mod save_tests {
1187 use super::*;
1188 use tempfile::TempDir;
1189
1190 #[test]
1191 fn test_save_without_stop_error() {
1192 let config = VideoConfig::new(10, 10);
1193 let recorder = VideoRecorder::new(config);
1194 let temp_dir = TempDir::new().unwrap();
1195 let path = temp_dir.path().join("test.mp4");
1196
1197 let result = recorder.save(&path);
1198 assert!(result.is_err());
1199 }
1200
1201 #[test]
1202 fn test_save_after_stop() {
1203 let config = VideoConfig::new(10, 10).with_fps(1);
1204 let mut recorder = VideoRecorder::new(config);
1205
1206 recorder.start().unwrap();
1207 let data = vec![255, 0, 0, 255].repeat(100);
1208 recorder.capture_raw_frame(&data, 10, 10).unwrap();
1209 std::thread::sleep(std::time::Duration::from_millis(1100));
1210 recorder.capture_raw_frame(&data, 10, 10).unwrap();
1211 recorder.stop().unwrap();
1212
1213 let temp_dir = TempDir::new().unwrap();
1214 let path = temp_dir.path().join("test.mp4");
1215 recorder.save(&path).unwrap();
1216
1217 assert!(path.exists());
1218 let saved_data = std::fs::read(&path).unwrap();
1219 assert!(!saved_data.is_empty());
1220 }
1221 }
1222
1223 mod frame_rate_tests {
1224 use super::*;
1225
1226 #[test]
1227 fn test_frame_skipping() {
1228 let config = VideoConfig::new(10, 10).with_fps(1);
1229 let mut recorder = VideoRecorder::new(config);
1230
1231 recorder.start().unwrap();
1232 let data = vec![255, 0, 0, 255].repeat(100);
1233
1234 for _ in 0..5 {
1236 recorder.capture_raw_frame(&data, 10, 10).unwrap();
1237 }
1238
1239 assert_eq!(recorder.frame_count(), 1);
1241 }
1242 }
1243
1244 mod resize_tests {
1245 use super::*;
1246
1247 #[test]
1248 fn test_resize_frame() {
1249 let config = VideoConfig::new(20, 20).with_fps(1);
1250 let mut recorder = VideoRecorder::new(config);
1251
1252 recorder.start().unwrap();
1253
1254 let data = vec![255, 0, 0, 255].repeat(100);
1256 recorder.capture_raw_frame(&data, 10, 10).unwrap();
1257
1258 assert_eq!(recorder.frame_count(), 1);
1259 }
1260 }
1261
1262 mod invalid_frame_tests {
1263 use super::*;
1264
1265 #[test]
1266 fn test_invalid_raw_frame_dimensions() {
1267 let config = VideoConfig::new(10, 10).with_fps(1);
1268 let mut recorder = VideoRecorder::new(config);
1269
1270 recorder.start().unwrap();
1271
1272 let data = vec![255u8; 10];
1274 let result = recorder.capture_raw_frame(&data, 10, 10);
1275 assert!(result.is_err());
1276 }
1277 }
1278
1279 mod codec_tests {
1280 use super::*;
1281
1282 #[test]
1283 fn test_raw_codec() {
1284 let config = VideoConfig::new(10, 10)
1285 .with_fps(1)
1286 .with_codec(VideoCodec::Raw);
1287 let mut recorder = VideoRecorder::new(config);
1288
1289 recorder.start().unwrap();
1290 let data = vec![255, 0, 0, 255].repeat(100);
1291 recorder.capture_raw_frame(&data, 10, 10).unwrap();
1292
1293 assert_eq!(recorder.frame_count(), 1);
1295 }
1296
1297 #[test]
1298 fn test_codec_debug() {
1299 assert!(format!("{:?}", VideoCodec::Mjpeg).contains("Mjpeg"));
1300 assert!(format!("{:?}", VideoCodec::Raw).contains("Raw"));
1301 }
1302
1303 #[test]
1304 fn test_codec_clone() {
1305 let codec = VideoCodec::Mjpeg;
1306 let cloned = codec;
1307 assert_eq!(codec, cloned);
1308 }
1309 }
1310
1311 mod recording_state_debug {
1312 use super::*;
1313
1314 #[test]
1315 fn test_state_debug() {
1316 assert!(format!("{:?}", RecordingState::Idle).contains("Idle"));
1317 assert!(format!("{:?}", RecordingState::Recording).contains("Recording"));
1318 assert!(format!("{:?}", RecordingState::Stopped).contains("Stopped"));
1319 }
1320
1321 #[test]
1322 fn test_state_clone() {
1323 let state = RecordingState::Recording;
1324 let cloned = state;
1325 assert_eq!(state, cloned);
1326 }
1327 }
1328
1329 mod debug_tests {
1330 use super::*;
1331
1332 #[test]
1333 fn test_video_recorder_debug() {
1334 let config = VideoConfig::new(10, 10);
1335 let recorder = VideoRecorder::new(config);
1336 let debug = format!("{:?}", recorder);
1337 assert!(debug.contains("VideoRecorder"));
1338 }
1339
1340 #[test]
1341 fn test_video_config_debug() {
1342 let config = VideoConfig::default();
1343 let debug = format!("{:?}", config);
1344 assert!(debug.contains("VideoConfig"));
1345 }
1346
1347 #[test]
1348 fn test_encoded_frame_debug() {
1349 let frame = EncodedFrame {
1350 data: vec![1, 2, 3],
1351 timestamp_ms: 100,
1352 duration_ms: 33,
1353 };
1354 let debug = format!("{:?}", frame);
1355 assert!(debug.contains("EncodedFrame"));
1356 }
1357 }
1358
1359 mod screenshot_tests {
1360 use super::*;
1361 use crate::driver::Screenshot;
1362 use std::time::SystemTime;
1363
1364 fn create_minimal_png(width: u32, height: u32) -> Vec<u8> {
1365 let data = vec![255u8; (width * height * 4) as usize]; let img = image::RgbaImage::from_raw(width, height, data).unwrap();
1368
1369 let mut buffer = std::io::Cursor::new(Vec::new());
1370 image::DynamicImage::ImageRgba8(img)
1371 .write_to(&mut buffer, image::ImageFormat::Png)
1372 .unwrap();
1373 buffer.into_inner()
1374 }
1375
1376 #[test]
1377 fn test_capture_frame_with_screenshot() {
1378 let config = VideoConfig::new(10, 10).with_fps(1);
1379 let mut recorder = VideoRecorder::new(config);
1380
1381 recorder.start().unwrap();
1382
1383 let screenshot = Screenshot {
1384 data: create_minimal_png(10, 10),
1385 width: 10,
1386 height: 10,
1387 device_pixel_ratio: 1.0,
1388 timestamp: SystemTime::now(),
1389 };
1390
1391 recorder.capture_frame(&screenshot).unwrap();
1392 assert_eq!(recorder.frame_count(), 1);
1393 }
1394
1395 #[test]
1396 fn test_capture_frame_resize() {
1397 let config = VideoConfig::new(20, 20).with_fps(1); let mut recorder = VideoRecorder::new(config);
1399
1400 recorder.start().unwrap();
1401
1402 let screenshot = Screenshot {
1403 data: create_minimal_png(10, 10), width: 10,
1405 height: 10,
1406 device_pixel_ratio: 1.0,
1407 timestamp: SystemTime::now(),
1408 };
1409
1410 recorder.capture_frame(&screenshot).unwrap();
1411 assert_eq!(recorder.frame_count(), 1);
1412 }
1413
1414 #[test]
1415 fn test_capture_frame_not_started() {
1416 let config = VideoConfig::new(10, 10);
1417 let mut recorder = VideoRecorder::new(config);
1418
1419 let screenshot = Screenshot {
1420 data: create_minimal_png(10, 10),
1421 width: 10,
1422 height: 10,
1423 device_pixel_ratio: 1.0,
1424 timestamp: SystemTime::now(),
1425 };
1426
1427 let result = recorder.capture_frame(&screenshot);
1428 assert!(result.is_err());
1429 }
1430 }
1431
1432 mod mp4_box_tests {
1433 use super::*;
1434
1435 #[test]
1436 fn test_multiple_frames_mp4() {
1437 let config = VideoConfig::new(10, 10).with_fps(30);
1438 let mut recorder = VideoRecorder::new(config);
1439
1440 recorder.start().unwrap();
1441 let data = vec![255, 0, 0, 255].repeat(100);
1442 recorder.capture_raw_frame(&data, 10, 10).unwrap();
1443
1444 std::thread::sleep(std::time::Duration::from_millis(40));
1446 recorder.capture_raw_frame(&data, 10, 10).unwrap();
1447
1448 std::thread::sleep(std::time::Duration::from_millis(40));
1449 recorder.capture_raw_frame(&data, 10, 10).unwrap();
1450
1451 let video = recorder.stop().unwrap();
1452
1453 assert!(find_box(&video, b"ftyp").is_some());
1455 assert!(find_box(&video, b"mdat").is_some());
1456 assert!(find_box(&video, b"moov").is_some());
1457 }
1458
1459 #[test]
1460 fn test_calculate_duration() {
1461 let config = VideoConfig::new(10, 10).with_fps(30);
1462 let mut recorder = VideoRecorder::new(config);
1463
1464 recorder.start().unwrap();
1465 let data = vec![255, 0, 0, 255].repeat(100);
1466 recorder.capture_raw_frame(&data, 10, 10).unwrap();
1467
1468 assert_eq!(recorder.frame_count(), 1);
1470 }
1471 }
1472
1473 mod config_clone_tests {
1474 use super::*;
1475
1476 #[test]
1477 fn test_video_config_clone() {
1478 let config = VideoConfig::new(1920, 1080)
1479 .with_fps(60)
1480 .with_bitrate(10000);
1481 let cloned = config.clone();
1482
1483 assert_eq!(config.width, cloned.width);
1484 assert_eq!(config.height, cloned.height);
1485 assert_eq!(config.fps, cloned.fps);
1486 assert_eq!(config.bitrate, cloned.bitrate);
1487 }
1488
1489 #[test]
1490 fn test_encoded_frame_clone() {
1491 let frame = EncodedFrame {
1492 data: vec![1, 2, 3],
1493 timestamp_ms: 100,
1494 duration_ms: 33,
1495 };
1496 let cloned = frame.clone();
1497
1498 assert_eq!(frame.data, cloned.data);
1499 assert_eq!(frame.timestamp_ms, cloned.timestamp_ms);
1500 }
1501 }
1502
1503 fn find_box(data: &[u8], box_type: &[u8; 4]) -> Option<usize> {
1505 let mut offset = 0;
1506 while offset + 8 <= data.len() {
1507 let size = u32::from_be_bytes([
1508 data[offset],
1509 data[offset + 1],
1510 data[offset + 2],
1511 data[offset + 3],
1512 ]) as usize;
1513
1514 if &data[offset + 4..offset + 8] == box_type {
1515 return Some(offset);
1516 }
1517
1518 if size == 0 {
1519 break;
1520 }
1521
1522 offset += size;
1523 }
1524 None
1525 }
1526
1527 mod h0_video_config_tests {
1532 use super::*;
1533
1534 #[test]
1535 fn h0_video_01_config_default_fps() {
1536 let config = VideoConfig::default();
1537 assert_eq!(config.fps, 30);
1538 }
1539
1540 #[test]
1541 fn h0_video_02_config_default_width() {
1542 let config = VideoConfig::default();
1543 assert_eq!(config.width, 1280);
1544 }
1545
1546 #[test]
1547 fn h0_video_03_config_default_height() {
1548 let config = VideoConfig::default();
1549 assert_eq!(config.height, 720);
1550 }
1551
1552 #[test]
1553 fn h0_video_04_config_default_bitrate() {
1554 let config = VideoConfig::default();
1555 assert_eq!(config.bitrate, 5000);
1556 }
1557
1558 #[test]
1559 fn h0_video_05_config_default_codec() {
1560 let config = VideoConfig::default();
1561 assert_eq!(config.codec, VideoCodec::Mjpeg);
1562 }
1563
1564 #[test]
1565 fn h0_video_06_config_default_max_duration() {
1566 let config = VideoConfig::default();
1567 assert_eq!(config.max_duration_secs, 300);
1568 }
1569
1570 #[test]
1571 fn h0_video_07_config_default_jpeg_quality() {
1572 let config = VideoConfig::default();
1573 assert_eq!(config.jpeg_quality, 85);
1574 }
1575
1576 #[test]
1577 fn h0_video_08_config_new_dimensions() {
1578 let config = VideoConfig::new(1920, 1080);
1579 assert_eq!(config.width, 1920);
1580 assert_eq!(config.height, 1080);
1581 }
1582
1583 #[test]
1584 fn h0_video_09_config_with_fps() {
1585 let config = VideoConfig::default().with_fps(60);
1586 assert_eq!(config.fps, 60);
1587 }
1588
1589 #[test]
1590 fn h0_video_10_config_fps_clamp_min() {
1591 let config = VideoConfig::default().with_fps(0);
1592 assert_eq!(config.fps, 1);
1593 }
1594 }
1595
1596 mod h0_video_config_builder_tests {
1597 use super::*;
1598
1599 #[test]
1600 fn h0_video_11_config_fps_clamp_max() {
1601 let config = VideoConfig::default().with_fps(100);
1602 assert_eq!(config.fps, 60);
1603 }
1604
1605 #[test]
1606 fn h0_video_12_config_with_bitrate() {
1607 let config = VideoConfig::default().with_bitrate(10000);
1608 assert_eq!(config.bitrate, 10000);
1609 }
1610
1611 #[test]
1612 fn h0_video_13_config_with_codec_raw() {
1613 let config = VideoConfig::default().with_codec(VideoCodec::Raw);
1614 assert_eq!(config.codec, VideoCodec::Raw);
1615 }
1616
1617 #[test]
1618 fn h0_video_14_config_with_max_duration() {
1619 let config = VideoConfig::default().with_max_duration(600);
1620 assert_eq!(config.max_duration_secs, 600);
1621 }
1622
1623 #[test]
1624 fn h0_video_15_config_with_jpeg_quality() {
1625 let config = VideoConfig::default().with_jpeg_quality(95);
1626 assert_eq!(config.jpeg_quality, 95);
1627 }
1628
1629 #[test]
1630 fn h0_video_16_config_jpeg_clamp_min() {
1631 let config = VideoConfig::default().with_jpeg_quality(0);
1632 assert_eq!(config.jpeg_quality, 1);
1633 }
1634
1635 #[test]
1636 fn h0_video_17_config_jpeg_clamp_max() {
1637 let config = VideoConfig::default().with_jpeg_quality(200);
1638 assert_eq!(config.jpeg_quality, 100);
1639 }
1640
1641 #[test]
1642 fn h0_video_18_config_frame_duration_30fps() {
1643 let config = VideoConfig::default().with_fps(30);
1644 assert_eq!(config.frame_duration().as_millis(), 33);
1645 }
1646
1647 #[test]
1648 fn h0_video_19_config_frame_duration_60fps() {
1649 let config = VideoConfig::default().with_fps(60);
1650 assert_eq!(config.frame_duration().as_millis(), 16);
1651 }
1652
1653 #[test]
1654 fn h0_video_20_config_timescale_30fps() {
1655 let config = VideoConfig::default().with_fps(30);
1656 assert_eq!(config.timescale(), 3000);
1657 }
1658 }
1659
1660 mod h0_video_codec_tests {
1661 use super::*;
1662
1663 #[test]
1664 fn h0_video_21_codec_default_mjpeg() {
1665 assert_eq!(VideoCodec::default(), VideoCodec::Mjpeg);
1666 }
1667
1668 #[test]
1669 fn h0_video_22_codec_equality_mjpeg() {
1670 assert_eq!(VideoCodec::Mjpeg, VideoCodec::Mjpeg);
1671 }
1672
1673 #[test]
1674 fn h0_video_23_codec_equality_raw() {
1675 assert_eq!(VideoCodec::Raw, VideoCodec::Raw);
1676 }
1677
1678 #[test]
1679 fn h0_video_24_codec_inequality() {
1680 assert_ne!(VideoCodec::Mjpeg, VideoCodec::Raw);
1681 }
1682
1683 #[test]
1684 fn h0_video_25_codec_debug_mjpeg() {
1685 let debug = format!("{:?}", VideoCodec::Mjpeg);
1686 assert!(debug.contains("Mjpeg"));
1687 }
1688
1689 #[test]
1690 fn h0_video_26_codec_debug_raw() {
1691 let debug = format!("{:?}", VideoCodec::Raw);
1692 assert!(debug.contains("Raw"));
1693 }
1694
1695 #[test]
1696 fn h0_video_27_codec_clone() {
1697 let codec = VideoCodec::Mjpeg;
1698 let cloned = codec;
1699 assert_eq!(codec, cloned);
1700 }
1701
1702 #[test]
1703 fn h0_video_28_codec_copy() {
1704 let codec = VideoCodec::Raw;
1705 let copied: VideoCodec = codec;
1706 assert_eq!(codec, copied);
1707 }
1708 }
1709
1710 mod h0_recording_state_tests {
1711 use super::*;
1712
1713 #[test]
1714 fn h0_video_29_state_idle() {
1715 assert_eq!(RecordingState::Idle, RecordingState::Idle);
1716 }
1717
1718 #[test]
1719 fn h0_video_30_state_recording() {
1720 assert_eq!(RecordingState::Recording, RecordingState::Recording);
1721 }
1722
1723 #[test]
1724 fn h0_video_31_state_stopped() {
1725 assert_eq!(RecordingState::Stopped, RecordingState::Stopped);
1726 }
1727
1728 #[test]
1729 fn h0_video_32_state_inequality() {
1730 assert_ne!(RecordingState::Idle, RecordingState::Recording);
1731 assert_ne!(RecordingState::Recording, RecordingState::Stopped);
1732 }
1733
1734 #[test]
1735 fn h0_video_33_state_debug() {
1736 assert!(format!("{:?}", RecordingState::Idle).contains("Idle"));
1737 }
1738
1739 #[test]
1740 fn h0_video_34_state_copy() {
1741 let state = RecordingState::Recording;
1742 let copied: RecordingState = state;
1743 assert_eq!(state, copied);
1744 }
1745 }
1746
1747 mod h0_recorder_tests {
1748 use super::*;
1749
1750 #[test]
1751 fn h0_video_35_recorder_new_idle() {
1752 let recorder = VideoRecorder::new(VideoConfig::default());
1753 assert_eq!(recorder.state(), RecordingState::Idle);
1754 }
1755
1756 #[test]
1757 fn h0_video_36_recorder_new_no_frames() {
1758 let recorder = VideoRecorder::new(VideoConfig::default());
1759 assert_eq!(recorder.frame_count(), 0);
1760 }
1761
1762 #[test]
1763 fn h0_video_37_recorder_start_recording() {
1764 let mut recorder = VideoRecorder::new(VideoConfig::default());
1765 recorder.start().unwrap();
1766 assert_eq!(recorder.state(), RecordingState::Recording);
1767 }
1768
1769 #[test]
1770 fn h0_video_38_recorder_double_start_error() {
1771 let mut recorder = VideoRecorder::new(VideoConfig::default());
1772 recorder.start().unwrap();
1773 assert!(recorder.start().is_err());
1774 }
1775
1776 #[test]
1777 fn h0_video_39_recorder_capture_without_start() {
1778 let mut recorder = VideoRecorder::new(VideoConfig::new(10, 10));
1779 let data = vec![255u8; 400];
1780 assert!(recorder.capture_raw_frame(&data, 10, 10).is_err());
1781 }
1782
1783 #[test]
1784 fn h0_video_40_recorder_stop_without_start() {
1785 let mut recorder = VideoRecorder::new(VideoConfig::default());
1786 assert!(recorder.stop().is_err());
1787 }
1788 }
1789
1790 mod h0_recorder_frame_tests {
1791 use super::*;
1792
1793 #[test]
1794 fn h0_video_41_recorder_capture_frame() {
1795 let mut recorder = VideoRecorder::new(VideoConfig::new(10, 10).with_fps(1));
1796 recorder.start().unwrap();
1797 let data = vec![255, 0, 0, 255].repeat(100);
1798 recorder.capture_raw_frame(&data, 10, 10).unwrap();
1799 assert_eq!(recorder.frame_count(), 1);
1800 }
1801
1802 #[test]
1803 fn h0_video_42_recorder_config_accessor() {
1804 let config = VideoConfig::new(1920, 1080).with_fps(60);
1805 let recorder = VideoRecorder::new(config);
1806 assert_eq!(recorder.config().width, 1920);
1807 }
1808
1809 #[test]
1810 fn h0_video_43_recorder_invalid_dimensions() {
1811 let mut recorder = VideoRecorder::new(VideoConfig::new(10, 10).with_fps(1));
1812 recorder.start().unwrap();
1813 let data = vec![255u8; 10]; assert!(recorder.capture_raw_frame(&data, 10, 10).is_err());
1815 }
1816
1817 #[test]
1818 fn h0_video_44_recorder_debug() {
1819 let recorder = VideoRecorder::new(VideoConfig::default());
1820 let debug = format!("{:?}", recorder);
1821 assert!(debug.contains("VideoRecorder"));
1822 }
1823 }
1824
1825 mod h0_encoded_frame_tests {
1826 use super::*;
1827
1828 #[test]
1829 fn h0_video_45_frame_data() {
1830 let frame = EncodedFrame {
1831 data: vec![1, 2, 3],
1832 timestamp_ms: 0,
1833 duration_ms: 33,
1834 };
1835 assert_eq!(frame.data.len(), 3);
1836 }
1837
1838 #[test]
1839 fn h0_video_46_frame_timestamp() {
1840 let frame = EncodedFrame {
1841 data: vec![],
1842 timestamp_ms: 100,
1843 duration_ms: 33,
1844 };
1845 assert_eq!(frame.timestamp_ms, 100);
1846 }
1847
1848 #[test]
1849 fn h0_video_47_frame_duration() {
1850 let frame = EncodedFrame {
1851 data: vec![],
1852 timestamp_ms: 0,
1853 duration_ms: 16,
1854 };
1855 assert_eq!(frame.duration_ms, 16);
1856 }
1857
1858 #[test]
1859 fn h0_video_48_frame_clone() {
1860 let frame = EncodedFrame {
1861 data: vec![1, 2, 3],
1862 timestamp_ms: 50,
1863 duration_ms: 33,
1864 };
1865 let cloned = frame;
1866 assert_eq!(cloned.data, vec![1, 2, 3]);
1867 }
1868
1869 #[test]
1870 fn h0_video_49_frame_debug() {
1871 let frame = EncodedFrame {
1872 data: vec![],
1873 timestamp_ms: 0,
1874 duration_ms: 33,
1875 };
1876 let debug = format!("{:?}", frame);
1877 assert!(debug.contains("EncodedFrame"));
1878 }
1879
1880 #[test]
1881 fn h0_video_50_config_timescale_60fps() {
1882 let config = VideoConfig::default().with_fps(60);
1883 assert_eq!(config.timescale(), 6000);
1884 }
1885 }
1886
1887 mod max_duration_tests {
1892 use super::*;
1893
1894 #[test]
1896 fn test_capture_frame_max_duration_exceeded() {
1897 use crate::driver::Screenshot;
1898 use std::time::SystemTime;
1899
1900 let config = VideoConfig::new(10, 10).with_fps(1).with_max_duration(0);
1903 let mut recorder = VideoRecorder::new(config);
1904
1905 recorder.start().unwrap();
1906
1907 let data = vec![255u8; (10 * 10 * 4) as usize];
1909 let img = image::RgbaImage::from_raw(10, 10, data).unwrap();
1910 let mut buffer = std::io::Cursor::new(Vec::new());
1911 image::DynamicImage::ImageRgba8(img)
1912 .write_to(&mut buffer, image::ImageFormat::Png)
1913 .unwrap();
1914
1915 let screenshot = Screenshot {
1916 data: buffer.into_inner(),
1917 width: 10,
1918 height: 10,
1919 device_pixel_ratio: 1.0,
1920 timestamp: SystemTime::now(),
1921 };
1922
1923 recorder.capture_frame(&screenshot).unwrap();
1925 assert_eq!(recorder.frame_count(), 1);
1926 }
1927
1928 #[test]
1930 fn test_raw_frame_max_duration_zero_unlimited() {
1931 let config = VideoConfig::new(10, 10).with_fps(1).with_max_duration(0);
1932 let mut recorder = VideoRecorder::new(config);
1933
1934 recorder.start().unwrap();
1935 let data = vec![255, 0, 0, 255].repeat(100);
1936 recorder.capture_raw_frame(&data, 10, 10).unwrap();
1937
1938 assert_eq!(recorder.frame_count(), 1);
1940 }
1941 }
1942
1943 mod frame_rate_limiting_tests {
1944 use super::*;
1945
1946 #[test]
1948 fn test_capture_frame_rate_limiting() {
1949 use crate::driver::Screenshot;
1950 use std::time::SystemTime;
1951
1952 let config = VideoConfig::new(10, 10).with_fps(1);
1953 let mut recorder = VideoRecorder::new(config);
1954
1955 recorder.start().unwrap();
1956
1957 let data = vec![255u8; (10 * 10 * 4) as usize];
1959 let img = image::RgbaImage::from_raw(10, 10, data).unwrap();
1960 let mut buffer = std::io::Cursor::new(Vec::new());
1961 image::DynamicImage::ImageRgba8(img)
1962 .write_to(&mut buffer, image::ImageFormat::Png)
1963 .unwrap();
1964 let png_data = buffer.into_inner();
1965
1966 let screenshot1 = Screenshot {
1968 data: png_data.clone(),
1969 width: 10,
1970 height: 10,
1971 device_pixel_ratio: 1.0,
1972 timestamp: SystemTime::now(),
1973 };
1974 recorder.capture_frame(&screenshot1).unwrap();
1975
1976 let screenshot2 = Screenshot {
1978 data: png_data,
1979 width: 10,
1980 height: 10,
1981 device_pixel_ratio: 1.0,
1982 timestamp: SystemTime::now(),
1983 };
1984 recorder.capture_frame(&screenshot2).unwrap();
1985
1986 assert_eq!(recorder.frame_count(), 1);
1988 }
1989 }
1990
1991 mod save_edge_case_tests {
1992 use super::*;
1993 use tempfile::TempDir;
1994
1995 #[test]
1997 fn test_save_while_recording_error() {
1998 let config = VideoConfig::new(10, 10).with_fps(1);
1999 let mut recorder = VideoRecorder::new(config);
2000
2001 recorder.start().unwrap();
2002 let data = vec![255, 0, 0, 255].repeat(100);
2003 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2004
2005 let temp_dir = TempDir::new().unwrap();
2006 let path = temp_dir.path().join("test.mp4");
2007
2008 let result = recorder.save(&path);
2010 assert!(result.is_err());
2011 }
2012
2013 #[test]
2015 fn test_save_from_idle_error() {
2016 let config = VideoConfig::new(10, 10);
2017 let recorder = VideoRecorder::new(config);
2018
2019 let temp_dir = TempDir::new().unwrap();
2020 let path = temp_dir.path().join("test.mp4");
2021
2022 let result = recorder.save(&path);
2023 assert!(result.is_err());
2024 }
2025 }
2026
2027 mod raw_codec_tests {
2028 use super::*;
2029
2030 #[test]
2032 fn test_raw_codec_full_cycle() {
2033 let config = VideoConfig::new(10, 10)
2034 .with_fps(1)
2035 .with_codec(VideoCodec::Raw);
2036 let mut recorder = VideoRecorder::new(config);
2037
2038 recorder.start().unwrap();
2039 let data = vec![255, 0, 0, 255].repeat(100);
2040 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2041
2042 let video = recorder.stop().unwrap();
2043
2044 assert!(find_box(&video, b"ftyp").is_some());
2046 assert!(find_box(&video, b"mdat").is_some());
2047 assert!(find_box(&video, b"moov").is_some());
2048 }
2049
2050 #[test]
2052 fn test_raw_codec_frame_encoding() {
2053 let raw_config = VideoConfig::new(10, 10)
2054 .with_fps(1)
2055 .with_codec(VideoCodec::Raw);
2056 let mjpeg_config = VideoConfig::new(10, 10)
2057 .with_fps(1)
2058 .with_codec(VideoCodec::Mjpeg);
2059
2060 let mut raw_recorder = VideoRecorder::new(raw_config);
2061 let mut mjpeg_recorder = VideoRecorder::new(mjpeg_config);
2062
2063 raw_recorder.start().unwrap();
2064 mjpeg_recorder.start().unwrap();
2065
2066 let data = vec![255, 128, 64, 255].repeat(100);
2067 raw_recorder.capture_raw_frame(&data, 10, 10).unwrap();
2068 mjpeg_recorder.capture_raw_frame(&data, 10, 10).unwrap();
2069
2070 assert_eq!(raw_recorder.frame_count(), 1);
2072 assert_eq!(mjpeg_recorder.frame_count(), 1);
2073 }
2074 }
2075
2076 mod screenshot_error_tests {
2077 use super::*;
2078
2079 #[test]
2081 fn test_invalid_png_decode_error() {
2082 use crate::driver::Screenshot;
2083 use std::time::SystemTime;
2084
2085 let config = VideoConfig::new(10, 10).with_fps(1);
2086 let mut recorder = VideoRecorder::new(config);
2087
2088 recorder.start().unwrap();
2089
2090 let screenshot = Screenshot {
2092 data: vec![0, 1, 2, 3, 4, 5], width: 10,
2094 height: 10,
2095 device_pixel_ratio: 1.0,
2096 timestamp: SystemTime::now(),
2097 };
2098
2099 let result = recorder.capture_frame(&screenshot);
2100 assert!(result.is_err());
2101
2102 if let Err(ProbarError::VideoRecording { message }) = result {
2104 assert!(
2105 message.contains("decode") || message.contains("Failed"),
2106 "Error message should mention decode failure"
2107 );
2108 }
2109 }
2110 }
2111
2112 mod screenshot_same_size_tests {
2113 use super::*;
2114
2115 #[test]
2117 fn test_screenshot_no_resize_needed() {
2118 use crate::driver::Screenshot;
2119 use std::time::SystemTime;
2120
2121 let config = VideoConfig::new(10, 10).with_fps(1);
2122 let mut recorder = VideoRecorder::new(config);
2123
2124 recorder.start().unwrap();
2125
2126 let data = vec![128u8; (10 * 10 * 4) as usize];
2128 let img = image::RgbaImage::from_raw(10, 10, data).unwrap();
2129 let mut buffer = std::io::Cursor::new(Vec::new());
2130 image::DynamicImage::ImageRgba8(img)
2131 .write_to(&mut buffer, image::ImageFormat::Png)
2132 .unwrap();
2133
2134 let screenshot = Screenshot {
2135 data: buffer.into_inner(),
2136 width: 10,
2137 height: 10,
2138 device_pixel_ratio: 1.0,
2139 timestamp: SystemTime::now(),
2140 };
2141
2142 recorder.capture_frame(&screenshot).unwrap();
2143 assert_eq!(recorder.frame_count(), 1);
2144 }
2145 }
2146
2147 mod raw_frame_same_size_tests {
2148 use super::*;
2149
2150 #[test]
2152 fn test_raw_frame_no_resize_needed() {
2153 let config = VideoConfig::new(10, 10).with_fps(1);
2154 let mut recorder = VideoRecorder::new(config);
2155
2156 recorder.start().unwrap();
2157
2158 let data = vec![255, 0, 0, 255].repeat(100); recorder.capture_raw_frame(&data, 10, 10).unwrap();
2161 assert_eq!(recorder.frame_count(), 1);
2162 }
2163
2164 #[test]
2166 fn test_raw_frame_needs_resize() {
2167 let config = VideoConfig::new(20, 20).with_fps(1); let mut recorder = VideoRecorder::new(config);
2169
2170 recorder.start().unwrap();
2171
2172 let data = vec![255, 0, 0, 255].repeat(100); recorder.capture_raw_frame(&data, 10, 10).unwrap();
2175 assert_eq!(recorder.frame_count(), 1);
2176 }
2177 }
2178
2179 mod serialization_tests {
2180 use super::*;
2181
2182 #[test]
2184 fn test_codec_serialization() {
2185 let mjpeg = VideoCodec::Mjpeg;
2186 let raw = VideoCodec::Raw;
2187
2188 let mjpeg_json = serde_json::to_string(&mjpeg).unwrap();
2189 let raw_json = serde_json::to_string(&raw).unwrap();
2190
2191 assert!(mjpeg_json.contains("Mjpeg"));
2192 assert!(raw_json.contains("Raw"));
2193
2194 let mjpeg_back: VideoCodec = serde_json::from_str(&mjpeg_json).unwrap();
2196 let raw_back: VideoCodec = serde_json::from_str(&raw_json).unwrap();
2197
2198 assert_eq!(mjpeg, mjpeg_back);
2199 assert_eq!(raw, raw_back);
2200 }
2201
2202 #[test]
2204 fn test_config_serialization() {
2205 let config = VideoConfig::new(1920, 1080)
2206 .with_fps(60)
2207 .with_bitrate(10000)
2208 .with_codec(VideoCodec::Raw)
2209 .with_max_duration(600)
2210 .with_jpeg_quality(95);
2211
2212 let json = serde_json::to_string(&config).unwrap();
2213
2214 assert!(json.contains("1920"));
2216 assert!(json.contains("1080"));
2217 assert!(json.contains("60"));
2218 assert!(json.contains("10000"));
2219 assert!(json.contains("Raw"));
2220 assert!(json.contains("600"));
2221 assert!(json.contains("95"));
2222
2223 let config_back: VideoConfig = serde_json::from_str(&json).unwrap();
2225 assert_eq!(config.width, config_back.width);
2226 assert_eq!(config.height, config_back.height);
2227 assert_eq!(config.fps, config_back.fps);
2228 assert_eq!(config.bitrate, config_back.bitrate);
2229 assert_eq!(config.codec, config_back.codec);
2230 assert_eq!(config.max_duration_secs, config_back.max_duration_secs);
2231 assert_eq!(config.jpeg_quality, config_back.jpeg_quality);
2232 }
2233 }
2234
2235 mod raw_frame_rate_limiting_tests {
2236 use super::*;
2237
2238 #[test]
2240 fn test_raw_frame_rate_limiting_detailed() {
2241 let config = VideoConfig::new(10, 10).with_fps(60); let mut recorder = VideoRecorder::new(config);
2243
2244 recorder.start().unwrap();
2245
2246 let data = vec![255, 0, 0, 255].repeat(100);
2247
2248 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2250 assert_eq!(recorder.frame_count(), 1);
2251
2252 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2254 assert_eq!(recorder.frame_count(), 1);
2255
2256 std::thread::sleep(std::time::Duration::from_millis(20));
2258 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2259 assert_eq!(recorder.frame_count(), 2);
2260 }
2261 }
2262
2263 mod multiple_frames_with_different_codecs {
2264 use super::*;
2265
2266 #[test]
2268 fn test_mjpeg_multiple_frames_mp4() {
2269 let config = VideoConfig::new(10, 10)
2270 .with_fps(60)
2271 .with_codec(VideoCodec::Mjpeg);
2272 let mut recorder = VideoRecorder::new(config);
2273
2274 recorder.start().unwrap();
2275
2276 let data = vec![255, 0, 0, 255].repeat(100);
2277 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2278
2279 std::thread::sleep(std::time::Duration::from_millis(20));
2280 let data2 = vec![0, 255, 0, 255].repeat(100);
2281 recorder.capture_raw_frame(&data2, 10, 10).unwrap();
2282
2283 std::thread::sleep(std::time::Duration::from_millis(20));
2284 let data3 = vec![0, 0, 255, 255].repeat(100);
2285 recorder.capture_raw_frame(&data3, 10, 10).unwrap();
2286
2287 let video = recorder.stop().unwrap();
2288
2289 assert!(find_box(&video, b"ftyp").is_some());
2291 assert!(find_box(&video, b"mdat").is_some());
2292 assert!(find_box(&video, b"moov").is_some());
2293 }
2294
2295 #[test]
2297 fn test_raw_multiple_frames_mp4() {
2298 let config = VideoConfig::new(10, 10)
2299 .with_fps(60)
2300 .with_codec(VideoCodec::Raw);
2301 let mut recorder = VideoRecorder::new(config);
2302
2303 recorder.start().unwrap();
2304
2305 let data = vec![255, 0, 0, 255].repeat(100);
2306 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2307
2308 std::thread::sleep(std::time::Duration::from_millis(20));
2309 let data2 = vec![0, 255, 0, 255].repeat(100);
2310 recorder.capture_raw_frame(&data2, 10, 10).unwrap();
2311
2312 let video = recorder.stop().unwrap();
2313
2314 assert!(find_box(&video, b"ftyp").is_some());
2316 assert!(find_box(&video, b"mdat").is_some());
2317 assert!(find_box(&video, b"moov").is_some());
2318 }
2319 }
2320
2321 mod start_after_stop_tests {
2322 use super::*;
2323
2324 #[test]
2326 fn test_restart_after_stop() {
2327 let config = VideoConfig::new(10, 10).with_fps(1);
2328 let mut recorder = VideoRecorder::new(config);
2329
2330 recorder.start().unwrap();
2332 let data = vec![255, 0, 0, 255].repeat(100);
2333 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2334 let video1 = recorder.stop().unwrap();
2335 assert!(!video1.is_empty());
2336
2337 recorder.start().unwrap();
2339 assert_eq!(recorder.state(), RecordingState::Recording);
2340 assert_eq!(recorder.frame_count(), 0); }
2342 }
2343
2344 mod frame_duration_edge_cases {
2345 use super::*;
2346
2347 #[test]
2349 fn test_frame_duration_min_fps() {
2350 let config = VideoConfig::default().with_fps(1);
2351 let duration = config.frame_duration();
2352 assert_eq!(duration.as_millis(), 1000);
2353 }
2354
2355 #[test]
2357 fn test_frame_duration_with_zero_fps_config() {
2358 let mut config = VideoConfig::default();
2360 config = config.with_fps(0);
2362 assert_eq!(config.fps, 1);
2363 assert_eq!(config.frame_duration().as_millis(), 1000);
2364 }
2365 }
2366
2367 mod calculate_duration_tests {
2368 use super::*;
2369
2370 #[test]
2372 fn test_duration_calculation_multiple_frames() {
2373 let config = VideoConfig::new(10, 10).with_fps(30);
2374 let mut recorder = VideoRecorder::new(config);
2375
2376 recorder.start().unwrap();
2377
2378 let data = vec![255, 0, 0, 255].repeat(100);
2379
2380 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2382 std::thread::sleep(std::time::Duration::from_millis(40));
2383 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2384 std::thread::sleep(std::time::Duration::from_millis(40));
2385 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2386
2387 assert_eq!(recorder.frame_count(), 3);
2388 }
2389 }
2390
2391 mod write_error_path_tests {
2392 use super::*;
2393 use tempfile::TempDir;
2394
2395 #[test]
2397 fn test_save_to_nonexistent_directory() {
2398 let config = VideoConfig::new(10, 10).with_fps(1);
2399 let mut recorder = VideoRecorder::new(config);
2400
2401 recorder.start().unwrap();
2402 let data = vec![255, 0, 0, 255].repeat(100);
2403 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2404 recorder.stop().unwrap();
2405
2406 let result = recorder.save(std::path::Path::new(
2408 "/nonexistent/directory/that/does/not/exist/test.mp4",
2409 ));
2410 assert!(result.is_err());
2411 }
2412
2413 #[test]
2415 fn test_save_creates_valid_mp4_file() {
2416 let config = VideoConfig::new(10, 10).with_fps(1);
2417 let mut recorder = VideoRecorder::new(config);
2418
2419 recorder.start().unwrap();
2420 let data = vec![255, 0, 0, 255].repeat(100);
2421 recorder.capture_raw_frame(&data, 10, 10).unwrap();
2422 recorder.stop().unwrap();
2423
2424 let temp_dir = TempDir::new().unwrap();
2425 let path = temp_dir.path().join("test_video.mp4");
2426 recorder.save(&path).unwrap();
2427
2428 assert!(path.exists());
2430 let content = std::fs::read(&path).unwrap();
2431 assert!(!content.is_empty());
2432
2433 assert_eq!(&content[4..8], b"ftyp");
2435 }
2436 }
2437
2438 mod config_chaining_tests {
2439 use super::*;
2440
2441 #[test]
2443 fn test_full_config_builder_chain() {
2444 let config = VideoConfig::new(640, 480)
2445 .with_fps(24)
2446 .with_bitrate(2000)
2447 .with_codec(VideoCodec::Mjpeg)
2448 .with_max_duration(120)
2449 .with_jpeg_quality(75);
2450
2451 assert_eq!(config.width, 640);
2452 assert_eq!(config.height, 480);
2453 assert_eq!(config.fps, 24);
2454 assert_eq!(config.bitrate, 2000);
2455 assert_eq!(config.codec, VideoCodec::Mjpeg);
2456 assert_eq!(config.max_duration_secs, 120);
2457 assert_eq!(config.jpeg_quality, 75);
2458 }
2459 }
2460
2461 mod encoded_frame_edge_cases {
2462 use super::*;
2463
2464 #[test]
2466 fn test_encoded_frame_empty_data() {
2467 let frame = EncodedFrame {
2468 data: Vec::new(),
2469 timestamp_ms: 0,
2470 duration_ms: 33,
2471 };
2472 assert!(frame.data.is_empty());
2473 }
2474
2475 #[test]
2477 fn test_encoded_frame_large_timestamp() {
2478 let frame = EncodedFrame {
2479 data: vec![1],
2480 timestamp_ms: u64::MAX,
2481 duration_ms: 0,
2482 };
2483 assert_eq!(frame.timestamp_ms, u64::MAX);
2484 }
2485 }
2486
2487 mod screenshot_with_resize_tests {
2488 use super::*;
2489
2490 #[test]
2492 fn test_screenshot_resize_to_larger() {
2493 use crate::driver::Screenshot;
2494 use std::time::SystemTime;
2495
2496 let config = VideoConfig::new(100, 100).with_fps(1);
2498 let mut recorder = VideoRecorder::new(config);
2499
2500 recorder.start().unwrap();
2501
2502 let data = vec![200u8; (10 * 10 * 4) as usize];
2504 let img = image::RgbaImage::from_raw(10, 10, data).unwrap();
2505 let mut buffer = std::io::Cursor::new(Vec::new());
2506 image::DynamicImage::ImageRgba8(img)
2507 .write_to(&mut buffer, image::ImageFormat::Png)
2508 .unwrap();
2509
2510 let screenshot = Screenshot {
2511 data: buffer.into_inner(),
2512 width: 10,
2513 height: 10,
2514 device_pixel_ratio: 1.0,
2515 timestamp: SystemTime::now(),
2516 };
2517
2518 recorder.capture_frame(&screenshot).unwrap();
2520 assert_eq!(recorder.frame_count(), 1);
2521 }
2522 }
2523}