From fe92b384fd2e07196c89563fbf2e159172c4cc33 Mon Sep 17 00:00:00 2001 From: David Anyatonwu Date: Fri, 27 Dec 2024 22:19:15 +0100 Subject: [PATCH 1/2] fix(export): prevent export progress from freezing at 102% Signed-off-by: David Anyatonwu --- apps/desktop/src-tauri/src/export.rs | 8 +++++--- apps/desktop/src/routes/editor/Header.tsx | 16 ++++++++++++++-- apps/desktop/src/routes/recordings-overlay.tsx | 16 ++++++++++++++-- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-tauri/src/export.rs index fc022ef4..6c130094 100644 --- a/apps/desktop/src-tauri/src/export.rs +++ b/apps/desktop/src-tauri/src/export.rs @@ -21,8 +21,8 @@ pub async fn export_video( .await .unwrap(); - // 30 FPS (calculated for output video) - let total_frames = (duration * 30.0).round() as u32; + // Calculate total frames with ceiling to ensure we don't exceed 100% + let total_frames = ((duration * 30.0).ceil() as u32).max(1); let editor_instance = upsert_editor_instance(&app, video_id.clone()).await; @@ -41,9 +41,11 @@ pub async fn export_video( project, output_path.clone(), move |frame_index| { + // Ensure progress never exceeds total frames + let current_frame = (frame_index + 1).min(total_frames); progress .send(RenderProgress::FrameRendered { - current_frame: frame_index + 1, + current_frame, }) .ok(); }, diff --git a/apps/desktop/src/routes/editor/Header.tsx b/apps/desktop/src/routes/editor/Header.tsx index 2742f59b..f2b14cbf 100644 --- a/apps/desktop/src/routes/editor/Header.tsx +++ b/apps/desktop/src/routes/editor/Header.tsx @@ -260,14 +260,26 @@ function ExportButton() { const progress = new Channel(); progress.onmessage = (p) => { if (p.type === "FrameRendered" && progressState.type === "saving") { - const percentComplete = Math.round( - (p.current_frame / (progressState.totalFrames || 1)) * 100 + const percentComplete = Math.min( + Math.round( + (p.current_frame / (progressState.totalFrames || 1)) * 100 + ), + 100 ); + setProgressState({ ...progressState, renderProgress: p.current_frame, message: `Rendering video - ${percentComplete}%`, }); + + // If rendering is complete, update the message + if (percentComplete === 100) { + setProgressState({ + ...progressState, + message: "Finalizing export...", + }); + } } if ( p.type === "EstimatedTotalFrames" && diff --git a/apps/desktop/src/routes/recordings-overlay.tsx b/apps/desktop/src/routes/recordings-overlay.tsx index b0a6b2d6..83a1d636 100644 --- a/apps/desktop/src/routes/recordings-overlay.tsx +++ b/apps/desktop/src/routes/recordings-overlay.tsx @@ -375,14 +375,26 @@ export default function () { undefined && progressState.totalFrames ) { - return `${Math.min( + const progress = Math.min( Math.round( (progressState.renderProgress / progressState.totalFrames) * 100 ), 100 - )}%`; + ); + + // If we hit 100%, transition to the next stage + if (progress === 100 && progressState.type === "uploading") { + setProgressState({ + ...progressState, + stage: "uploading", + message: "Starting upload...", + uploadProgress: 0 + }); + } + + return `${progress}%`; } return progressState.message; From 173abf95cb630cd4833fa8091ea3ff0477e8d6fc Mon Sep 17 00:00:00 2001 From: David Anyatonwu Date: Sat, 28 Dec 2024 08:51:52 +0100 Subject: [PATCH 2/2] fix(export): check pro access before export and fix progress UI Signed-off-by: David Anyatonwu --- apps/desktop/src-tauri/src/export.rs | 36 +++++++++------ apps/desktop/src/routes/editor/Header.tsx | 56 +++++++++++++---------- 2 files changed, 54 insertions(+), 38 deletions(-) diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-tauri/src/export.rs index 6c130094..44468bd3 100644 --- a/apps/desktop/src-tauri/src/export.rs +++ b/apps/desktop/src-tauri/src/export.rs @@ -16,10 +16,15 @@ pub async fn export_video( force: bool, use_custom_muxer: bool, ) -> Result { - let VideoRecordingMetadata { duration, .. } = - get_video_metadata(app.clone(), video_id.clone(), Some(VideoType::Screen)) - .await - .unwrap(); + let metadata = match get_video_metadata(app.clone(), video_id.clone(), Some(VideoType::Screen)).await { + Ok(meta) => meta, + Err(e) => { + sentry::capture_message(&format!("Failed to get video metadata: {}", e), sentry::Level::Error); + return Err("Failed to read video metadata. The recording may be from an incompatible version.".to_string()); + } + }; + + let VideoRecordingMetadata { duration, .. } = metadata; // Calculate total frames with ceiling to ensure we don't exceed 100% let total_frames = ((duration * 30.0).ceil() as u32).max(1); @@ -28,7 +33,7 @@ pub async fn export_video( let output_path = editor_instance.meta().output_path(); - // If the file exists, return it immediately + // If the file exists and we're not forcing a re-render, return it if output_path.exists() && !force { return Ok(output_path); } @@ -59,17 +64,20 @@ pub async fn export_video( e.to_string() })?; - if use_custom_muxer { + let result = if use_custom_muxer { exporter.export_with_custom_muxer().await } else { exporter.export_with_ffmpeg_cli().await - } - .map_err(|e| { - sentry::capture_message(&e.to_string(), sentry::Level::Error); - e.to_string() - })?; + }; - ShowCapWindow::PrevRecordings.show(&app).ok(); - - Ok(output_path) + match result { + Ok(_) => { + ShowCapWindow::PrevRecordings.show(&app).ok(); + Ok(output_path) + } + Err(e) => { + sentry::capture_message(&e.to_string(), sentry::Level::Error); + Err(e.to_string()) + } + } } diff --git a/apps/desktop/src/routes/editor/Header.tsx b/apps/desktop/src/routes/editor/Header.tsx index f2b14cbf..0d67ce12 100644 --- a/apps/desktop/src/routes/editor/Header.tsx +++ b/apps/desktop/src/routes/editor/Header.tsx @@ -235,6 +235,7 @@ import { save } from "@tauri-apps/plugin-dialog"; import { DEFAULT_PROJECT_CONFIG } from "./projectConfig"; import { createMutation } from "@tanstack/solid-query"; import { getRequestEvent } from "solid-js/web"; +import { checkIsUpgradedAndUpdate } from "~/utils/plans"; function ExportButton() { const { videoId, project, prettyName } = useEditorContext(); @@ -261,9 +262,7 @@ function ExportButton() { progress.onmessage = (p) => { if (p.type === "FrameRendered" && progressState.type === "saving") { const percentComplete = Math.min( - Math.round( - (p.current_frame / (progressState.totalFrames || 1)) * 100 - ), + Math.round((p.current_frame / (progressState.totalFrames || 1)) * 100), 100 ); @@ -273,7 +272,7 @@ function ExportButton() { message: `Rendering video - ${percentComplete}%`, }); - // If rendering is complete, update the message + // If rendering is complete, update to finalizing state if (percentComplete === 100) { setProgressState({ ...progressState, @@ -281,10 +280,7 @@ function ExportButton() { }); } } - if ( - p.type === "EstimatedTotalFrames" && - progressState.type === "saving" - ) { + if (p.type === "EstimatedTotalFrames" && progressState.type === "saving") { setProgressState({ ...progressState, totalFrames: p.total_frames, @@ -293,25 +289,30 @@ function ExportButton() { } }; - const videoPath = await commands.exportVideo( - videoId, - project, - progress, - true, - useCustomMuxer - ); - await commands.copyFileToPath(videoPath, path); + try { + const videoPath = await commands.exportVideo( + videoId, + project, + progress, + true, + useCustomMuxer + ); + await commands.copyFileToPath(videoPath, path); - setProgressState({ - type: "saving", - progress: 100, - message: "Saved successfully!", - mediaPath: path, - }); + setProgressState({ + type: "saving", + progress: 100, + message: "Saved successfully!", + mediaPath: path, + }); - setTimeout(() => { + setTimeout(() => { + setProgressState({ type: "idle" }); + }, 1500); + } catch (error) { setProgressState({ type: "idle" }); - }, 1500); + throw error; + } }, })); @@ -342,6 +343,13 @@ function ShareButton() { throw new Error("Recording metadata not available"); } + // Check for pro access first before starting the export + const isUpgraded = await checkIsUpgradedAndUpdate(); + if (!isUpgraded) { + await commands.showWindow("Upgrade"); + throw new Error("Upgrade required to share recordings"); + } + let unlisten: (() => void) | undefined; try {