diff --git a/lightningbeam-ui/lightningbeam-core/src/video.rs b/lightningbeam-ui/lightningbeam-core/src/video.rs index 555dcdb..444bd88 100644 --- a/lightningbeam-ui/lightningbeam-core/src/video.rs +++ b/lightningbeam-ui/lightningbeam-core/src/video.rs @@ -769,6 +769,26 @@ pub fn extract_audio_from_video(path: &str) -> Result, St // Extract f32 samples (interleaved format) let data_ptr = resampled_frame.data(0).as_ptr() as *const f32; let total_samples = resampled_frame.samples() * frame_channels; + + // Safety checks before creating slice from FFmpeg data + // 1. Verify f32 alignment (required: 4 bytes) + if data_ptr.align_offset(std::mem::align_of::()) != 0 { + return Err("FFmpeg audio data is not properly aligned for f32".to_string()); + } + + // 2. Verify the frame actually has enough data + let byte_size = resampled_frame.data(0).len(); + let expected_bytes = total_samples * std::mem::size_of::(); + if byte_size < expected_bytes { + return Err(format!( + "FFmpeg frame buffer too small: {} bytes, need {} bytes", + byte_size, expected_bytes + )); + } + + // SAFETY: We verified alignment and bounds above. + // The slice lifetime is tied to resampled_frame which lives until + // after extend_from_slice completes. let samples_slice = unsafe { std::slice::from_raw_parts(data_ptr, total_samples) }; @@ -800,6 +820,26 @@ pub fn extract_audio_from_video(path: &str) -> Result, St let data_ptr = resampled_frame.data(0).as_ptr() as *const f32; let total_samples = resampled_frame.samples() * frame_channels; + + // Safety checks before creating slice from FFmpeg data + // 1. Verify f32 alignment (required: 4 bytes) + if data_ptr.align_offset(std::mem::align_of::()) != 0 { + return Err("FFmpeg audio data is not properly aligned for f32".to_string()); + } + + // 2. Verify the frame actually has enough data + let byte_size = resampled_frame.data(0).len(); + let expected_bytes = total_samples * std::mem::size_of::(); + if byte_size < expected_bytes { + return Err(format!( + "FFmpeg frame buffer too small: {} bytes, need {} bytes", + byte_size, expected_bytes + )); + } + + // SAFETY: We verified alignment and bounds above. + // The slice lifetime is tied to resampled_frame which lives until + // after extend_from_slice completes. let samples_slice = unsafe { std::slice::from_raw_parts(data_ptr, total_samples) }; diff --git a/lightningbeam-ui/lightningbeam-editor/examples/ffmpeg_test.rs b/lightningbeam-ui/lightningbeam-editor/examples/ffmpeg_test.rs index 8eec46f..f8226b2 100644 --- a/lightningbeam-ui/lightningbeam-editor/examples/ffmpeg_test.rs +++ b/lightningbeam-ui/lightningbeam-editor/examples/ffmpeg_test.rs @@ -195,17 +195,16 @@ fn encode_pcm_to_mp3( frame.set_rate(sample_rate); // Copy planar samples to frame - unsafe { - for ch in 0..channels as usize { - let plane = frame.data_mut(ch); - let offset = samples_encoded; - let src = &planar_samples[ch][offset..offset + chunk_size]; + for ch in 0..channels as usize { + let plane = frame.data_mut(ch); + let offset = samples_encoded; + let src = &planar_samples[ch][offset..offset + chunk_size]; - std::ptr::copy_nonoverlapping( - src.as_ptr() as *const u8, - plane.as_mut_ptr(), - chunk_size * std::mem::size_of::(), - ); + // Safe byte-level copy + for (i, &sample) in src.iter().enumerate() { + let bytes = sample.to_ne_bytes(); + let byte_offset = i * 2; + plane[byte_offset..byte_offset + 2].copy_from_slice(&bytes); } } @@ -360,17 +359,16 @@ fn encode_pcm_to_aac( frame.set_rate(sample_rate); // Copy planar samples to frame - unsafe { - for ch in 0..channels as usize { - let plane = frame.data_mut(ch); - let offset = samples_encoded; - let src = &planar_samples[ch][offset..offset + chunk_size]; + for ch in 0..channels as usize { + let plane = frame.data_mut(ch); + let offset = samples_encoded; + let src = &planar_samples[ch][offset..offset + chunk_size]; - std::ptr::copy_nonoverlapping( - src.as_ptr() as *const u8, - plane.as_mut_ptr(), - chunk_size * std::mem::size_of::(), - ); + // Safe byte-level copy + for (i, &sample) in src.iter().enumerate() { + let bytes = sample.to_ne_bytes(); + let byte_offset = i * 4; + plane[byte_offset..byte_offset + 4].copy_from_slice(&bytes); } } diff --git a/lightningbeam-ui/lightningbeam-editor/examples/video_export_test.rs b/lightningbeam-ui/lightningbeam-editor/examples/video_export_test.rs index 3722894..c3f6de7 100644 --- a/lightningbeam-ui/lightningbeam-editor/examples/video_export_test.rs +++ b/lightningbeam-ui/lightningbeam-editor/examples/video_export_test.rs @@ -115,17 +115,18 @@ fn main() -> Result<(), String> { 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()); + // Copy YUV planes (safe slice copy) + let y_plane = video_frame.data_mut(0); + let y_len = y.len().min(y_plane.len()); + y_plane[..y_len].copy_from_slice(&y[..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 u_plane = video_frame.data_mut(1); + let u_len = u.len().min(u_plane.len()); + u_plane[..u_len].copy_from_slice(&u[..u_len]); - let v_plane = video_frame.data_mut(2); - std::ptr::copy_nonoverlapping(v.as_ptr(), v_plane.as_mut_ptr(), v.len()); - } + let v_plane = video_frame.data_mut(2); + let v_len = v.len().min(v_plane.len()); + v_plane[..v_len].copy_from_slice(&v[..v_len]); // Set PTS let timestamp = frame_num as f64 / framerate; diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/audio_exporter.rs b/lightningbeam-ui/lightningbeam-editor/src/export/audio_exporter.rs index b3b75a3..177e934 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/audio_exporter.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/audio_exporter.rs @@ -198,17 +198,25 @@ fn export_audio_ffmpeg_mp3>( frame.set_rate(settings.sample_rate); // Copy planar samples to frame - unsafe { - for ch in 0..settings.channels as usize { - let plane = frame.data_mut(ch); - let offset = samples_encoded; - let src = &planar_samples[ch][offset..offset + chunk_size]; + for ch in 0..settings.channels as usize { + let plane = frame.data_mut(ch); + let offset = samples_encoded; + let src = &planar_samples[ch][offset..offset + chunk_size]; - std::ptr::copy_nonoverlapping( - src.as_ptr() as *const u8, - plane.as_mut_ptr(), - chunk_size * std::mem::size_of::(), - ); + // Convert i16 samples to bytes and copy + let byte_size = chunk_size * std::mem::size_of::(); + if plane.len() < byte_size { + return Err(format!( + "FFmpeg frame buffer too small: {} bytes, need {} bytes", + plane.len(), byte_size + )); + } + + // Safe byte-level copy using slice operations + for (i, &sample) in src.iter().enumerate() { + let bytes = sample.to_ne_bytes(); + let offset = i * 2; + plane[offset..offset + 2].copy_from_slice(&bytes); } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs index 255d7d4..6364f76 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs @@ -1182,16 +1182,18 @@ impl ExportOrchestrator { ); // Copy YUV planes to frame - unsafe { - let y_dest = video_frame.data_mut(0); - std::ptr::copy_nonoverlapping(y_plane.as_ptr(), y_dest.as_mut_ptr(), y_plane.len()); + // Use safe slice copy - LLVM optimizes this to memcpy, same performance as copy_nonoverlapping + let y_dest = video_frame.data_mut(0); + let y_len = y_plane.len().min(y_dest.len()); + y_dest[..y_len].copy_from_slice(&y_plane[..y_len]); - let u_dest = video_frame.data_mut(1); - std::ptr::copy_nonoverlapping(u_plane.as_ptr(), u_dest.as_mut_ptr(), u_plane.len()); + let u_dest = video_frame.data_mut(1); + let u_len = u_plane.len().min(u_dest.len()); + u_dest[..u_len].copy_from_slice(&u_plane[..u_len]); - let v_dest = video_frame.data_mut(2); - std::ptr::copy_nonoverlapping(v_plane.as_ptr(), v_dest.as_mut_ptr(), v_plane.len()); - } + let v_dest = video_frame.data_mut(2); + let v_len = v_plane.len().min(v_dest.len()); + v_dest[..v_len].copy_from_slice(&v_plane[..v_len]); // Set PTS (presentation timestamp) in encoder's time base // Encoder time base is 1/(framerate * 1000), so PTS = timestamp * (framerate * 1000)