/// Test program to validate video export with synthetic frames /// /// This creates a simple 5-second video with: /// - Red frame for 1 second /// - Green frame for 1 second /// - Blue frame for 1 second /// - White frame for 1 second /// - Black frame for 1 second /// /// Run with: cargo run --example video_export_test use std::path::Path; fn main() -> Result<(), String> { println!("Testing H.264 video export with synthetic frames...\n"); // Initialize FFmpeg ffmpeg_next::init().map_err(|e| format!("Failed to initialize FFmpeg: {}", e))?; // Output file let output_path = "/tmp/test_synthetic.mp4"; let width = 1920u32; let height = 1080u32; let framerate = 30.0; let bitrate_kbps = 5000; // 5 Mbps let duration_secs = 5.0; let total_frames = (duration_secs * framerate) as usize; println!("Settings:"); println!(" Resolution: {}×{}", width, height); println!(" Framerate: {} fps", framerate); println!(" Bitrate: {} kbps", bitrate_kbps); println!(" Duration: {} seconds ({} frames)", duration_secs, total_frames); println!(); // Find H.264 encoder let encoder_codec = ffmpeg_next::encoder::find(ffmpeg_next::codec::Id::H264) .ok_or("H.264 encoder not found")?; println!("Using encoder: {}", encoder_codec.name()); // Create output format context let mut output = ffmpeg_next::format::output(&output_path) .map_err(|e| format!("Failed to create output file: {}", e))?; // Create encoder from codec let mut encoder = ffmpeg_next::codec::Context::new_with_codec(encoder_codec) .encoder() .video() .map_err(|e| format!("Failed to create encoder: {}", e))?; // Configure encoder parameters BEFORE opening (like working MP3 code) encoder.set_width(width); encoder.set_height(height); encoder.set_format(ffmpeg_next::format::Pixel::YUV420P); encoder.set_time_base(ffmpeg_next::Rational(1, (framerate * 1000.0) as i32)); encoder.set_frame_rate(Some(ffmpeg_next::Rational(framerate as i32, 1))); encoder.set_bit_rate((bitrate_kbps * 1000) as usize); encoder.set_gop(framerate as u32); // 1 second GOP println!("Opening encoder with open_as()..."); // Open encoder with codec (like working MP3 code) let mut encoder = encoder .open_as(encoder_codec) .map_err(|e| format!("Failed to open encoder: {}", e))?; println!("✅ H.264 encoder opened successfully!"); println!("Opened encoder format: {:?}", encoder.format()); // Add stream AFTER opening encoder (like working MP3 code) { let mut stream = output .add_stream(encoder_codec) .map_err(|e| format!("Failed to add stream: {}", e))?; stream.set_parameters(&encoder); } output .write_header() .map_err(|e| format!("Failed to write header: {}", e))?; println!("✅ Output file created: {}", output_path); println!(); // Generate and encode frames println!("Encoding frames..."); let frame_size_rgba = (width * height * 4) as usize; let mut rgba_buffer = vec![0u8; frame_size_rgba]; for frame_num in 0..total_frames { // Fill RGBA buffer with color based on time let color = match frame_num / 30 { 0 => (255, 0, 0, 255), // Red (0-1s) 1 => (0, 255, 0, 255), // Green (1-2s) 2 => (0, 0, 255, 255), // Blue (2-3s) 3 => (255, 255, 255, 255), // White (3-4s) _ => (0, 0, 0, 255), // Black (4-5s) }; for pixel in rgba_buffer.chunks_mut(4) { pixel[0] = color.0; pixel[1] = color.1; pixel[2] = color.2; pixel[3] = color.3; } // Convert RGBA to YUV420p let (y, u, v) = rgba_to_yuv420p(&rgba_buffer, width, height); // Create video frame let mut video_frame = ffmpeg_next::frame::Video::new( ffmpeg_next::format::Pixel::YUV420P, width, height, ); // Copy YUV planes unsafe { let y_plane = video_frame.data_mut(0); std::ptr::copy_nonoverlapping(y.as_ptr(), y_plane.as_mut_ptr(), y.len()); let u_plane = video_frame.data_mut(1); std::ptr::copy_nonoverlapping(u.as_ptr(), u_plane.as_mut_ptr(), u.len()); let v_plane = video_frame.data_mut(2); std::ptr::copy_nonoverlapping(v.as_ptr(), v_plane.as_mut_ptr(), v.len()); } // Set PTS let timestamp = frame_num as f64 / framerate; video_frame.set_pts(Some((timestamp * 1000.0) as i64)); // Encode frame encoder .send_frame(&video_frame) .map_err(|e| format!("Failed to send frame: {}", e))?; // Receive and write packets let mut encoded = ffmpeg_next::Packet::empty(); while encoder.receive_packet(&mut encoded).is_ok() { encoded.set_stream(0); encoded .write_interleaved(&mut output) .map_err(|e| format!("Failed to write packet: {}", e))?; } // Progress indicator if (frame_num + 1) % 30 == 0 || frame_num + 1 == total_frames { let percent = ((frame_num + 1) as f64 / total_frames as f64 * 100.0) as u32; println!(" Frame {}/{} ({}%)", frame_num + 1, total_frames, percent); } } // Flush encoder encoder .send_eof() .map_err(|e| format!("Failed to send EOF: {}", e))?; let mut encoded = ffmpeg_next::Packet::empty(); while encoder.receive_packet(&mut encoded).is_ok() { encoded.set_stream(0); encoded .write_interleaved(&mut output) .map_err(|e| format!("Failed to write packet: {}", e))?; } output .write_trailer() .map_err(|e| format!("Failed to write trailer: {}", e))?; // Check output file if Path::new(output_path).exists() { let metadata = std::fs::metadata(output_path).unwrap(); println!(); println!("✅ Video export successful!"); println!(" Output: {} ({:.2} MB)", output_path, metadata.len() as f64 / 1_048_576.0); println!(); println!("Test with: ffplay {}", output_path); println!("Or: vlc {}", output_path); } else { return Err("Output file was not created!".to_string()); } Ok(()) } /// Convert RGBA8 to YUV420p using BT.709 color space fn rgba_to_yuv420p(rgba: &[u8], width: u32, height: u32) -> (Vec, Vec, Vec) { let w = width as usize; let h = height as usize; // Y plane (full resolution) let mut y_plane = Vec::with_capacity(w * h); for y in 0..h { for x in 0..w { let idx = (y * w + x) * 4; let r = rgba[idx] as f32; let g = rgba[idx + 1] as f32; let b = rgba[idx + 2] as f32; // BT.709 luma let y_val = (0.2126 * r + 0.7152 * g + 0.0722 * b).clamp(0.0, 255.0) as u8; y_plane.push(y_val); } } // U and V planes (quarter resolution) let mut u_plane = Vec::with_capacity((w * h) / 4); let mut v_plane = Vec::with_capacity((w * h) / 4); for y in (0..h).step_by(2) { for x in (0..w).step_by(2) { let mut r_sum = 0.0; let mut g_sum = 0.0; let mut b_sum = 0.0; for dy in 0..2 { for dx in 0..2 { if y + dy < h && x + dx < w { let idx = ((y + dy) * w + (x + dx)) * 4; r_sum += rgba[idx] as f32; g_sum += rgba[idx + 1] as f32; b_sum += rgba[idx + 2] as f32; } } } let r = r_sum / 4.0; let g = g_sum / 4.0; let b = b_sum / 4.0; // BT.709 chroma (centered at 128) let u_val = (-0.1146 * r - 0.3854 * g + 0.5000 * b + 128.0).clamp(0.0, 255.0) as u8; let v_val = (0.5000 * r - 0.4542 * g - 0.0458 * b + 128.0).clamp(0.0, 255.0) as u8; u_plane.push(u_val); v_plane.push(v_val); } } (y_plane, u_plane, v_plane) }