-
Notifications
You must be signed in to change notification settings - Fork 1.1k
wip: Optimize macOS recording pipeline with M4S muxer and async finalization (+ optimisations) #1464
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
wip: Optimize macOS recording pipeline with M4S muxer and async finalization (+ optimisations) #1464
Changes from all commits
2663374
636f5b3
b7011d8
a122436
84cba3c
b33d74c
04066dd
3bfdbbc
9b03f4e
94ad34c
ef81bbc
4ff10db
cf96969
9232b71
89bb3a1
c44ad76
cdb9b15
962df11
c79f8de
c1c98f1
5fb8232
8df37a9
f32f3a2
b15ae5d
9ecd43e
e4517f1
da6841b
e9809d1
1f1fb45
d7edd42
4a07659
bbd80f3
599c380
9285e02
3ff5712
340f7f8
d43f7e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,15 @@ | ||||||||||||||||||||||||||||||||
| use crate::editor_window::WindowEditorInstance; | ||||||||||||||||||||||||||||||||
| use crate::{FramesRendered, get_video_metadata}; | ||||||||||||||||||||||||||||||||
| use cap_export::ExporterBase; | ||||||||||||||||||||||||||||||||
| use cap_project::RecordingMeta; | ||||||||||||||||||||||||||||||||
| use serde::Deserialize; | ||||||||||||||||||||||||||||||||
| use cap_project::{RecordingMeta, XY}; | ||||||||||||||||||||||||||||||||
| use cap_rendering::{ | ||||||||||||||||||||||||||||||||
| FrameRenderer, ProjectRecordingsMeta, ProjectUniforms, RenderSegment, RenderVideoConstants, | ||||||||||||||||||||||||||||||||
| RendererLayers, | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| use image::codecs::jpeg::JpegEncoder; | ||||||||||||||||||||||||||||||||
| use serde::{Deserialize, Serialize}; | ||||||||||||||||||||||||||||||||
| use specta::Type; | ||||||||||||||||||||||||||||||||
| use std::path::PathBuf; | ||||||||||||||||||||||||||||||||
| use std::{path::PathBuf, sync::Arc}; | ||||||||||||||||||||||||||||||||
| use tracing::{info, instrument}; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| #[derive(Deserialize, Clone, Copy, Debug, Type)] | ||||||||||||||||||||||||||||||||
|
|
@@ -164,3 +170,281 @@ pub async fn get_export_estimates( | |||||||||||||||||||||||||||||||
| estimated_size_mb, | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| #[derive(Debug, Deserialize, Type)] | ||||||||||||||||||||||||||||||||
| pub struct ExportPreviewSettings { | ||||||||||||||||||||||||||||||||
| pub fps: u32, | ||||||||||||||||||||||||||||||||
| pub resolution_base: XY<u32>, | ||||||||||||||||||||||||||||||||
| pub compression_bpp: f32, | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| #[derive(Debug, Serialize, Type)] | ||||||||||||||||||||||||||||||||
| pub struct ExportPreviewResult { | ||||||||||||||||||||||||||||||||
| pub jpeg_base64: String, | ||||||||||||||||||||||||||||||||
| pub estimated_size_mb: f64, | ||||||||||||||||||||||||||||||||
| pub actual_width: u32, | ||||||||||||||||||||||||||||||||
| pub actual_height: u32, | ||||||||||||||||||||||||||||||||
| pub frame_render_time_ms: f64, | ||||||||||||||||||||||||||||||||
| pub total_frames: u32, | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| fn bpp_to_jpeg_quality(bpp: f32) -> u8 { | ||||||||||||||||||||||||||||||||
| ((bpp - 0.04) / (0.3 - 0.04) * (95.0 - 40.0) + 40.0).clamp(40.0, 95.0) as u8 | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| #[tauri::command] | ||||||||||||||||||||||||||||||||
| #[specta::specta] | ||||||||||||||||||||||||||||||||
| #[instrument(skip_all)] | ||||||||||||||||||||||||||||||||
| pub async fn generate_export_preview( | ||||||||||||||||||||||||||||||||
| project_path: PathBuf, | ||||||||||||||||||||||||||||||||
| frame_time: f64, | ||||||||||||||||||||||||||||||||
| settings: ExportPreviewSettings, | ||||||||||||||||||||||||||||||||
| ) -> Result<ExportPreviewResult, String> { | ||||||||||||||||||||||||||||||||
| use base64::{Engine, engine::general_purpose::STANDARD}; | ||||||||||||||||||||||||||||||||
| use cap_editor::create_segments; | ||||||||||||||||||||||||||||||||
| use std::time::Instant; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let recording_meta = RecordingMeta::load_for_project(&project_path) | ||||||||||||||||||||||||||||||||
| .map_err(|e| format!("Failed to load recording meta: {e}"))?; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let cap_project::RecordingMetaInner::Studio(studio_meta) = &recording_meta.inner else { | ||||||||||||||||||||||||||||||||
| return Err("Cannot preview non-studio recordings".to_string()); | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let project_config = recording_meta.project_config(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let recordings = Arc::new( | ||||||||||||||||||||||||||||||||
| ProjectRecordingsMeta::new(&recording_meta.project_path, studio_meta) | ||||||||||||||||||||||||||||||||
| .map_err(|e| format!("Failed to load recordings: {e}"))?, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let render_constants = Arc::new( | ||||||||||||||||||||||||||||||||
| RenderVideoConstants::new( | ||||||||||||||||||||||||||||||||
| &recordings.segments, | ||||||||||||||||||||||||||||||||
| recording_meta.clone(), | ||||||||||||||||||||||||||||||||
| studio_meta.clone(), | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||
| .map_err(|e| format!("Failed to create render constants: {e}"))?, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let segments = create_segments(&recording_meta, studio_meta) | ||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||
| .map_err(|e| format!("Failed to create segments: {e}"))?; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let render_segments: Vec<RenderSegment> = segments | ||||||||||||||||||||||||||||||||
| .iter() | ||||||||||||||||||||||||||||||||
| .map(|s| RenderSegment { | ||||||||||||||||||||||||||||||||
| cursor: s.cursor.clone(), | ||||||||||||||||||||||||||||||||
| decoders: s.decoders.clone(), | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| .collect(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else { | ||||||||||||||||||||||||||||||||
| return Err("Frame time is outside video duration".to_string()); | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let render_segment = &render_segments[segment.recording_clip as usize]; | ||||||||||||||||||||||||||||||||
| let clip_config = project_config | ||||||||||||||||||||||||||||||||
| .clips | ||||||||||||||||||||||||||||||||
| .iter() | ||||||||||||||||||||||||||||||||
| .find(|v| v.index == segment.recording_clip); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let render_start = Instant::now(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let segment_frames = render_segment | ||||||||||||||||||||||||||||||||
| .decoders | ||||||||||||||||||||||||||||||||
| .get_frames( | ||||||||||||||||||||||||||||||||
| segment_time as f32, | ||||||||||||||||||||||||||||||||
| !project_config.camera.hide, | ||||||||||||||||||||||||||||||||
| clip_config.map(|v| v.offsets).unwrap_or_default(), | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||
| .ok_or_else(|| "Failed to decode frame".to_string())?; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let frame_number = (frame_time * settings.fps as f64).floor() as u32; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let uniforms = ProjectUniforms::new( | ||||||||||||||||||||||||||||||||
| &render_constants, | ||||||||||||||||||||||||||||||||
| &project_config, | ||||||||||||||||||||||||||||||||
| frame_number, | ||||||||||||||||||||||||||||||||
| settings.fps, | ||||||||||||||||||||||||||||||||
| settings.resolution_base, | ||||||||||||||||||||||||||||||||
| &render_segment.cursor, | ||||||||||||||||||||||||||||||||
| &segment_frames, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let mut frame_renderer = FrameRenderer::new(&render_constants); | ||||||||||||||||||||||||||||||||
| let mut layers = RendererLayers::new_with_options( | ||||||||||||||||||||||||||||||||
| &render_constants.device, | ||||||||||||||||||||||||||||||||
| &render_constants.queue, | ||||||||||||||||||||||||||||||||
| render_constants.is_software_adapter, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let frame = frame_renderer | ||||||||||||||||||||||||||||||||
| .render( | ||||||||||||||||||||||||||||||||
| segment_frames, | ||||||||||||||||||||||||||||||||
| uniforms, | ||||||||||||||||||||||||||||||||
| &render_segment.cursor, | ||||||||||||||||||||||||||||||||
| &mut layers, | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||
| .map_err(|e| format!("Failed to render frame: {e}"))?; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let frame_render_time_ms = render_start.elapsed().as_secs_f64() * 1000.0; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let width = frame.width; | ||||||||||||||||||||||||||||||||
| let height = frame.height; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let rgb_data: Vec<u8> = frame | ||||||||||||||||||||||||||||||||
| .data | ||||||||||||||||||||||||||||||||
| .chunks(frame.padded_bytes_per_row as usize) | ||||||||||||||||||||||||||||||||
| .flat_map(|row| { | ||||||||||||||||||||||||||||||||
| row[0..(frame.width * 4) as usize] | ||||||||||||||||||||||||||||||||
| .chunks(4) | ||||||||||||||||||||||||||||||||
| .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]]) | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| .collect(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let jpeg_quality = bpp_to_jpeg_quality(settings.compression_bpp); | ||||||||||||||||||||||||||||||||
| let mut jpeg_buffer = Vec::new(); | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, jpeg_quality); | ||||||||||||||||||||||||||||||||
| encoder | ||||||||||||||||||||||||||||||||
| .encode(&rgb_data, width, height, image::ExtendedColorType::Rgb8) | ||||||||||||||||||||||||||||||||
| .map_err(|e| format!("Failed to encode JPEG: {e}"))?; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+299
to
+316
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Extract duplicated RGB conversion and JPEG encoding into helper functions. The RGB data conversion (lines 299-307, 410-418) and JPEG encoding logic (lines 309-316, 420-427) are identical in both preview functions, violating DRY principles. This increases maintenance burden and the risk of inconsistent fixes. 🔎 Proposed helper functionsAdd these helper functions before fn frame_to_rgb(frame: &cap_rendering::FrameData) -> Vec<u8> {
frame
.data
.chunks(frame.padded_bytes_per_row as usize)
.flat_map(|row| {
row[0..(frame.width * 4) as usize]
.chunks(4)
.flat_map(|chunk| [chunk[0], chunk[1], chunk[2]])
})
.collect()
}
fn encode_jpeg_base64(rgb_data: &[u8], width: u32, height: u32, quality: u8) -> Result<String, String> {
use base64::{Engine, engine::general_purpose::STANDARD};
let mut jpeg_buffer = Vec::new();
{
let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, quality);
encoder
.encode(rgb_data, width, height, image::ExtendedColorType::Rgb8)
.map_err(|e| format!("Failed to encode JPEG: {e}"))?;
}
Ok(STANDARD.encode(&jpeg_buffer))
}Then replace the duplicated code with: -let rgb_data: Vec<u8> = frame
- .data
- .chunks(frame.padded_bytes_per_row as usize)
- .flat_map(|row| {
- row[0..(frame.width * 4) as usize]
- .chunks(4)
- .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]])
- })
- .collect();
+let rgb_data = frame_to_rgb(&frame);
let jpeg_quality = bpp_to_jpeg_quality(settings.compression_bpp);
-let mut jpeg_buffer = Vec::new();
-{
- let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, jpeg_quality);
- encoder
- .encode(&rgb_data, width, height, image::ExtendedColorType::Rgb8)
- .map_err(|e| format!("Failed to encode JPEG: {e}"))?;
-}
-
-let jpeg_base64 = STANDARD.encode(&jpeg_buffer);
+let jpeg_base64 = encode_jpeg_base64(&rgb_data, width, height, jpeg_quality)?;Also applies to: 410-427 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let jpeg_base64 = STANDARD.encode(&jpeg_buffer); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let total_pixels = (settings.resolution_base.x * settings.resolution_base.y) as f64; | ||||||||||||||||||||||||||||||||
| let fps_f64 = settings.fps as f64; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let metadata = get_video_metadata(project_path.clone()).await?; | ||||||||||||||||||||||||||||||||
| let duration_seconds = if let Some(timeline) = &project_config.timeline { | ||||||||||||||||||||||||||||||||
| timeline.segments.iter().map(|s| s.duration()).sum() | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| metadata.duration | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| let total_frames = (duration_seconds * fps_f64).ceil() as u32; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let video_bitrate = total_pixels * settings.compression_bpp as f64 * fps_f64; | ||||||||||||||||||||||||||||||||
| let audio_bitrate = 192_000.0; | ||||||||||||||||||||||||||||||||
| let total_bitrate = video_bitrate + audio_bitrate; | ||||||||||||||||||||||||||||||||
| let estimated_size_mb = (total_bitrate * duration_seconds) / (8.0 * 1024.0 * 1024.0); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Ok(ExportPreviewResult { | ||||||||||||||||||||||||||||||||
| jpeg_base64, | ||||||||||||||||||||||||||||||||
| estimated_size_mb, | ||||||||||||||||||||||||||||||||
| actual_width: width, | ||||||||||||||||||||||||||||||||
| actual_height: height, | ||||||||||||||||||||||||||||||||
| frame_render_time_ms, | ||||||||||||||||||||||||||||||||
| total_frames, | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| #[tauri::command] | ||||||||||||||||||||||||||||||||
| #[specta::specta] | ||||||||||||||||||||||||||||||||
| #[instrument(skip_all)] | ||||||||||||||||||||||||||||||||
| pub async fn generate_export_preview_fast( | ||||||||||||||||||||||||||||||||
| editor: WindowEditorInstance, | ||||||||||||||||||||||||||||||||
| frame_time: f64, | ||||||||||||||||||||||||||||||||
| settings: ExportPreviewSettings, | ||||||||||||||||||||||||||||||||
| ) -> Result<ExportPreviewResult, String> { | ||||||||||||||||||||||||||||||||
| use base64::{Engine, engine::general_purpose::STANDARD}; | ||||||||||||||||||||||||||||||||
| use std::time::Instant; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let project_config = editor.project_config.1.borrow().clone(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else { | ||||||||||||||||||||||||||||||||
| return Err("Frame time is outside video duration".to_string()); | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let segment_media = &editor.segment_medias[segment.recording_clip as usize]; | ||||||||||||||||||||||||||||||||
|
Comment on lines
+359
to
+363
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verify array bounds before indexing. Similar to the issue at line 247, the indexing 🔎 Proposed bounds check let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else {
return Err("Frame time is outside video duration".to_string());
};
+let clip_index = segment.recording_clip as usize;
+if clip_index >= editor.segment_medias.len() {
+ return Err(format!("Invalid clip index: {}", clip_index));
+}
+
-let segment_media = &editor.segment_medias[segment.recording_clip as usize];
+let segment_media = &editor.segment_medias[clip_index];📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| let clip_config = project_config | ||||||||||||||||||||||||||||||||
| .clips | ||||||||||||||||||||||||||||||||
| .iter() | ||||||||||||||||||||||||||||||||
| .find(|v| v.index == segment.recording_clip); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let render_start = Instant::now(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let segment_frames = segment_media | ||||||||||||||||||||||||||||||||
| .decoders | ||||||||||||||||||||||||||||||||
| .get_frames( | ||||||||||||||||||||||||||||||||
| segment_time as f32, | ||||||||||||||||||||||||||||||||
| !project_config.camera.hide, | ||||||||||||||||||||||||||||||||
| clip_config.map(|v| v.offsets).unwrap_or_default(), | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||
| .ok_or_else(|| "Failed to decode frame".to_string())?; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let frame_number = (frame_time * settings.fps as f64).floor() as u32; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let uniforms = ProjectUniforms::new( | ||||||||||||||||||||||||||||||||
| &editor.render_constants, | ||||||||||||||||||||||||||||||||
| &project_config, | ||||||||||||||||||||||||||||||||
| frame_number, | ||||||||||||||||||||||||||||||||
| settings.fps, | ||||||||||||||||||||||||||||||||
| settings.resolution_base, | ||||||||||||||||||||||||||||||||
| &segment_media.cursor, | ||||||||||||||||||||||||||||||||
| &segment_frames, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let mut frame_renderer = FrameRenderer::new(&editor.render_constants); | ||||||||||||||||||||||||||||||||
| let mut layers = RendererLayers::new_with_options( | ||||||||||||||||||||||||||||||||
| &editor.render_constants.device, | ||||||||||||||||||||||||||||||||
| &editor.render_constants.queue, | ||||||||||||||||||||||||||||||||
| editor.render_constants.is_software_adapter, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let frame = frame_renderer | ||||||||||||||||||||||||||||||||
| .render(segment_frames, uniforms, &segment_media.cursor, &mut layers) | ||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||
| .map_err(|e| format!("Failed to render frame: {e}"))?; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let frame_render_time_ms = render_start.elapsed().as_secs_f64() * 1000.0; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let width = frame.width; | ||||||||||||||||||||||||||||||||
| let height = frame.height; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let rgb_data: Vec<u8> = frame | ||||||||||||||||||||||||||||||||
| .data | ||||||||||||||||||||||||||||||||
| .chunks(frame.padded_bytes_per_row as usize) | ||||||||||||||||||||||||||||||||
| .flat_map(|row| { | ||||||||||||||||||||||||||||||||
| row[0..(frame.width * 4) as usize] | ||||||||||||||||||||||||||||||||
| .chunks(4) | ||||||||||||||||||||||||||||||||
| .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]]) | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| .collect(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let jpeg_quality = bpp_to_jpeg_quality(settings.compression_bpp); | ||||||||||||||||||||||||||||||||
| let mut jpeg_buffer = Vec::new(); | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, jpeg_quality); | ||||||||||||||||||||||||||||||||
| encoder | ||||||||||||||||||||||||||||||||
| .encode(&rgb_data, width, height, image::ExtendedColorType::Rgb8) | ||||||||||||||||||||||||||||||||
| .map_err(|e| format!("Failed to encode JPEG: {e}"))?; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let jpeg_base64 = STANDARD.encode(&jpeg_buffer); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let total_pixels = (settings.resolution_base.x * settings.resolution_base.y) as f64; | ||||||||||||||||||||||||||||||||
| let fps_f64 = settings.fps as f64; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let duration_seconds = editor.recordings.duration(); | ||||||||||||||||||||||||||||||||
| let total_frames = (duration_seconds * fps_f64).ceil() as u32; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let video_bitrate = total_pixels * settings.compression_bpp as f64 * fps_f64; | ||||||||||||||||||||||||||||||||
| let audio_bitrate = 192_000.0; | ||||||||||||||||||||||||||||||||
| let total_bitrate = video_bitrate + audio_bitrate; | ||||||||||||||||||||||||||||||||
| let estimated_size_mb = (total_bitrate * duration_seconds) / (8.0 * 1024.0 * 1024.0); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Ok(ExportPreviewResult { | ||||||||||||||||||||||||||||||||
| jpeg_base64, | ||||||||||||||||||||||||||||||||
| estimated_size_mb, | ||||||||||||||||||||||||||||||||
| actual_width: width, | ||||||||||||||||||||||||||||||||
| actual_height: height, | ||||||||||||||||||||||||||||||||
| frame_render_time_ms, | ||||||||||||||||||||||||||||||||
| total_frames, | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verify array bounds before indexing.
The indexing at line 247 (
&render_segments[segment.recording_clip as usize]) could panic ifsegment.recording_clipis out of bounds. Whileget_segment_timevalidates the segment exists, it doesn't guarantee the clip index is withinrender_segments.len().🔎 Proposed bounds check
let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else { return Err("Frame time is outside video duration".to_string()); }; +let clip_index = segment.recording_clip as usize; +if clip_index >= render_segments.len() { + return Err(format!("Invalid clip index: {}", clip_index)); +} + -let render_segment = &render_segments[segment.recording_clip as usize]; +let render_segment = &render_segments[clip_index];🤖 Prompt for AI Agents