|
| 1 | +use crate::editor_window::WindowEditorInstance; |
1 | 2 | use crate::{FramesRendered, get_video_metadata}; |
2 | 3 | use cap_export::ExporterBase; |
3 | | -use cap_project::RecordingMeta; |
4 | | -use serde::Deserialize; |
| 4 | +use cap_project::{RecordingMeta, XY}; |
| 5 | +use cap_rendering::{ |
| 6 | + FrameRenderer, ProjectRecordingsMeta, ProjectUniforms, RenderSegment, RenderVideoConstants, |
| 7 | + RendererLayers, |
| 8 | +}; |
| 9 | +use image::codecs::jpeg::JpegEncoder; |
| 10 | +use serde::{Deserialize, Serialize}; |
5 | 11 | use specta::Type; |
6 | | -use std::path::PathBuf; |
| 12 | +use std::{path::PathBuf, sync::Arc}; |
7 | 13 | use tracing::{info, instrument}; |
8 | 14 |
|
9 | 15 | #[derive(Deserialize, Clone, Copy, Debug, Type)] |
@@ -164,3 +170,281 @@ pub async fn get_export_estimates( |
164 | 170 | estimated_size_mb, |
165 | 171 | }) |
166 | 172 | } |
| 173 | + |
| 174 | +#[derive(Debug, Deserialize, Type)] |
| 175 | +pub struct ExportPreviewSettings { |
| 176 | + pub fps: u32, |
| 177 | + pub resolution_base: XY<u32>, |
| 178 | + pub compression_bpp: f32, |
| 179 | +} |
| 180 | + |
| 181 | +#[derive(Debug, Serialize, Type)] |
| 182 | +pub struct ExportPreviewResult { |
| 183 | + pub jpeg_base64: String, |
| 184 | + pub estimated_size_mb: f64, |
| 185 | + pub actual_width: u32, |
| 186 | + pub actual_height: u32, |
| 187 | + pub frame_render_time_ms: f64, |
| 188 | + pub total_frames: u32, |
| 189 | +} |
| 190 | + |
| 191 | +fn bpp_to_jpeg_quality(bpp: f32) -> u8 { |
| 192 | + ((bpp - 0.04) / (0.3 - 0.04) * (95.0 - 40.0) + 40.0).clamp(40.0, 95.0) as u8 |
| 193 | +} |
| 194 | + |
| 195 | +#[tauri::command] |
| 196 | +#[specta::specta] |
| 197 | +#[instrument(skip_all)] |
| 198 | +pub async fn generate_export_preview( |
| 199 | + project_path: PathBuf, |
| 200 | + frame_time: f64, |
| 201 | + settings: ExportPreviewSettings, |
| 202 | +) -> Result<ExportPreviewResult, String> { |
| 203 | + use base64::{Engine, engine::general_purpose::STANDARD}; |
| 204 | + use cap_editor::create_segments; |
| 205 | + use std::time::Instant; |
| 206 | + |
| 207 | + let recording_meta = RecordingMeta::load_for_project(&project_path) |
| 208 | + .map_err(|e| format!("Failed to load recording meta: {e}"))?; |
| 209 | + |
| 210 | + let cap_project::RecordingMetaInner::Studio(studio_meta) = &recording_meta.inner else { |
| 211 | + return Err("Cannot preview non-studio recordings".to_string()); |
| 212 | + }; |
| 213 | + |
| 214 | + let project_config = recording_meta.project_config(); |
| 215 | + |
| 216 | + let recordings = Arc::new( |
| 217 | + ProjectRecordingsMeta::new(&recording_meta.project_path, studio_meta) |
| 218 | + .map_err(|e| format!("Failed to load recordings: {e}"))?, |
| 219 | + ); |
| 220 | + |
| 221 | + let render_constants = Arc::new( |
| 222 | + RenderVideoConstants::new( |
| 223 | + &recordings.segments, |
| 224 | + recording_meta.clone(), |
| 225 | + studio_meta.clone(), |
| 226 | + ) |
| 227 | + .await |
| 228 | + .map_err(|e| format!("Failed to create render constants: {e}"))?, |
| 229 | + ); |
| 230 | + |
| 231 | + let segments = create_segments(&recording_meta, studio_meta) |
| 232 | + .await |
| 233 | + .map_err(|e| format!("Failed to create segments: {e}"))?; |
| 234 | + |
| 235 | + let render_segments: Vec<RenderSegment> = segments |
| 236 | + .iter() |
| 237 | + .map(|s| RenderSegment { |
| 238 | + cursor: s.cursor.clone(), |
| 239 | + decoders: s.decoders.clone(), |
| 240 | + }) |
| 241 | + .collect(); |
| 242 | + |
| 243 | + let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else { |
| 244 | + return Err("Frame time is outside video duration".to_string()); |
| 245 | + }; |
| 246 | + |
| 247 | + let render_segment = &render_segments[segment.recording_clip as usize]; |
| 248 | + let clip_config = project_config |
| 249 | + .clips |
| 250 | + .iter() |
| 251 | + .find(|v| v.index == segment.recording_clip); |
| 252 | + |
| 253 | + let render_start = Instant::now(); |
| 254 | + |
| 255 | + let segment_frames = render_segment |
| 256 | + .decoders |
| 257 | + .get_frames( |
| 258 | + segment_time as f32, |
| 259 | + !project_config.camera.hide, |
| 260 | + clip_config.map(|v| v.offsets).unwrap_or_default(), |
| 261 | + ) |
| 262 | + .await |
| 263 | + .ok_or_else(|| "Failed to decode frame".to_string())?; |
| 264 | + |
| 265 | + let frame_number = (frame_time * settings.fps as f64).floor() as u32; |
| 266 | + |
| 267 | + let uniforms = ProjectUniforms::new( |
| 268 | + &render_constants, |
| 269 | + &project_config, |
| 270 | + frame_number, |
| 271 | + settings.fps, |
| 272 | + settings.resolution_base, |
| 273 | + &render_segment.cursor, |
| 274 | + &segment_frames, |
| 275 | + ); |
| 276 | + |
| 277 | + let mut frame_renderer = FrameRenderer::new(&render_constants); |
| 278 | + let mut layers = RendererLayers::new_with_options( |
| 279 | + &render_constants.device, |
| 280 | + &render_constants.queue, |
| 281 | + render_constants.is_software_adapter, |
| 282 | + ); |
| 283 | + |
| 284 | + let frame = frame_renderer |
| 285 | + .render( |
| 286 | + segment_frames, |
| 287 | + uniforms, |
| 288 | + &render_segment.cursor, |
| 289 | + &mut layers, |
| 290 | + ) |
| 291 | + .await |
| 292 | + .map_err(|e| format!("Failed to render frame: {e}"))?; |
| 293 | + |
| 294 | + let frame_render_time_ms = render_start.elapsed().as_secs_f64() * 1000.0; |
| 295 | + |
| 296 | + let width = frame.width; |
| 297 | + let height = frame.height; |
| 298 | + |
| 299 | + let rgb_data: Vec<u8> = frame |
| 300 | + .data |
| 301 | + .chunks(frame.padded_bytes_per_row as usize) |
| 302 | + .flat_map(|row| { |
| 303 | + row[0..(frame.width * 4) as usize] |
| 304 | + .chunks(4) |
| 305 | + .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]]) |
| 306 | + }) |
| 307 | + .collect(); |
| 308 | + |
| 309 | + let jpeg_quality = bpp_to_jpeg_quality(settings.compression_bpp); |
| 310 | + let mut jpeg_buffer = Vec::new(); |
| 311 | + { |
| 312 | + let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, jpeg_quality); |
| 313 | + encoder |
| 314 | + .encode(&rgb_data, width, height, image::ExtendedColorType::Rgb8) |
| 315 | + .map_err(|e| format!("Failed to encode JPEG: {e}"))?; |
| 316 | + } |
| 317 | + |
| 318 | + let jpeg_base64 = STANDARD.encode(&jpeg_buffer); |
| 319 | + |
| 320 | + let total_pixels = (settings.resolution_base.x * settings.resolution_base.y) as f64; |
| 321 | + let fps_f64 = settings.fps as f64; |
| 322 | + |
| 323 | + let metadata = get_video_metadata(project_path.clone()).await?; |
| 324 | + let duration_seconds = if let Some(timeline) = &project_config.timeline { |
| 325 | + timeline.segments.iter().map(|s| s.duration()).sum() |
| 326 | + } else { |
| 327 | + metadata.duration |
| 328 | + }; |
| 329 | + let total_frames = (duration_seconds * fps_f64).ceil() as u32; |
| 330 | + |
| 331 | + let video_bitrate = total_pixels * settings.compression_bpp as f64 * fps_f64; |
| 332 | + let audio_bitrate = 192_000.0; |
| 333 | + let total_bitrate = video_bitrate + audio_bitrate; |
| 334 | + let estimated_size_mb = (total_bitrate * duration_seconds) / (8.0 * 1024.0 * 1024.0); |
| 335 | + |
| 336 | + Ok(ExportPreviewResult { |
| 337 | + jpeg_base64, |
| 338 | + estimated_size_mb, |
| 339 | + actual_width: width, |
| 340 | + actual_height: height, |
| 341 | + frame_render_time_ms, |
| 342 | + total_frames, |
| 343 | + }) |
| 344 | +} |
| 345 | + |
| 346 | +#[tauri::command] |
| 347 | +#[specta::specta] |
| 348 | +#[instrument(skip_all)] |
| 349 | +pub async fn generate_export_preview_fast( |
| 350 | + editor: WindowEditorInstance, |
| 351 | + frame_time: f64, |
| 352 | + settings: ExportPreviewSettings, |
| 353 | +) -> Result<ExportPreviewResult, String> { |
| 354 | + use base64::{Engine, engine::general_purpose::STANDARD}; |
| 355 | + use std::time::Instant; |
| 356 | + |
| 357 | + let project_config = editor.project_config.1.borrow().clone(); |
| 358 | + |
| 359 | + let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else { |
| 360 | + return Err("Frame time is outside video duration".to_string()); |
| 361 | + }; |
| 362 | + |
| 363 | + let segment_media = &editor.segment_medias[segment.recording_clip as usize]; |
| 364 | + let clip_config = project_config |
| 365 | + .clips |
| 366 | + .iter() |
| 367 | + .find(|v| v.index == segment.recording_clip); |
| 368 | + |
| 369 | + let render_start = Instant::now(); |
| 370 | + |
| 371 | + let segment_frames = segment_media |
| 372 | + .decoders |
| 373 | + .get_frames( |
| 374 | + segment_time as f32, |
| 375 | + !project_config.camera.hide, |
| 376 | + clip_config.map(|v| v.offsets).unwrap_or_default(), |
| 377 | + ) |
| 378 | + .await |
| 379 | + .ok_or_else(|| "Failed to decode frame".to_string())?; |
| 380 | + |
| 381 | + let frame_number = (frame_time * settings.fps as f64).floor() as u32; |
| 382 | + |
| 383 | + let uniforms = ProjectUniforms::new( |
| 384 | + &editor.render_constants, |
| 385 | + &project_config, |
| 386 | + frame_number, |
| 387 | + settings.fps, |
| 388 | + settings.resolution_base, |
| 389 | + &segment_media.cursor, |
| 390 | + &segment_frames, |
| 391 | + ); |
| 392 | + |
| 393 | + let mut frame_renderer = FrameRenderer::new(&editor.render_constants); |
| 394 | + let mut layers = RendererLayers::new_with_options( |
| 395 | + &editor.render_constants.device, |
| 396 | + &editor.render_constants.queue, |
| 397 | + editor.render_constants.is_software_adapter, |
| 398 | + ); |
| 399 | + |
| 400 | + let frame = frame_renderer |
| 401 | + .render(segment_frames, uniforms, &segment_media.cursor, &mut layers) |
| 402 | + .await |
| 403 | + .map_err(|e| format!("Failed to render frame: {e}"))?; |
| 404 | + |
| 405 | + let frame_render_time_ms = render_start.elapsed().as_secs_f64() * 1000.0; |
| 406 | + |
| 407 | + let width = frame.width; |
| 408 | + let height = frame.height; |
| 409 | + |
| 410 | + let rgb_data: Vec<u8> = frame |
| 411 | + .data |
| 412 | + .chunks(frame.padded_bytes_per_row as usize) |
| 413 | + .flat_map(|row| { |
| 414 | + row[0..(frame.width * 4) as usize] |
| 415 | + .chunks(4) |
| 416 | + .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]]) |
| 417 | + }) |
| 418 | + .collect(); |
| 419 | + |
| 420 | + let jpeg_quality = bpp_to_jpeg_quality(settings.compression_bpp); |
| 421 | + let mut jpeg_buffer = Vec::new(); |
| 422 | + { |
| 423 | + let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, jpeg_quality); |
| 424 | + encoder |
| 425 | + .encode(&rgb_data, width, height, image::ExtendedColorType::Rgb8) |
| 426 | + .map_err(|e| format!("Failed to encode JPEG: {e}"))?; |
| 427 | + } |
| 428 | + |
| 429 | + let jpeg_base64 = STANDARD.encode(&jpeg_buffer); |
| 430 | + |
| 431 | + let total_pixels = (settings.resolution_base.x * settings.resolution_base.y) as f64; |
| 432 | + let fps_f64 = settings.fps as f64; |
| 433 | + |
| 434 | + let duration_seconds = editor.recordings.duration(); |
| 435 | + let total_frames = (duration_seconds * fps_f64).ceil() as u32; |
| 436 | + |
| 437 | + let video_bitrate = total_pixels * settings.compression_bpp as f64 * fps_f64; |
| 438 | + let audio_bitrate = 192_000.0; |
| 439 | + let total_bitrate = video_bitrate + audio_bitrate; |
| 440 | + let estimated_size_mb = (total_bitrate * duration_seconds) / (8.0 * 1024.0 * 1024.0); |
| 441 | + |
| 442 | + Ok(ExportPreviewResult { |
| 443 | + jpeg_base64, |
| 444 | + estimated_size_mb, |
| 445 | + actual_width: width, |
| 446 | + actual_height: height, |
| 447 | + frame_render_time_ms, |
| 448 | + total_frames, |
| 449 | + }) |
| 450 | +} |
0 commit comments