use crate::driver::Screenshot;
use crate::result::{ProbarError, ProbarResult};
use image::{DynamicImage, ImageFormat};
use serde::{Deserialize, Serialize};
use std::io::{Cursor, Write};
use std::path::Path;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VideoCodec {
Mjpeg,
Raw,
}
impl Default for VideoCodec {
fn default() -> Self {
Self::Mjpeg
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecordingState {
Idle,
Recording,
Stopped,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoConfig {
pub fps: u8,
pub width: u32,
pub height: u32,
pub bitrate: u32,
pub codec: VideoCodec,
pub max_duration_secs: u32,
pub jpeg_quality: u8,
}
impl Default for VideoConfig {
fn default() -> Self {
Self {
fps: 30,
width: 1280,
height: 720,
bitrate: 5000,
codec: VideoCodec::Mjpeg,
max_duration_secs: 300, jpeg_quality: 85,
}
}
}
impl VideoConfig {
#[must_use]
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
..Default::default()
}
}
#[must_use]
pub fn with_fps(mut self, fps: u8) -> Self {
self.fps = fps.clamp(1, 60);
self
}
#[must_use]
pub fn with_bitrate(mut self, bitrate: u32) -> Self {
self.bitrate = bitrate;
self
}
#[must_use]
pub fn with_codec(mut self, codec: VideoCodec) -> Self {
self.codec = codec;
self
}
#[must_use]
pub fn with_max_duration(mut self, secs: u32) -> Self {
self.max_duration_secs = secs;
self
}
#[must_use]
pub fn with_jpeg_quality(mut self, quality: u8) -> Self {
self.jpeg_quality = quality.clamp(1, 100);
self
}
#[must_use]
pub fn frame_duration(&self) -> Duration {
Duration::from_millis(1000 / u64::from(self.fps.max(1)))
}
#[must_use]
pub fn timescale(&self) -> u32 {
u32::from(self.fps) * 100
}
}
#[derive(Debug, Clone)]
pub struct EncodedFrame {
pub data: Vec<u8>,
pub timestamp_ms: u64,
pub duration_ms: u64,
}
#[derive(Debug)]
pub struct VideoRecorder {
config: VideoConfig,
frames: Vec<EncodedFrame>,
state: RecordingState,
start_time: Option<Instant>,
last_frame_time: Option<Instant>,
}
impl VideoRecorder {
#[must_use]
pub fn new(config: VideoConfig) -> Self {
Self {
config,
frames: Vec::new(),
state: RecordingState::Idle,
start_time: None,
last_frame_time: None,
}
}
#[must_use]
pub fn state(&self) -> RecordingState {
self.state
}
#[must_use]
pub fn frame_count(&self) -> usize {
self.frames.len()
}
#[must_use]
pub fn config(&self) -> &VideoConfig {
&self.config
}
pub fn start(&mut self) -> ProbarResult<()> {
if self.state == RecordingState::Recording {
return Err(ProbarError::VideoRecording {
message: "Recording already in progress".to_string(),
});
}
self.frames.clear();
self.state = RecordingState::Recording;
self.start_time = Some(Instant::now());
self.last_frame_time = None;
Ok(())
}
pub fn capture_frame(&mut self, screenshot: &Screenshot) -> ProbarResult<()> {
if self.state != RecordingState::Recording {
return Err(ProbarError::VideoRecording {
message: "Recording not started".to_string(),
});
}
let start_time = self.start_time.ok_or_else(|| ProbarError::VideoRecording {
message: "Recording start time not set".to_string(),
})?;
let elapsed = start_time.elapsed();
if self.config.max_duration_secs > 0
&& elapsed.as_secs() > u64::from(self.config.max_duration_secs)
{
return Err(ProbarError::VideoRecording {
message: format!(
"Maximum recording duration of {} seconds exceeded",
self.config.max_duration_secs
),
});
}
let frame_duration = self.config.frame_duration();
if let Some(last_time) = self.last_frame_time {
let since_last = last_time.elapsed();
if since_last < frame_duration {
return Ok(());
}
}
let encoded = self.encode_frame(screenshot)?;
let timestamp_ms = elapsed.as_millis() as u64;
self.frames.push(EncodedFrame {
data: encoded,
timestamp_ms,
duration_ms: frame_duration.as_millis() as u64,
});
self.last_frame_time = Some(Instant::now());
Ok(())
}
pub fn capture_raw_frame(&mut self, data: &[u8], width: u32, height: u32) -> ProbarResult<()> {
if self.state != RecordingState::Recording {
return Err(ProbarError::VideoRecording {
message: "Recording not started".to_string(),
});
}
let start_time = self.start_time.ok_or_else(|| ProbarError::VideoRecording {
message: "Recording start time not set".to_string(),
})?;
let elapsed = start_time.elapsed();
if self.config.max_duration_secs > 0
&& elapsed.as_secs() > u64::from(self.config.max_duration_secs)
{
return Err(ProbarError::VideoRecording {
message: format!(
"Maximum recording duration of {} seconds exceeded",
self.config.max_duration_secs
),
});
}
let frame_duration = self.config.frame_duration();
if let Some(last_time) = self.last_frame_time {
if last_time.elapsed() < frame_duration {
return Ok(());
}
}
let encoded = self.encode_raw_frame(data, width, height)?;
let timestamp_ms = elapsed.as_millis() as u64;
self.frames.push(EncodedFrame {
data: encoded,
timestamp_ms,
duration_ms: frame_duration.as_millis() as u64,
});
self.last_frame_time = Some(Instant::now());
Ok(())
}
pub fn stop(&mut self) -> ProbarResult<Vec<u8>> {
if self.state != RecordingState::Recording {
return Err(ProbarError::VideoRecording {
message: "Recording not in progress".to_string(),
});
}
self.state = RecordingState::Stopped;
if self.frames.is_empty() {
return Err(ProbarError::VideoRecording {
message: "No frames captured".to_string(),
});
}
self.generate_mp4()
}
pub fn save(&self, path: &Path) -> ProbarResult<()> {
if self.state != RecordingState::Stopped {
return Err(ProbarError::VideoRecording {
message: "Recording must be stopped before saving".to_string(),
});
}
if self.frames.is_empty() {
return Err(ProbarError::VideoRecording {
message: "No frames to save".to_string(),
});
}
let video_data = self.generate_mp4()?;
std::fs::write(path, video_data)?;
Ok(())
}
fn encode_frame(&self, screenshot: &Screenshot) -> ProbarResult<Vec<u8>> {
let cursor = Cursor::new(&screenshot.data);
let img =
image::load(cursor, ImageFormat::Png).map_err(|e| ProbarError::VideoRecording {
message: format!("Failed to decode screenshot: {e}"),
})?;
let img = if img.width() != self.config.width || img.height() != self.config.height {
img.resize_exact(
self.config.width,
self.config.height,
image::imageops::FilterType::Lanczos3,
)
} else {
img
};
self.encode_image(&img)
}
fn encode_raw_frame(&self, data: &[u8], width: u32, height: u32) -> ProbarResult<Vec<u8>> {
let img = image::RgbaImage::from_raw(width, height, data.to_vec()).ok_or_else(|| {
ProbarError::VideoRecording {
message: "Invalid raw frame dimensions".to_string(),
}
})?;
let img = DynamicImage::ImageRgba8(img);
let img = if width != self.config.width || height != self.config.height {
img.resize_exact(
self.config.width,
self.config.height,
image::imageops::FilterType::Lanczos3,
)
} else {
img
};
self.encode_image(&img)
}
fn encode_image(&self, img: &DynamicImage) -> ProbarResult<Vec<u8>> {
match self.config.codec {
VideoCodec::Mjpeg => {
let rgb = img.to_rgb8();
let mut buffer = Cursor::new(Vec::new());
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(
&mut buffer,
self.config.jpeg_quality,
);
encoder
.encode(
rgb.as_raw(),
self.config.width,
self.config.height,
image::ExtendedColorType::Rgb8,
)
.map_err(|e| ProbarError::VideoRecording {
message: format!("JPEG encoding failed: {e}"),
})?;
Ok(buffer.into_inner())
}
VideoCodec::Raw => {
Ok(img.to_rgb8().into_raw())
}
}
}
fn generate_mp4(&self) -> ProbarResult<Vec<u8>> {
let mut output = Vec::new();
self.write_ftyp_box(&mut output)?;
let frames_size: usize = self.frames.iter().map(|f| f.data.len()).sum();
self.write_mdat_box(&mut output, frames_size)?;
self.write_moov_box(&mut output)?;
Ok(output)
}
fn write_ftyp_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let brand = b"isom";
let minor_version: u32 = 512;
let compatible_brands = [b"isom", b"iso2", b"mp41"];
let size = 8 + 4 + 4 + (compatible_brands.len() * 4);
self.write_box_header(out, size as u32, b"ftyp")?;
out.write_all(brand)?;
out.write_all(&minor_version.to_be_bytes())?;
for brand in &compatible_brands {
out.write_all(*brand)?;
}
Ok(())
}
fn write_mdat_box(&self, out: &mut Vec<u8>, data_size: usize) -> ProbarResult<()> {
let box_size = 8 + data_size;
self.write_box_header(out, box_size as u32, b"mdat")?;
for frame in &self.frames {
out.write_all(&frame.data)?;
}
Ok(())
}
fn write_moov_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut moov_contents = Vec::new();
self.write_mvhd_box(&mut moov_contents)?;
self.write_trak_box(&mut moov_contents)?;
let moov_size = 8 + moov_contents.len();
self.write_box_header(out, moov_size as u32, b"moov")?;
out.write_all(&moov_contents)?;
Ok(())
}
fn write_mvhd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let timescale = self.config.timescale();
let duration = self.calculate_duration();
let mut content = Vec::new();
content.write_all(&[0, 0, 0, 0])?;
content.write_all(&0u32.to_be_bytes())?;
content.write_all(&0u32.to_be_bytes())?;
content.write_all(×cale.to_be_bytes())?;
content.write_all(&duration.to_be_bytes())?;
content.write_all(&0x00010000u32.to_be_bytes())?;
content.write_all(&[0x01, 0x00])?;
content.write_all(&[0u8; 10])?;
let matrix: [u32; 9] = [0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000];
for val in &matrix {
content.write_all(&val.to_be_bytes())?;
}
content.write_all(&[0u8; 24])?;
content.write_all(&2u32.to_be_bytes())?;
let size = 8 + content.len();
self.write_box_header(out, size as u32, b"mvhd")?;
out.write_all(&content)?;
Ok(())
}
fn write_trak_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut trak_contents = Vec::new();
self.write_tkhd_box(&mut trak_contents)?;
self.write_mdia_box(&mut trak_contents)?;
let trak_size = 8 + trak_contents.len();
self.write_box_header(out, trak_size as u32, b"trak")?;
out.write_all(&trak_contents)?;
Ok(())
}
fn write_tkhd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let duration = self.calculate_duration();
let mut content = Vec::new();
content.write_all(&[0, 0, 0, 3])?;
content.write_all(&0u32.to_be_bytes())?;
content.write_all(&0u32.to_be_bytes())?;
content.write_all(&1u32.to_be_bytes())?;
content.write_all(&0u32.to_be_bytes())?;
content.write_all(&duration.to_be_bytes())?;
content.write_all(&[0u8; 8])?;
content.write_all(&0u16.to_be_bytes())?;
content.write_all(&0u16.to_be_bytes())?;
content.write_all(&0u16.to_be_bytes())?;
content.write_all(&0u16.to_be_bytes())?;
let matrix: [u32; 9] = [0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000];
for val in &matrix {
content.write_all(&val.to_be_bytes())?;
}
content.write_all(&(self.config.width << 16).to_be_bytes())?;
content.write_all(&(self.config.height << 16).to_be_bytes())?;
let size = 8 + content.len();
self.write_box_header(out, size as u32, b"tkhd")?;
out.write_all(&content)?;
Ok(())
}
fn write_mdia_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut mdia_contents = Vec::new();
self.write_mdhd_box(&mut mdia_contents)?;
self.write_hdlr_box(&mut mdia_contents)?;
self.write_minf_box(&mut mdia_contents)?;
let mdia_size = 8 + mdia_contents.len();
self.write_box_header(out, mdia_size as u32, b"mdia")?;
out.write_all(&mdia_contents)?;
Ok(())
}
fn write_mdhd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let timescale = self.config.timescale();
let duration = self.calculate_duration();
let mut content = Vec::new();
content.write_all(&[0, 0, 0, 0])?;
content.write_all(&0u32.to_be_bytes())?;
content.write_all(&0u32.to_be_bytes())?;
content.write_all(×cale.to_be_bytes())?;
content.write_all(&duration.to_be_bytes())?;
content.write_all(&0x55c4u16.to_be_bytes())?;
content.write_all(&0u16.to_be_bytes())?;
let size = 8 + content.len();
self.write_box_header(out, size as u32, b"mdhd")?;
out.write_all(&content)?;
Ok(())
}
fn write_hdlr_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut content = Vec::new();
content.write_all(&[0, 0, 0, 0])?;
content.write_all(&0u32.to_be_bytes())?;
content.write_all(b"vide")?;
content.write_all(&[0u8; 12])?;
content.write_all(b"Probar Video Handler\0")?;
let size = 8 + content.len();
self.write_box_header(out, size as u32, b"hdlr")?;
out.write_all(&content)?;
Ok(())
}
fn write_minf_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut minf_contents = Vec::new();
self.write_vmhd_box(&mut minf_contents)?;
self.write_dinf_box(&mut minf_contents)?;
self.write_stbl_box(&mut minf_contents)?;
let minf_size = 8 + minf_contents.len();
self.write_box_header(out, minf_size as u32, b"minf")?;
out.write_all(&minf_contents)?;
Ok(())
}
fn write_vmhd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut content = Vec::new();
content.write_all(&[0, 0, 0, 1])?;
content.write_all(&0u16.to_be_bytes())?;
content.write_all(&[0u8; 6])?;
let size = 8 + content.len();
self.write_box_header(out, size as u32, b"vmhd")?;
out.write_all(&content)?;
Ok(())
}
fn write_dinf_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut dinf_contents = Vec::new();
self.write_dref_box(&mut dinf_contents)?;
let dinf_size = 8 + dinf_contents.len();
self.write_box_header(out, dinf_size as u32, b"dinf")?;
out.write_all(&dinf_contents)?;
Ok(())
}
fn write_dref_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut content = Vec::new();
content.write_all(&[0, 0, 0, 0])?;
content.write_all(&1u32.to_be_bytes())?;
content.write_all(&12u32.to_be_bytes())?; content.write_all(b"url ")?;
content.write_all(&[0, 0, 0, 1])?;
let size = 8 + content.len();
self.write_box_header(out, size as u32, b"dref")?;
out.write_all(&content)?;
Ok(())
}
fn write_stbl_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut stbl_contents = Vec::new();
self.write_stsd_box(&mut stbl_contents)?;
self.write_stts_box(&mut stbl_contents)?;
self.write_stsc_box(&mut stbl_contents)?;
self.write_stsz_box(&mut stbl_contents)?;
self.write_stco_box(&mut stbl_contents)?;
let stbl_size = 8 + stbl_contents.len();
self.write_box_header(out, stbl_size as u32, b"stbl")?;
out.write_all(&stbl_contents)?;
Ok(())
}
fn write_stsd_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut content = Vec::new();
content.write_all(&[0, 0, 0, 0])?;
content.write_all(&1u32.to_be_bytes())?;
let codec_tag = match self.config.codec {
VideoCodec::Mjpeg => b"jpeg",
VideoCodec::Raw => b"raw ",
};
let mut entry = Vec::new();
entry.write_all(&[0u8; 6])?;
entry.write_all(&1u16.to_be_bytes())?;
entry.write_all(&0u16.to_be_bytes())?;
entry.write_all(&0u16.to_be_bytes())?;
entry.write_all(&[0u8; 12])?;
entry.write_all(&(self.config.width as u16).to_be_bytes())?;
entry.write_all(&(self.config.height as u16).to_be_bytes())?;
entry.write_all(&0x00480000u32.to_be_bytes())?;
entry.write_all(&0x00480000u32.to_be_bytes())?;
entry.write_all(&0u32.to_be_bytes())?;
entry.write_all(&1u16.to_be_bytes())?;
let mut compressor_name = [0u8; 32];
let name = b"Probar Video";
compressor_name[0] = name.len() as u8;
compressor_name[1..1 + name.len()].copy_from_slice(name);
entry.write_all(&compressor_name)?;
entry.write_all(&24u16.to_be_bytes())?;
entry.write_all(&(-1i16).to_be_bytes())?;
let entry_size = 8 + entry.len();
content.write_all(&(entry_size as u32).to_be_bytes())?;
content.write_all(codec_tag)?;
content.write_all(&entry)?;
let size = 8 + content.len();
self.write_box_header(out, size as u32, b"stsd")?;
out.write_all(&content)?;
Ok(())
}
fn write_stts_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let frame_duration_ticks = self.config.timescale() / u32::from(self.config.fps);
let mut content = Vec::new();
content.write_all(&[0, 0, 0, 0])?;
content.write_all(&1u32.to_be_bytes())?;
content.write_all(&(self.frames.len() as u32).to_be_bytes())?;
content.write_all(&frame_duration_ticks.to_be_bytes())?;
let size = 8 + content.len();
self.write_box_header(out, size as u32, b"stts")?;
out.write_all(&content)?;
Ok(())
}
fn write_stsc_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut content = Vec::new();
content.write_all(&[0, 0, 0, 0])?;
content.write_all(&1u32.to_be_bytes())?;
content.write_all(&1u32.to_be_bytes())?;
content.write_all(&(self.frames.len() as u32).to_be_bytes())?;
content.write_all(&1u32.to_be_bytes())?;
let size = 8 + content.len();
self.write_box_header(out, size as u32, b"stsc")?;
out.write_all(&content)?;
Ok(())
}
fn write_stsz_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let mut content = Vec::new();
content.write_all(&[0, 0, 0, 0])?;
content.write_all(&0u32.to_be_bytes())?;
content.write_all(&(self.frames.len() as u32).to_be_bytes())?;
for frame in &self.frames {
content.write_all(&(frame.data.len() as u32).to_be_bytes())?;
}
let size = 8 + content.len();
self.write_box_header(out, size as u32, b"stsz")?;
out.write_all(&content)?;
Ok(())
}
fn write_stco_box(&self, out: &mut Vec<u8>) -> ProbarResult<()> {
let ftyp_size = 8 + 4 + 4 + 12; let mdat_header_size = 8;
let mdat_offset = ftyp_size + mdat_header_size;
let mut content = Vec::new();
content.write_all(&[0, 0, 0, 0])?;
content.write_all(&1u32.to_be_bytes())?;
content.write_all(&(mdat_offset as u32).to_be_bytes())?;
let size = 8 + content.len();
self.write_box_header(out, size as u32, b"stco")?;
out.write_all(&content)?;
Ok(())
}
fn write_box_header(
&self,
out: &mut Vec<u8>,
size: u32,
box_type: &[u8; 4],
) -> ProbarResult<()> {
out.write_all(&size.to_be_bytes())?;
out.write_all(box_type)?;
Ok(())
}
fn calculate_duration(&self) -> u32 {
let frame_count = self.frames.len() as u32;
let frame_duration_ticks = self.config.timescale() / u32::from(self.config.fps);
frame_count * frame_duration_ticks
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
mod video_config_tests {
use super::*;
#[test]
fn test_default_config() {
let config = VideoConfig::default();
assert_eq!(config.fps, 30);
assert_eq!(config.width, 1280);
assert_eq!(config.height, 720);
assert_eq!(config.bitrate, 5000);
assert_eq!(config.codec, VideoCodec::Mjpeg);
assert_eq!(config.max_duration_secs, 300);
assert_eq!(config.jpeg_quality, 85);
}
#[test]
fn test_config_new() {
let config = VideoConfig::new(1920, 1080);
assert_eq!(config.width, 1920);
assert_eq!(config.height, 1080);
}
#[test]
fn test_config_builder() {
let config = VideoConfig::new(800, 600)
.with_fps(60)
.with_bitrate(10000)
.with_codec(VideoCodec::Raw)
.with_max_duration(600)
.with_jpeg_quality(95);
assert_eq!(config.fps, 60);
assert_eq!(config.bitrate, 10000);
assert_eq!(config.codec, VideoCodec::Raw);
assert_eq!(config.max_duration_secs, 600);
assert_eq!(config.jpeg_quality, 95);
}
#[test]
fn test_fps_clamping() {
let config = VideoConfig::default().with_fps(0);
assert_eq!(config.fps, 1);
let config = VideoConfig::default().with_fps(100);
assert_eq!(config.fps, 60);
}
#[test]
fn test_jpeg_quality_clamping() {
let config = VideoConfig::default().with_jpeg_quality(0);
assert_eq!(config.jpeg_quality, 1);
let config = VideoConfig::default().with_jpeg_quality(200);
assert_eq!(config.jpeg_quality, 100);
}
#[test]
fn test_frame_duration() {
let config = VideoConfig::default().with_fps(30);
let duration = config.frame_duration();
assert_eq!(duration.as_millis(), 33);
let config = VideoConfig::default().with_fps(60);
let duration = config.frame_duration();
assert_eq!(duration.as_millis(), 16);
}
#[test]
fn test_timescale() {
let config = VideoConfig::default().with_fps(30);
assert_eq!(config.timescale(), 3000);
let config = VideoConfig::default().with_fps(60);
assert_eq!(config.timescale(), 6000);
}
}
mod video_codec_tests {
use super::*;
#[test]
fn test_default_codec() {
let codec = VideoCodec::default();
assert_eq!(codec, VideoCodec::Mjpeg);
}
#[test]
fn test_codec_equality() {
assert_eq!(VideoCodec::Mjpeg, VideoCodec::Mjpeg);
assert_eq!(VideoCodec::Raw, VideoCodec::Raw);
assert_ne!(VideoCodec::Mjpeg, VideoCodec::Raw);
}
}
mod recording_state_tests {
use super::*;
#[test]
fn test_state_equality() {
assert_eq!(RecordingState::Idle, RecordingState::Idle);
assert_eq!(RecordingState::Recording, RecordingState::Recording);
assert_eq!(RecordingState::Stopped, RecordingState::Stopped);
assert_ne!(RecordingState::Idle, RecordingState::Recording);
}
}
mod video_recorder_tests {
use super::*;
#[test]
fn test_new_recorder() {
let config = VideoConfig::default();
let recorder = VideoRecorder::new(config);
assert_eq!(recorder.state(), RecordingState::Idle);
assert_eq!(recorder.frame_count(), 0);
}
#[test]
fn test_start_recording() {
let config = VideoConfig::default();
let mut recorder = VideoRecorder::new(config);
recorder.start().expect("Failed to start recording");
assert_eq!(recorder.state(), RecordingState::Recording);
}
#[test]
fn test_double_start_error() {
let config = VideoConfig::default();
let mut recorder = VideoRecorder::new(config);
recorder.start().expect("Failed to start recording");
let result = recorder.start();
assert!(result.is_err());
}
#[test]
fn test_capture_without_start_error() {
let config = VideoConfig::default();
let mut recorder = VideoRecorder::new(config);
let data = vec![255u8; 800 * 600 * 4];
let result = recorder.capture_raw_frame(&data, 800, 600);
assert!(result.is_err());
}
#[test]
fn test_stop_without_start_error() {
let config = VideoConfig::default();
let mut recorder = VideoRecorder::new(config);
let result = recorder.stop();
assert!(result.is_err());
}
#[test]
fn test_stop_without_frames_error() {
let config = VideoConfig::default();
let mut recorder = VideoRecorder::new(config);
recorder.start().expect("Failed to start recording");
let result = recorder.stop();
assert!(result.is_err());
}
#[test]
fn test_capture_raw_frame() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().expect("Failed to start recording");
let data = vec![255, 0, 0, 255].repeat(100); recorder
.capture_raw_frame(&data, 10, 10)
.expect("Failed to capture frame");
assert_eq!(recorder.frame_count(), 1);
}
#[test]
fn test_full_recording_cycle() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().expect("Failed to start recording");
for _ in 0..3 {
let data = vec![255, 0, 0, 255].repeat(100);
recorder
.capture_raw_frame(&data, 10, 10)
.expect("Failed to capture frame");
std::thread::sleep(std::time::Duration::from_millis(1100));
}
let video_data = recorder.stop().expect("Failed to stop recording");
assert!(!video_data.is_empty());
assert!(video_data.len() >= 8);
assert_eq!(&video_data[4..8], b"ftyp");
}
#[test]
fn test_config_accessor() {
let config = VideoConfig::new(1920, 1080).with_fps(60);
let recorder = VideoRecorder::new(config);
assert_eq!(recorder.config().width, 1920);
assert_eq!(recorder.config().height, 1080);
assert_eq!(recorder.config().fps, 60);
}
}
mod encoded_frame_tests {
use super::*;
#[test]
fn test_encoded_frame_creation() {
let frame = EncodedFrame {
data: vec![1, 2, 3, 4],
timestamp_ms: 100,
duration_ms: 33,
};
assert_eq!(frame.data.len(), 4);
assert_eq!(frame.timestamp_ms, 100);
assert_eq!(frame.duration_ms, 33);
}
}
mod mp4_generation_tests {
use super::*;
#[test]
fn test_mp4_has_correct_structure() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().expect("Failed to start");
let data = vec![255, 0, 0, 255].repeat(100);
recorder
.capture_raw_frame(&data, 10, 10)
.expect("Failed to capture");
let video = recorder.stop().expect("Failed to stop");
assert!(find_box(&video, b"ftyp").is_some());
assert!(find_box(&video, b"mdat").is_some());
assert!(find_box(&video, b"moov").is_some());
}
}
mod save_tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_save_without_stop_error() {
let config = VideoConfig::new(10, 10);
let recorder = VideoRecorder::new(config);
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test.mp4");
let result = recorder.save(&path);
assert!(result.is_err());
}
#[test]
fn test_save_after_stop() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1100));
recorder.capture_raw_frame(&data, 10, 10).unwrap();
recorder.stop().unwrap();
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test.mp4");
recorder.save(&path).unwrap();
assert!(path.exists());
let saved_data = std::fs::read(&path).unwrap();
assert!(!saved_data.is_empty());
}
}
mod frame_rate_tests {
use super::*;
#[test]
fn test_frame_skipping() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
for _ in 0..5 {
recorder.capture_raw_frame(&data, 10, 10).unwrap();
}
assert_eq!(recorder.frame_count(), 1);
}
}
mod resize_tests {
use super::*;
#[test]
fn test_resize_frame() {
let config = VideoConfig::new(20, 20).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
}
mod invalid_frame_tests {
use super::*;
#[test]
fn test_invalid_raw_frame_dimensions() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255u8; 10];
let result = recorder.capture_raw_frame(&data, 10, 10);
assert!(result.is_err());
}
}
mod codec_tests {
use super::*;
#[test]
fn test_raw_codec() {
let config = VideoConfig::new(10, 10)
.with_fps(1)
.with_codec(VideoCodec::Raw);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
#[test]
fn test_codec_debug() {
assert!(format!("{:?}", VideoCodec::Mjpeg).contains("Mjpeg"));
assert!(format!("{:?}", VideoCodec::Raw).contains("Raw"));
}
#[test]
fn test_codec_clone() {
let codec = VideoCodec::Mjpeg;
let cloned = codec;
assert_eq!(codec, cloned);
}
}
mod recording_state_debug {
use super::*;
#[test]
fn test_state_debug() {
assert!(format!("{:?}", RecordingState::Idle).contains("Idle"));
assert!(format!("{:?}", RecordingState::Recording).contains("Recording"));
assert!(format!("{:?}", RecordingState::Stopped).contains("Stopped"));
}
#[test]
fn test_state_clone() {
let state = RecordingState::Recording;
let cloned = state;
assert_eq!(state, cloned);
}
}
mod debug_tests {
use super::*;
#[test]
fn test_video_recorder_debug() {
let config = VideoConfig::new(10, 10);
let recorder = VideoRecorder::new(config);
let debug = format!("{:?}", recorder);
assert!(debug.contains("VideoRecorder"));
}
#[test]
fn test_video_config_debug() {
let config = VideoConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("VideoConfig"));
}
#[test]
fn test_encoded_frame_debug() {
let frame = EncodedFrame {
data: vec![1, 2, 3],
timestamp_ms: 100,
duration_ms: 33,
};
let debug = format!("{:?}", frame);
assert!(debug.contains("EncodedFrame"));
}
}
mod screenshot_tests {
use super::*;
use crate::driver::Screenshot;
use std::time::SystemTime;
fn create_minimal_png(width: u32, height: u32) -> Vec<u8> {
let data = vec![255u8; (width * height * 4) as usize]; let img = image::RgbaImage::from_raw(width, height, data).unwrap();
let mut buffer = std::io::Cursor::new(Vec::new());
image::DynamicImage::ImageRgba8(img)
.write_to(&mut buffer, image::ImageFormat::Png)
.unwrap();
buffer.into_inner()
}
#[test]
fn test_capture_frame_with_screenshot() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let screenshot = Screenshot {
data: create_minimal_png(10, 10),
width: 10,
height: 10,
device_pixel_ratio: 1.0,
timestamp: SystemTime::now(),
};
recorder.capture_frame(&screenshot).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
#[test]
fn test_capture_frame_resize() {
let config = VideoConfig::new(20, 20).with_fps(1); let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let screenshot = Screenshot {
data: create_minimal_png(10, 10), width: 10,
height: 10,
device_pixel_ratio: 1.0,
timestamp: SystemTime::now(),
};
recorder.capture_frame(&screenshot).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
#[test]
fn test_capture_frame_not_started() {
let config = VideoConfig::new(10, 10);
let mut recorder = VideoRecorder::new(config);
let screenshot = Screenshot {
data: create_minimal_png(10, 10),
width: 10,
height: 10,
device_pixel_ratio: 1.0,
timestamp: SystemTime::now(),
};
let result = recorder.capture_frame(&screenshot);
assert!(result.is_err());
}
}
mod mp4_box_tests {
use super::*;
#[test]
fn test_multiple_frames_mp4() {
let config = VideoConfig::new(10, 10).with_fps(30);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
std::thread::sleep(std::time::Duration::from_millis(40));
recorder.capture_raw_frame(&data, 10, 10).unwrap();
std::thread::sleep(std::time::Duration::from_millis(40));
recorder.capture_raw_frame(&data, 10, 10).unwrap();
let video = recorder.stop().unwrap();
assert!(find_box(&video, b"ftyp").is_some());
assert!(find_box(&video, b"mdat").is_some());
assert!(find_box(&video, b"moov").is_some());
}
#[test]
fn test_calculate_duration() {
let config = VideoConfig::new(10, 10).with_fps(30);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
}
mod config_clone_tests {
use super::*;
#[test]
fn test_video_config_clone() {
let config = VideoConfig::new(1920, 1080)
.with_fps(60)
.with_bitrate(10000);
let cloned = config.clone();
assert_eq!(config.width, cloned.width);
assert_eq!(config.height, cloned.height);
assert_eq!(config.fps, cloned.fps);
assert_eq!(config.bitrate, cloned.bitrate);
}
#[test]
fn test_encoded_frame_clone() {
let frame = EncodedFrame {
data: vec![1, 2, 3],
timestamp_ms: 100,
duration_ms: 33,
};
let cloned = frame.clone();
assert_eq!(frame.data, cloned.data);
assert_eq!(frame.timestamp_ms, cloned.timestamp_ms);
}
}
fn find_box(data: &[u8], box_type: &[u8; 4]) -> Option<usize> {
let mut offset = 0;
while offset + 8 <= data.len() {
let size = u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as usize;
if &data[offset + 4..offset + 8] == box_type {
return Some(offset);
}
if size == 0 {
break;
}
offset += size;
}
None
}
mod h0_video_config_tests {
use super::*;
#[test]
fn h0_video_01_config_default_fps() {
let config = VideoConfig::default();
assert_eq!(config.fps, 30);
}
#[test]
fn h0_video_02_config_default_width() {
let config = VideoConfig::default();
assert_eq!(config.width, 1280);
}
#[test]
fn h0_video_03_config_default_height() {
let config = VideoConfig::default();
assert_eq!(config.height, 720);
}
#[test]
fn h0_video_04_config_default_bitrate() {
let config = VideoConfig::default();
assert_eq!(config.bitrate, 5000);
}
#[test]
fn h0_video_05_config_default_codec() {
let config = VideoConfig::default();
assert_eq!(config.codec, VideoCodec::Mjpeg);
}
#[test]
fn h0_video_06_config_default_max_duration() {
let config = VideoConfig::default();
assert_eq!(config.max_duration_secs, 300);
}
#[test]
fn h0_video_07_config_default_jpeg_quality() {
let config = VideoConfig::default();
assert_eq!(config.jpeg_quality, 85);
}
#[test]
fn h0_video_08_config_new_dimensions() {
let config = VideoConfig::new(1920, 1080);
assert_eq!(config.width, 1920);
assert_eq!(config.height, 1080);
}
#[test]
fn h0_video_09_config_with_fps() {
let config = VideoConfig::default().with_fps(60);
assert_eq!(config.fps, 60);
}
#[test]
fn h0_video_10_config_fps_clamp_min() {
let config = VideoConfig::default().with_fps(0);
assert_eq!(config.fps, 1);
}
}
mod h0_video_config_builder_tests {
use super::*;
#[test]
fn h0_video_11_config_fps_clamp_max() {
let config = VideoConfig::default().with_fps(100);
assert_eq!(config.fps, 60);
}
#[test]
fn h0_video_12_config_with_bitrate() {
let config = VideoConfig::default().with_bitrate(10000);
assert_eq!(config.bitrate, 10000);
}
#[test]
fn h0_video_13_config_with_codec_raw() {
let config = VideoConfig::default().with_codec(VideoCodec::Raw);
assert_eq!(config.codec, VideoCodec::Raw);
}
#[test]
fn h0_video_14_config_with_max_duration() {
let config = VideoConfig::default().with_max_duration(600);
assert_eq!(config.max_duration_secs, 600);
}
#[test]
fn h0_video_15_config_with_jpeg_quality() {
let config = VideoConfig::default().with_jpeg_quality(95);
assert_eq!(config.jpeg_quality, 95);
}
#[test]
fn h0_video_16_config_jpeg_clamp_min() {
let config = VideoConfig::default().with_jpeg_quality(0);
assert_eq!(config.jpeg_quality, 1);
}
#[test]
fn h0_video_17_config_jpeg_clamp_max() {
let config = VideoConfig::default().with_jpeg_quality(200);
assert_eq!(config.jpeg_quality, 100);
}
#[test]
fn h0_video_18_config_frame_duration_30fps() {
let config = VideoConfig::default().with_fps(30);
assert_eq!(config.frame_duration().as_millis(), 33);
}
#[test]
fn h0_video_19_config_frame_duration_60fps() {
let config = VideoConfig::default().with_fps(60);
assert_eq!(config.frame_duration().as_millis(), 16);
}
#[test]
fn h0_video_20_config_timescale_30fps() {
let config = VideoConfig::default().with_fps(30);
assert_eq!(config.timescale(), 3000);
}
}
mod h0_video_codec_tests {
use super::*;
#[test]
fn h0_video_21_codec_default_mjpeg() {
assert_eq!(VideoCodec::default(), VideoCodec::Mjpeg);
}
#[test]
fn h0_video_22_codec_equality_mjpeg() {
assert_eq!(VideoCodec::Mjpeg, VideoCodec::Mjpeg);
}
#[test]
fn h0_video_23_codec_equality_raw() {
assert_eq!(VideoCodec::Raw, VideoCodec::Raw);
}
#[test]
fn h0_video_24_codec_inequality() {
assert_ne!(VideoCodec::Mjpeg, VideoCodec::Raw);
}
#[test]
fn h0_video_25_codec_debug_mjpeg() {
let debug = format!("{:?}", VideoCodec::Mjpeg);
assert!(debug.contains("Mjpeg"));
}
#[test]
fn h0_video_26_codec_debug_raw() {
let debug = format!("{:?}", VideoCodec::Raw);
assert!(debug.contains("Raw"));
}
#[test]
fn h0_video_27_codec_clone() {
let codec = VideoCodec::Mjpeg;
let cloned = codec;
assert_eq!(codec, cloned);
}
#[test]
fn h0_video_28_codec_copy() {
let codec = VideoCodec::Raw;
let copied: VideoCodec = codec;
assert_eq!(codec, copied);
}
}
mod h0_recording_state_tests {
use super::*;
#[test]
fn h0_video_29_state_idle() {
assert_eq!(RecordingState::Idle, RecordingState::Idle);
}
#[test]
fn h0_video_30_state_recording() {
assert_eq!(RecordingState::Recording, RecordingState::Recording);
}
#[test]
fn h0_video_31_state_stopped() {
assert_eq!(RecordingState::Stopped, RecordingState::Stopped);
}
#[test]
fn h0_video_32_state_inequality() {
assert_ne!(RecordingState::Idle, RecordingState::Recording);
assert_ne!(RecordingState::Recording, RecordingState::Stopped);
}
#[test]
fn h0_video_33_state_debug() {
assert!(format!("{:?}", RecordingState::Idle).contains("Idle"));
}
#[test]
fn h0_video_34_state_copy() {
let state = RecordingState::Recording;
let copied: RecordingState = state;
assert_eq!(state, copied);
}
}
mod h0_recorder_tests {
use super::*;
#[test]
fn h0_video_35_recorder_new_idle() {
let recorder = VideoRecorder::new(VideoConfig::default());
assert_eq!(recorder.state(), RecordingState::Idle);
}
#[test]
fn h0_video_36_recorder_new_no_frames() {
let recorder = VideoRecorder::new(VideoConfig::default());
assert_eq!(recorder.frame_count(), 0);
}
#[test]
fn h0_video_37_recorder_start_recording() {
let mut recorder = VideoRecorder::new(VideoConfig::default());
recorder.start().unwrap();
assert_eq!(recorder.state(), RecordingState::Recording);
}
#[test]
fn h0_video_38_recorder_double_start_error() {
let mut recorder = VideoRecorder::new(VideoConfig::default());
recorder.start().unwrap();
assert!(recorder.start().is_err());
}
#[test]
fn h0_video_39_recorder_capture_without_start() {
let mut recorder = VideoRecorder::new(VideoConfig::new(10, 10));
let data = vec![255u8; 400];
assert!(recorder.capture_raw_frame(&data, 10, 10).is_err());
}
#[test]
fn h0_video_40_recorder_stop_without_start() {
let mut recorder = VideoRecorder::new(VideoConfig::default());
assert!(recorder.stop().is_err());
}
}
mod h0_recorder_frame_tests {
use super::*;
#[test]
fn h0_video_41_recorder_capture_frame() {
let mut recorder = VideoRecorder::new(VideoConfig::new(10, 10).with_fps(1));
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
#[test]
fn h0_video_42_recorder_config_accessor() {
let config = VideoConfig::new(1920, 1080).with_fps(60);
let recorder = VideoRecorder::new(config);
assert_eq!(recorder.config().width, 1920);
}
#[test]
fn h0_video_43_recorder_invalid_dimensions() {
let mut recorder = VideoRecorder::new(VideoConfig::new(10, 10).with_fps(1));
recorder.start().unwrap();
let data = vec![255u8; 10]; assert!(recorder.capture_raw_frame(&data, 10, 10).is_err());
}
#[test]
fn h0_video_44_recorder_debug() {
let recorder = VideoRecorder::new(VideoConfig::default());
let debug = format!("{:?}", recorder);
assert!(debug.contains("VideoRecorder"));
}
}
mod h0_encoded_frame_tests {
use super::*;
#[test]
fn h0_video_45_frame_data() {
let frame = EncodedFrame {
data: vec![1, 2, 3],
timestamp_ms: 0,
duration_ms: 33,
};
assert_eq!(frame.data.len(), 3);
}
#[test]
fn h0_video_46_frame_timestamp() {
let frame = EncodedFrame {
data: vec![],
timestamp_ms: 100,
duration_ms: 33,
};
assert_eq!(frame.timestamp_ms, 100);
}
#[test]
fn h0_video_47_frame_duration() {
let frame = EncodedFrame {
data: vec![],
timestamp_ms: 0,
duration_ms: 16,
};
assert_eq!(frame.duration_ms, 16);
}
#[test]
fn h0_video_48_frame_clone() {
let frame = EncodedFrame {
data: vec![1, 2, 3],
timestamp_ms: 50,
duration_ms: 33,
};
let cloned = frame;
assert_eq!(cloned.data, vec![1, 2, 3]);
}
#[test]
fn h0_video_49_frame_debug() {
let frame = EncodedFrame {
data: vec![],
timestamp_ms: 0,
duration_ms: 33,
};
let debug = format!("{:?}", frame);
assert!(debug.contains("EncodedFrame"));
}
#[test]
fn h0_video_50_config_timescale_60fps() {
let config = VideoConfig::default().with_fps(60);
assert_eq!(config.timescale(), 6000);
}
}
mod max_duration_tests {
use super::*;
#[test]
fn test_capture_frame_max_duration_exceeded() {
use crate::driver::Screenshot;
use std::time::SystemTime;
let config = VideoConfig::new(10, 10).with_fps(1).with_max_duration(0);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255u8; (10 * 10 * 4) as usize];
let img = image::RgbaImage::from_raw(10, 10, data).unwrap();
let mut buffer = std::io::Cursor::new(Vec::new());
image::DynamicImage::ImageRgba8(img)
.write_to(&mut buffer, image::ImageFormat::Png)
.unwrap();
let screenshot = Screenshot {
data: buffer.into_inner(),
width: 10,
height: 10,
device_pixel_ratio: 1.0,
timestamp: SystemTime::now(),
};
recorder.capture_frame(&screenshot).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
#[test]
fn test_raw_frame_max_duration_zero_unlimited() {
let config = VideoConfig::new(10, 10).with_fps(1).with_max_duration(0);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
}
mod frame_rate_limiting_tests {
use super::*;
#[test]
fn test_capture_frame_rate_limiting() {
use crate::driver::Screenshot;
use std::time::SystemTime;
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255u8; (10 * 10 * 4) as usize];
let img = image::RgbaImage::from_raw(10, 10, data).unwrap();
let mut buffer = std::io::Cursor::new(Vec::new());
image::DynamicImage::ImageRgba8(img)
.write_to(&mut buffer, image::ImageFormat::Png)
.unwrap();
let png_data = buffer.into_inner();
let screenshot1 = Screenshot {
data: png_data.clone(),
width: 10,
height: 10,
device_pixel_ratio: 1.0,
timestamp: SystemTime::now(),
};
recorder.capture_frame(&screenshot1).unwrap();
let screenshot2 = Screenshot {
data: png_data,
width: 10,
height: 10,
device_pixel_ratio: 1.0,
timestamp: SystemTime::now(),
};
recorder.capture_frame(&screenshot2).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
}
mod save_edge_case_tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_save_while_recording_error() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test.mp4");
let result = recorder.save(&path);
assert!(result.is_err());
}
#[test]
fn test_save_from_idle_error() {
let config = VideoConfig::new(10, 10);
let recorder = VideoRecorder::new(config);
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test.mp4");
let result = recorder.save(&path);
assert!(result.is_err());
}
}
mod raw_codec_tests {
use super::*;
#[test]
fn test_raw_codec_full_cycle() {
let config = VideoConfig::new(10, 10)
.with_fps(1)
.with_codec(VideoCodec::Raw);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
let video = recorder.stop().unwrap();
assert!(find_box(&video, b"ftyp").is_some());
assert!(find_box(&video, b"mdat").is_some());
assert!(find_box(&video, b"moov").is_some());
}
#[test]
fn test_raw_codec_frame_encoding() {
let raw_config = VideoConfig::new(10, 10)
.with_fps(1)
.with_codec(VideoCodec::Raw);
let mjpeg_config = VideoConfig::new(10, 10)
.with_fps(1)
.with_codec(VideoCodec::Mjpeg);
let mut raw_recorder = VideoRecorder::new(raw_config);
let mut mjpeg_recorder = VideoRecorder::new(mjpeg_config);
raw_recorder.start().unwrap();
mjpeg_recorder.start().unwrap();
let data = vec![255, 128, 64, 255].repeat(100);
raw_recorder.capture_raw_frame(&data, 10, 10).unwrap();
mjpeg_recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(raw_recorder.frame_count(), 1);
assert_eq!(mjpeg_recorder.frame_count(), 1);
}
}
mod screenshot_error_tests {
use super::*;
#[test]
fn test_invalid_png_decode_error() {
use crate::driver::Screenshot;
use std::time::SystemTime;
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let screenshot = Screenshot {
data: vec![0, 1, 2, 3, 4, 5], width: 10,
height: 10,
device_pixel_ratio: 1.0,
timestamp: SystemTime::now(),
};
let result = recorder.capture_frame(&screenshot);
assert!(result.is_err());
if let Err(ProbarError::VideoRecording { message }) = result {
assert!(
message.contains("decode") || message.contains("Failed"),
"Error message should mention decode failure"
);
}
}
}
mod screenshot_same_size_tests {
use super::*;
#[test]
fn test_screenshot_no_resize_needed() {
use crate::driver::Screenshot;
use std::time::SystemTime;
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![128u8; (10 * 10 * 4) as usize];
let img = image::RgbaImage::from_raw(10, 10, data).unwrap();
let mut buffer = std::io::Cursor::new(Vec::new());
image::DynamicImage::ImageRgba8(img)
.write_to(&mut buffer, image::ImageFormat::Png)
.unwrap();
let screenshot = Screenshot {
data: buffer.into_inner(),
width: 10,
height: 10,
device_pixel_ratio: 1.0,
timestamp: SystemTime::now(),
};
recorder.capture_frame(&screenshot).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
}
mod raw_frame_same_size_tests {
use super::*;
#[test]
fn test_raw_frame_no_resize_needed() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100); recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
#[test]
fn test_raw_frame_needs_resize() {
let config = VideoConfig::new(20, 20).with_fps(1); let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100); recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
}
mod serialization_tests {
use super::*;
#[test]
fn test_codec_serialization() {
let mjpeg = VideoCodec::Mjpeg;
let raw = VideoCodec::Raw;
let mjpeg_json = serde_json::to_string(&mjpeg).unwrap();
let raw_json = serde_json::to_string(&raw).unwrap();
assert!(mjpeg_json.contains("Mjpeg"));
assert!(raw_json.contains("Raw"));
let mjpeg_back: VideoCodec = serde_json::from_str(&mjpeg_json).unwrap();
let raw_back: VideoCodec = serde_json::from_str(&raw_json).unwrap();
assert_eq!(mjpeg, mjpeg_back);
assert_eq!(raw, raw_back);
}
#[test]
fn test_config_serialization() {
let config = VideoConfig::new(1920, 1080)
.with_fps(60)
.with_bitrate(10000)
.with_codec(VideoCodec::Raw)
.with_max_duration(600)
.with_jpeg_quality(95);
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("1920"));
assert!(json.contains("1080"));
assert!(json.contains("60"));
assert!(json.contains("10000"));
assert!(json.contains("Raw"));
assert!(json.contains("600"));
assert!(json.contains("95"));
let config_back: VideoConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.width, config_back.width);
assert_eq!(config.height, config_back.height);
assert_eq!(config.fps, config_back.fps);
assert_eq!(config.bitrate, config_back.bitrate);
assert_eq!(config.codec, config_back.codec);
assert_eq!(config.max_duration_secs, config_back.max_duration_secs);
assert_eq!(config.jpeg_quality, config_back.jpeg_quality);
}
}
mod raw_frame_rate_limiting_tests {
use super::*;
#[test]
fn test_raw_frame_rate_limiting_detailed() {
let config = VideoConfig::new(10, 10).with_fps(60); let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(recorder.frame_count(), 1);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(recorder.frame_count(), 1);
std::thread::sleep(std::time::Duration::from_millis(20));
recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(recorder.frame_count(), 2);
}
}
mod multiple_frames_with_different_codecs {
use super::*;
#[test]
fn test_mjpeg_multiple_frames_mp4() {
let config = VideoConfig::new(10, 10)
.with_fps(60)
.with_codec(VideoCodec::Mjpeg);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
let data2 = vec![0, 255, 0, 255].repeat(100);
recorder.capture_raw_frame(&data2, 10, 10).unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
let data3 = vec![0, 0, 255, 255].repeat(100);
recorder.capture_raw_frame(&data3, 10, 10).unwrap();
let video = recorder.stop().unwrap();
assert!(find_box(&video, b"ftyp").is_some());
assert!(find_box(&video, b"mdat").is_some());
assert!(find_box(&video, b"moov").is_some());
}
#[test]
fn test_raw_multiple_frames_mp4() {
let config = VideoConfig::new(10, 10)
.with_fps(60)
.with_codec(VideoCodec::Raw);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
let data2 = vec![0, 255, 0, 255].repeat(100);
recorder.capture_raw_frame(&data2, 10, 10).unwrap();
let video = recorder.stop().unwrap();
assert!(find_box(&video, b"ftyp").is_some());
assert!(find_box(&video, b"mdat").is_some());
assert!(find_box(&video, b"moov").is_some());
}
}
mod start_after_stop_tests {
use super::*;
#[test]
fn test_restart_after_stop() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
let video1 = recorder.stop().unwrap();
assert!(!video1.is_empty());
recorder.start().unwrap();
assert_eq!(recorder.state(), RecordingState::Recording);
assert_eq!(recorder.frame_count(), 0); }
}
mod frame_duration_edge_cases {
use super::*;
#[test]
fn test_frame_duration_min_fps() {
let config = VideoConfig::default().with_fps(1);
let duration = config.frame_duration();
assert_eq!(duration.as_millis(), 1000);
}
#[test]
fn test_frame_duration_with_zero_fps_config() {
let mut config = VideoConfig::default();
config = config.with_fps(0);
assert_eq!(config.fps, 1);
assert_eq!(config.frame_duration().as_millis(), 1000);
}
}
mod calculate_duration_tests {
use super::*;
#[test]
fn test_duration_calculation_multiple_frames() {
let config = VideoConfig::new(10, 10).with_fps(30);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
std::thread::sleep(std::time::Duration::from_millis(40));
recorder.capture_raw_frame(&data, 10, 10).unwrap();
std::thread::sleep(std::time::Duration::from_millis(40));
recorder.capture_raw_frame(&data, 10, 10).unwrap();
assert_eq!(recorder.frame_count(), 3);
}
}
mod write_error_path_tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_save_to_nonexistent_directory() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
recorder.stop().unwrap();
let result = recorder.save(std::path::Path::new(
"/nonexistent/directory/that/does/not/exist/test.mp4",
));
assert!(result.is_err());
}
#[test]
fn test_save_creates_valid_mp4_file() {
let config = VideoConfig::new(10, 10).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![255, 0, 0, 255].repeat(100);
recorder.capture_raw_frame(&data, 10, 10).unwrap();
recorder.stop().unwrap();
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_video.mp4");
recorder.save(&path).unwrap();
assert!(path.exists());
let content = std::fs::read(&path).unwrap();
assert!(!content.is_empty());
assert_eq!(&content[4..8], b"ftyp");
}
}
mod config_chaining_tests {
use super::*;
#[test]
fn test_full_config_builder_chain() {
let config = VideoConfig::new(640, 480)
.with_fps(24)
.with_bitrate(2000)
.with_codec(VideoCodec::Mjpeg)
.with_max_duration(120)
.with_jpeg_quality(75);
assert_eq!(config.width, 640);
assert_eq!(config.height, 480);
assert_eq!(config.fps, 24);
assert_eq!(config.bitrate, 2000);
assert_eq!(config.codec, VideoCodec::Mjpeg);
assert_eq!(config.max_duration_secs, 120);
assert_eq!(config.jpeg_quality, 75);
}
}
mod encoded_frame_edge_cases {
use super::*;
#[test]
fn test_encoded_frame_empty_data() {
let frame = EncodedFrame {
data: Vec::new(),
timestamp_ms: 0,
duration_ms: 33,
};
assert!(frame.data.is_empty());
}
#[test]
fn test_encoded_frame_large_timestamp() {
let frame = EncodedFrame {
data: vec![1],
timestamp_ms: u64::MAX,
duration_ms: 0,
};
assert_eq!(frame.timestamp_ms, u64::MAX);
}
}
mod screenshot_with_resize_tests {
use super::*;
#[test]
fn test_screenshot_resize_to_larger() {
use crate::driver::Screenshot;
use std::time::SystemTime;
let config = VideoConfig::new(100, 100).with_fps(1);
let mut recorder = VideoRecorder::new(config);
recorder.start().unwrap();
let data = vec![200u8; (10 * 10 * 4) as usize];
let img = image::RgbaImage::from_raw(10, 10, data).unwrap();
let mut buffer = std::io::Cursor::new(Vec::new());
image::DynamicImage::ImageRgba8(img)
.write_to(&mut buffer, image::ImageFormat::Png)
.unwrap();
let screenshot = Screenshot {
data: buffer.into_inner(),
width: 10,
height: 10,
device_pixel_ratio: 1.0,
timestamp: SystemTime::now(),
};
recorder.capture_frame(&screenshot).unwrap();
assert_eq!(recorder.frame_count(), 1);
}
}
}