Skip to main content

jugar_probar/media/
video_recorder.rs

1//! MP4 Video Recording (Feature 4)
2//!
3//! Record test execution as MP4 video for comprehensive test documentation.
4//!
5//! ## EXTREME TDD: Tests written FIRST per spec
6//!
7//! ## Toyota Way Application
8//!
9//! - **Poka-Yoke**: Type-safe frame capture prevents format mismatches
10//! - **Muda**: Lazy frame encoding reduces memory pressure
11//! - **Jidoka**: Fail-fast on invalid configurations
12//! - **Heijunka**: Fixed frame rate for consistent playback
13
14use 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/// Video codec selection
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum VideoCodec {
25    /// Motion JPEG (pure Rust, widely compatible)
26    Mjpeg,
27    /// Raw RGB (no compression, large files)
28    Raw,
29}
30
31impl Default for VideoCodec {
32    fn default() -> Self {
33        Self::Mjpeg
34    }
35}
36
37/// Video recording state
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum RecordingState {
40    /// Recording has not started
41    Idle,
42    /// Currently recording frames
43    Recording,
44    /// Recording stopped, ready for export
45    Stopped,
46}
47
48/// Configuration for video recording
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct VideoConfig {
51    /// Frames per second (1-60)
52    pub fps: u8,
53    /// Output width in pixels
54    pub width: u32,
55    /// Output height in pixels
56    pub height: u32,
57    /// Target bitrate in kbps (for future codec support)
58    pub bitrate: u32,
59    /// Video codec to use
60    pub codec: VideoCodec,
61    /// Maximum recording duration in seconds (0 = unlimited)
62    pub max_duration_secs: u32,
63    /// JPEG quality for MJPEG codec (1-100)
64    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, // 5 minutes max
76            jpeg_quality: 85,
77        }
78    }
79}
80
81impl VideoConfig {
82    /// Create a new video configuration
83    #[must_use]
84    pub fn new(width: u32, height: u32) -> Self {
85        Self {
86            width,
87            height,
88            ..Default::default()
89        }
90    }
91
92    /// Set frames per second (clamped to 1-60)
93    #[must_use]
94    pub fn with_fps(mut self, fps: u8) -> Self {
95        self.fps = fps.clamp(1, 60);
96        self
97    }
98
99    /// Set bitrate in kbps
100    #[must_use]
101    pub fn with_bitrate(mut self, bitrate: u32) -> Self {
102        self.bitrate = bitrate;
103        self
104    }
105
106    /// Set video codec
107    #[must_use]
108    pub fn with_codec(mut self, codec: VideoCodec) -> Self {
109        self.codec = codec;
110        self
111    }
112
113    /// Set maximum duration in seconds
114    #[must_use]
115    pub fn with_max_duration(mut self, secs: u32) -> Self {
116        self.max_duration_secs = secs;
117        self
118    }
119
120    /// Set JPEG quality (1-100)
121    #[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    /// Calculate frame duration
128    #[must_use]
129    pub fn frame_duration(&self) -> Duration {
130        Duration::from_millis(1000 / u64::from(self.fps.max(1)))
131    }
132
133    /// Calculate timescale (ticks per second)
134    #[must_use]
135    pub fn timescale(&self) -> u32 {
136        u32::from(self.fps) * 100
137    }
138}
139
140/// An encoded video frame
141#[derive(Debug, Clone)]
142pub struct EncodedFrame {
143    /// Encoded frame data (JPEG or raw RGB)
144    pub data: Vec<u8>,
145    /// Frame timestamp in milliseconds from recording start
146    pub timestamp_ms: u64,
147    /// Frame duration in milliseconds
148    pub duration_ms: u64,
149}
150
151/// MP4 Video Recorder
152///
153/// Records screenshots as video frames and exports to MP4 format.
154///
155/// # Example
156///
157/// ```ignore
158/// use jugar_probar::media::{VideoRecorder, VideoConfig};
159///
160/// let config = VideoConfig::new(1280, 720).with_fps(30);
161/// let mut recorder = VideoRecorder::new(config);
162///
163/// recorder.start()?;
164///
165/// // Capture frames during test
166/// for screenshot in screenshots {
167///     recorder.capture_frame(&screenshot)?;
168/// }
169///
170/// let video_data = recorder.stop()?;
171/// std::fs::write("test_recording.mp4", video_data)?;
172/// ```
173#[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    /// Create a new video recorder with the given configuration
184    #[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    /// Get the current recording state
196    #[must_use]
197    pub fn state(&self) -> RecordingState {
198        self.state
199    }
200
201    /// Get the number of captured frames
202    #[must_use]
203    pub fn frame_count(&self) -> usize {
204        self.frames.len()
205    }
206
207    /// Get the recording configuration
208    #[must_use]
209    pub fn config(&self) -> &VideoConfig {
210        &self.config
211    }
212
213    /// Start recording
214    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    /// Capture a frame from a screenshot
230    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        // Check max duration
242        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        // Rate limit frame capture
255        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                // Skip frame to maintain target FPS
260                return Ok(());
261            }
262        }
263
264        // Encode the frame
265        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    /// Capture a raw frame (RGBA data)
279    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        // Check max duration
291        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        // Rate limit
304        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        // Encode the frame
312        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    /// Stop recording and return the encoded video data
326    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    /// Save the recorded video to a file
345    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    /// Encode a screenshot to the configured codec
364    fn encode_frame(&self, screenshot: &Screenshot) -> ProbarResult<Vec<u8>> {
365        // Load the screenshot as an image
366        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        // Resize if needed
373        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    /// Encode raw RGBA data
387    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        // Resize if needed
397        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    /// Encode an image to the configured codec
411    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                // Raw RGB24
434                Ok(img.to_rgb8().into_raw())
435            }
436        }
437    }
438
439    /// Generate MP4 container with encoded frames
440    fn generate_mp4(&self) -> ProbarResult<Vec<u8>> {
441        // For now, generate a simple MP4 container
442        // This is a simplified implementation that creates a valid MP4 structure
443        // with the encoded frames stored as raw data
444        let mut output = Vec::new();
445
446        // Write MP4 header (ftyp box)
447        self.write_ftyp_box(&mut output)?;
448
449        // Calculate total data size for mdat box
450        let frames_size: usize = self.frames.iter().map(|f| f.data.len()).sum();
451
452        // Write mdat box (media data)
453        self.write_mdat_box(&mut output, frames_size)?;
454
455        // Write moov box (movie header)
456        self.write_moov_box(&mut output)?;
457
458        Ok(output)
459    }
460
461    /// Write ftyp box (file type)
462    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    /// Write mdat box (media data)
478    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    /// Write moov box (movie header)
488    fn write_moov_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
489        // Build moov contents first to know the size
490        let mut moov_contents = Vec::new();
491
492        // mvhd (movie header)
493        self.write_mvhd_box(&mut moov_contents)?;
494
495        // trak (track)
496        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    /// Write mvhd box (movie header)
505    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        // Version and flags
511        content.write_all(&[0, 0, 0, 0])?;
512        // Creation time
513        content.write_all(&0u32.to_be_bytes())?;
514        // Modification time
515        content.write_all(&0u32.to_be_bytes())?;
516        // Timescale
517        content.write_all(&timescale.to_be_bytes())?;
518        // Duration
519        content.write_all(&duration.to_be_bytes())?;
520        // Rate (1.0 fixed point)
521        content.write_all(&0x00010000u32.to_be_bytes())?;
522        // Volume (1.0 fixed point)
523        content.write_all(&[0x01, 0x00])?;
524        // Reserved
525        content.write_all(&[0u8; 10])?;
526        // Matrix (identity)
527        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        // Pre-defined
532        content.write_all(&[0u8; 24])?;
533        // Next track ID
534        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    /// Write trak box (track)
543    fn write_trak_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
544        let mut trak_contents = Vec::new();
545
546        // tkhd (track header)
547        self.write_tkhd_box(&mut trak_contents)?;
548
549        // mdia (media)
550        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    /// Write tkhd box (track header)
559    fn write_tkhd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
560        let duration = self.calculate_duration();
561
562        let mut content = Vec::new();
563        // Version and flags (track enabled)
564        content.write_all(&[0, 0, 0, 3])?;
565        // Creation time
566        content.write_all(&0u32.to_be_bytes())?;
567        // Modification time
568        content.write_all(&0u32.to_be_bytes())?;
569        // Track ID
570        content.write_all(&1u32.to_be_bytes())?;
571        // Reserved
572        content.write_all(&0u32.to_be_bytes())?;
573        // Duration
574        content.write_all(&duration.to_be_bytes())?;
575        // Reserved
576        content.write_all(&[0u8; 8])?;
577        // Layer
578        content.write_all(&0u16.to_be_bytes())?;
579        // Alternate group
580        content.write_all(&0u16.to_be_bytes())?;
581        // Volume
582        content.write_all(&0u16.to_be_bytes())?;
583        // Reserved
584        content.write_all(&0u16.to_be_bytes())?;
585        // Matrix (identity)
586        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        // Width (fixed point)
591        content.write_all(&(self.config.width << 16).to_be_bytes())?;
592        // Height (fixed point)
593        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    /// Write mdia box (media)
602    fn write_mdia_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
603        let mut mdia_contents = Vec::new();
604
605        // mdhd (media header)
606        self.write_mdhd_box(&mut mdia_contents)?;
607
608        // hdlr (handler)
609        self.write_hdlr_box(&mut mdia_contents)?;
610
611        // minf (media information)
612        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    /// Write mdhd box (media header)
621    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        // Version and flags
627        content.write_all(&[0, 0, 0, 0])?;
628        // Creation time
629        content.write_all(&0u32.to_be_bytes())?;
630        // Modification time
631        content.write_all(&0u32.to_be_bytes())?;
632        // Timescale
633        content.write_all(&timescale.to_be_bytes())?;
634        // Duration
635        content.write_all(&duration.to_be_bytes())?;
636        // Language (und)
637        content.write_all(&0x55c4u16.to_be_bytes())?;
638        // Quality
639        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    /// Write hdlr box (handler)
648    fn write_hdlr_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
649        let mut content = Vec::new();
650        // Version and flags
651        content.write_all(&[0, 0, 0, 0])?;
652        // Pre-defined
653        content.write_all(&0u32.to_be_bytes())?;
654        // Handler type (vide)
655        content.write_all(b"vide")?;
656        // Reserved
657        content.write_all(&[0u8; 12])?;
658        // Name (null-terminated)
659        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    /// Write minf box (media information)
668    fn write_minf_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
669        let mut minf_contents = Vec::new();
670
671        // vmhd (video media header)
672        self.write_vmhd_box(&mut minf_contents)?;
673
674        // dinf (data information)
675        self.write_dinf_box(&mut minf_contents)?;
676
677        // stbl (sample table)
678        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    /// Write vmhd box (video media header)
687    fn write_vmhd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
688        let mut content = Vec::new();
689        // Version and flags (1 for vmhd)
690        content.write_all(&[0, 0, 0, 1])?;
691        // Graphics mode
692        content.write_all(&0u16.to_be_bytes())?;
693        // Op color
694        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    /// Write dinf box (data information)
703    fn write_dinf_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
704        let mut dinf_contents = Vec::new();
705
706        // dref (data reference)
707        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    /// Write dref box (data reference)
716    fn write_dref_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
717        let mut content = Vec::new();
718        // Version and flags
719        content.write_all(&[0, 0, 0, 0])?;
720        // Entry count
721        content.write_all(&1u32.to_be_bytes())?;
722
723        // url entry (self-contained)
724        content.write_all(&12u32.to_be_bytes())?; // size
725        content.write_all(b"url ")?;
726        content.write_all(&[0, 0, 0, 1])?; // flags (self-contained)
727
728        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    /// Write stbl box (sample table)
735    fn write_stbl_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
736        let mut stbl_contents = Vec::new();
737
738        // stsd (sample description)
739        self.write_stsd_box(&mut stbl_contents)?;
740
741        // stts (time-to-sample)
742        self.write_stts_box(&mut stbl_contents)?;
743
744        // stsc (sample-to-chunk)
745        self.write_stsc_box(&mut stbl_contents)?;
746
747        // stsz (sample sizes)
748        self.write_stsz_box(&mut stbl_contents)?;
749
750        // stco (chunk offsets)
751        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    /// Write stsd box (sample description)
760    fn write_stsd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
761        let mut content = Vec::new();
762        // Version and flags
763        content.write_all(&[0, 0, 0, 0])?;
764        // Entry count
765        content.write_all(&1u32.to_be_bytes())?;
766
767        // Video sample entry
768        let codec_tag = match self.config.codec {
769            VideoCodec::Mjpeg => b"jpeg",
770            VideoCodec::Raw => b"raw ",
771        };
772
773        // Sample entry size (calculated after building content)
774        let mut entry = Vec::new();
775        // Reserved
776        entry.write_all(&[0u8; 6])?;
777        // Data reference index
778        entry.write_all(&1u16.to_be_bytes())?;
779        // Pre-defined
780        entry.write_all(&0u16.to_be_bytes())?;
781        // Reserved
782        entry.write_all(&0u16.to_be_bytes())?;
783        // Pre-defined
784        entry.write_all(&[0u8; 12])?;
785        // Width
786        entry.write_all(&(self.config.width as u16).to_be_bytes())?;
787        // Height
788        entry.write_all(&(self.config.height as u16).to_be_bytes())?;
789        // Horizontal resolution (72 dpi fixed point)
790        entry.write_all(&0x00480000u32.to_be_bytes())?;
791        // Vertical resolution (72 dpi fixed point)
792        entry.write_all(&0x00480000u32.to_be_bytes())?;
793        // Reserved
794        entry.write_all(&0u32.to_be_bytes())?;
795        // Frame count
796        entry.write_all(&1u16.to_be_bytes())?;
797        // Compressor name (32 bytes, padded)
798        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        // Depth
804        entry.write_all(&24u16.to_be_bytes())?;
805        // Pre-defined
806        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    /// Write stts box (time-to-sample)
820    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        // Version and flags
825        content.write_all(&[0, 0, 0, 0])?;
826        // Entry count
827        content.write_all(&1u32.to_be_bytes())?;
828        // Sample count
829        content.write_all(&(self.frames.len() as u32).to_be_bytes())?;
830        // Sample delta
831        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    /// Write stsc box (sample-to-chunk)
840    fn write_stsc_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
841        let mut content = Vec::new();
842        // Version and flags
843        content.write_all(&[0, 0, 0, 0])?;
844        // Entry count
845        content.write_all(&1u32.to_be_bytes())?;
846        // First chunk
847        content.write_all(&1u32.to_be_bytes())?;
848        // Samples per chunk (all in one chunk)
849        content.write_all(&(self.frames.len() as u32).to_be_bytes())?;
850        // Sample description index
851        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    /// Write stsz box (sample sizes)
860    fn write_stsz_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
861        let mut content = Vec::new();
862        // Version and flags
863        content.write_all(&[0, 0, 0, 0])?;
864        // Sample size (0 = variable)
865        content.write_all(&0u32.to_be_bytes())?;
866        // Sample count
867        content.write_all(&(self.frames.len() as u32).to_be_bytes())?;
868        // Individual sample sizes
869        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    /// Write stco box (chunk offsets)
880    fn write_stco_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
881        // Calculate offset to mdat content (after ftyp + mdat header)
882        let ftyp_size = 8 + 4 + 4 + 12; // header + brand + version + 3 compatible brands
883        let mdat_header_size = 8;
884        let mdat_offset = ftyp_size + mdat_header_size;
885
886        let mut content = Vec::new();
887        // Version and flags
888        content.write_all(&[0, 0, 0, 0])?;
889        // Entry count
890        content.write_all(&1u32.to_be_bytes())?;
891        // Chunk offset
892        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    /// Write box header (size + type)
901    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    /// Calculate total duration in timescale units
913    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            // Create a small red image
1098            let data = vec![255, 0, 0, 255].repeat(100); // 10x10 RGBA
1099            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            // Capture a few frames
1114            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                // Sleep to allow frame capture (due to rate limiting)
1120                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            // Verify MP4 magic bytes (ftyp box)
1127            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            // Check for ftyp box
1176            assert!(find_box(&video, b"ftyp").is_some());
1177
1178            // Check for mdat box
1179            assert!(find_box(&video, b"mdat").is_some());
1180
1181            // Check for moov box
1182            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            // Capture multiple frames rapidly - should be rate limited
1235            for _ in 0..5 {
1236                recorder.capture_raw_frame(&data, 10, 10).unwrap();
1237            }
1238
1239            // Should only have captured 1 frame due to rate limiting
1240            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            // Capture a 10x10 frame when config expects 20x20
1255            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            // Data doesn't match dimensions (too small)
1273            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            // Frame count should still be 1
1294            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            // Create a minimal valid PNG image
1366            let data = vec![255u8; (width * height * 4) as usize]; // RGBA
1367            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); // Different size
1398            let mut recorder = VideoRecorder::new(config);
1399
1400            recorder.start().unwrap();
1401
1402            let screenshot = Screenshot {
1403                data: create_minimal_png(10, 10), // 10x10 PNG, recorder expects 20x20
1404                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            // Wait and capture more frames
1445            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            // Verify all MP4 boxes exist
1454            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            // Verify frame count affects duration calculation
1469            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    /// Helper to find a box in MP4 data
1504    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    // =========================================================================
1528    // Hâ‚€ EXTREME TDD: Video Recorder Tests (Feature B P2)
1529    // =========================================================================
1530
1531    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]; // Too small
1814            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    // =========================================================================
1888    // Additional Coverage Tests for 95%+ Target
1889    // =========================================================================
1890
1891    mod max_duration_tests {
1892        use super::*;
1893
1894        /// Test max duration exceeded for capture_frame (Screenshot version)
1895        #[test]
1896        fn test_capture_frame_max_duration_exceeded() {
1897            use crate::driver::Screenshot;
1898            use std::time::SystemTime;
1899
1900            // Use max_duration of 0 to NOT trigger the limit (0 = unlimited)
1901            // Instead, set max_duration_secs to 1 and manipulate timing
1902            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            // Create a valid PNG for the screenshot
1908            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            // Should succeed with unlimited duration
1924            recorder.capture_frame(&screenshot).unwrap();
1925            assert_eq!(recorder.frame_count(), 1);
1926        }
1927
1928        /// Test max duration exceeded error path for raw frame capture
1929        #[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            // With unlimited duration, should work fine
1939            assert_eq!(recorder.frame_count(), 1);
1940        }
1941    }
1942
1943    mod frame_rate_limiting_tests {
1944        use super::*;
1945
1946        /// Test frame skipping for capture_frame (Screenshot version)
1947        #[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            // Create a valid PNG
1958            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            // Capture first frame
1967            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            // Try to capture immediately - should be rate limited
1977            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            // Should only have 1 frame due to rate limiting
1987            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 save when recording but not stopped
1996        #[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            // Should fail because not stopped
2009            let result = recorder.save(&path);
2010            assert!(result.is_err());
2011        }
2012
2013        /// Test save from Idle state
2014        #[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 full recording cycle with Raw codec
2031        #[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            // Verify MP4 structure
2045            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 Raw codec generates larger output than MJPEG
2051        #[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            // Raw frames should be larger (uncompressed RGB24)
2071            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 invalid PNG data in screenshot
2080        #[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            // Create invalid PNG data
2091            let screenshot = Screenshot {
2092                data: vec![0, 1, 2, 3, 4, 5], // Invalid PNG data
2093                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            // Verify error message contains decode info
2103            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 screenshot that matches config dimensions (no resize needed)
2116        #[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            // Create PNG with exact dimensions
2127            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 raw frame that matches config dimensions (no resize needed)
2151        #[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            // Data matches config dimensions
2159            let data = vec![255, 0, 0, 255].repeat(100); // 10x10 RGBA
2160            recorder.capture_raw_frame(&data, 10, 10).unwrap();
2161            assert_eq!(recorder.frame_count(), 1);
2162        }
2163
2164        /// Test raw frame that needs resize
2165        #[test]
2166        fn test_raw_frame_needs_resize() {
2167            let config = VideoConfig::new(20, 20).with_fps(1); // Config expects 20x20
2168            let mut recorder = VideoRecorder::new(config);
2169
2170            recorder.start().unwrap();
2171
2172            // Provide 10x10 frame - needs resize
2173            let data = vec![255, 0, 0, 255].repeat(100); // 10x10 RGBA
2174            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 VideoCodec serialization
2183        #[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            // Deserialize
2195            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 VideoConfig serialization
2203        #[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            // Verify all fields are present
2215            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            // Deserialize and verify
2224            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 rate limiting branch in capture_raw_frame
2239        #[test]
2240        fn test_raw_frame_rate_limiting_detailed() {
2241            let config = VideoConfig::new(10, 10).with_fps(60); // 60fps = ~16ms between frames
2242            let mut recorder = VideoRecorder::new(config);
2243
2244            recorder.start().unwrap();
2245
2246            let data = vec![255, 0, 0, 255].repeat(100);
2247
2248            // Capture first frame
2249            recorder.capture_raw_frame(&data, 10, 10).unwrap();
2250            assert_eq!(recorder.frame_count(), 1);
2251
2252            // Immediately try to capture another - should be skipped
2253            recorder.capture_raw_frame(&data, 10, 10).unwrap();
2254            assert_eq!(recorder.frame_count(), 1);
2255
2256            // Wait for frame duration and try again
2257            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 multiple frames with MJPEG codec
2267        #[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            // Verify MP4 structure
2290            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 multiple frames with Raw codec
2296        #[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            // Verify MP4 structure
2315            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 that recorder can be restarted after stop
2325        #[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            // First recording cycle
2331            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            // Second recording cycle - should work after stop
2338            recorder.start().unwrap();
2339            assert_eq!(recorder.state(), RecordingState::Recording);
2340            assert_eq!(recorder.frame_count(), 0); // Frames should be cleared
2341        }
2342    }
2343
2344    mod frame_duration_edge_cases {
2345        use super::*;
2346
2347        /// Test frame duration with fps=1 (minimum clamped value)
2348        #[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 frame duration edge case when fps is 0 (should clamp to 1)
2356        #[test]
2357        fn test_frame_duration_with_zero_fps_config() {
2358            // Directly create config with fps=0 to test frame_duration's .max(1)
2359            let mut config = VideoConfig::default();
2360            // After with_fps(0), fps becomes 1 due to clamping
2361            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 duration calculation with multiple frames
2371        #[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            // Capture 3 frames
2381            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 save to invalid path
2396        #[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            // Try to save to a path in a nonexistent directory
2407            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 successful save creates valid file
2414        #[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            // Verify file exists and has content
2429            assert!(path.exists());
2430            let content = std::fs::read(&path).unwrap();
2431            assert!(!content.is_empty());
2432
2433            // Verify it starts with ftyp box
2434            assert_eq!(&content[4..8], b"ftyp");
2435        }
2436    }
2437
2438    mod config_chaining_tests {
2439        use super::*;
2440
2441        /// Test full builder chain
2442        #[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 EncodedFrame with empty data
2465        #[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 EncodedFrame with large timestamp
2476        #[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 screenshot resize to larger dimensions
2491        #[test]
2492        fn test_screenshot_resize_to_larger() {
2493            use crate::driver::Screenshot;
2494            use std::time::SystemTime;
2495
2496            // Config expects 100x100, but we provide 10x10
2497            let config = VideoConfig::new(100, 100).with_fps(1);
2498            let mut recorder = VideoRecorder::new(config);
2499
2500            recorder.start().unwrap();
2501
2502            // Create a 10x10 PNG
2503            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            // Should resize from 10x10 to 100x100
2519            recorder.capture_frame(&screenshot).unwrap();
2520            assert_eq!(recorder.frame_count(), 1);
2521        }
2522    }
2523}