diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-tauri/src/export.rs index 44097fa772..4b4120d44a 100644 --- a/apps/desktop/src-tauri/src/export.rs +++ b/apps/desktop/src-tauri/src/export.rs @@ -1,9 +1,16 @@ -use crate::{FramesRendered, get_video_metadata}; +use crate::{ + FramesRendered, UploadMode, + auth::AuthStore, + get_video_metadata, + upload::{InstantMultipartUpload, build_video_meta, create_or_get_video}, + web_api::{AuthedApiError, ManagerExt}, +}; use cap_export::ExporterBase; -use cap_project::{RecordingMeta, XY}; +use cap_project::{RecordingMeta, S3UploadMeta, VideoUploadInfo, XY}; use serde::Deserialize; use specta::Type; -use std::path::PathBuf; +use std::{path::PathBuf, time::Duration}; +use tauri::AppHandle; use tracing::{info, instrument}; #[derive(Deserialize, Clone, Copy, Debug, Type)] @@ -24,13 +31,15 @@ impl ExportSettings { #[tauri::command] #[specta::specta] -#[instrument(skip(progress))] +#[instrument(skip(app, progress))] pub async fn export_video( + app: AppHandle, project_path: PathBuf, progress: tauri::ipc::Channel, settings: ExportSettings, + upload: bool, ) -> Result { - let exporter_base = ExporterBase::builder(project_path) + let exporter_base = ExporterBase::builder(project_path.clone()) .build() .await .map_err(|e| { @@ -45,6 +54,96 @@ pub async fn export_video( total_frames, }); + if upload { + println!("RUNNING MULTIPART UPLOADER"); + + let mode = UploadMode::Initial { + pre_created_video: None, + }; // TODO: Fix this + + let meta = RecordingMeta::load_for_project(&project_path).map_err(|v| v.to_string())?; + + let file_path = meta.output_path(); + if !file_path.exists() { + // notifications::send_notification(&app, notifications::NotificationType::UploadFailed); + // return Err("Failed to upload video: Rendered video not found".to_string()); + todo!(); + } + + let Ok(Some(auth)) = AuthStore::get(&app) else { + AuthStore::set(&app, None).map_err(|e| e.to_string())?; + // return Ok(UploadResult::NotAuthenticated); + todo!(); + }; + + let metadata = build_video_meta(&file_path) + .map_err(|err| format!("Error getting output video meta: {err}"))?; + + if !auth.is_upgraded() && metadata.duration_in_secs > 300.0 { + // return Ok(UploadResult::UpgradeRequired); + todo!(); + } + + let s3_config = match async { + let video_id = match mode { + UploadMode::Initial { pre_created_video } => { + if let Some(pre_created) = pre_created_video { + return Ok(pre_created.config); + } + None + } + UploadMode::Reupload => { + let Some(sharing) = meta.sharing.clone() else { + return Err("No sharing metadata found".into()); + }; + + Some(sharing.id) + } + }; + + create_or_get_video( + &app, + false, + video_id, + Some(meta.pretty_name.clone()), + Some(metadata.clone()), + ) + .await + } + .await + { + Ok(data) => data, + Err(AuthedApiError::InvalidAuthentication) => { + // return Ok(UploadResult::NotAuthenticated); + todo!(); + } + Err(AuthedApiError::UpgradeRequired) => todo!(), // return Ok(UploadResult::UpgradeRequired), + Err(err) => return Err(err.to_string()), + }; + + // TODO: Properly hook this up with the `ExportDialog` + let link = app.make_app_url(format!("/s/{}", s3_config.id)).await; + + // TODO: Cleanup `handle` when this is cancelled + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(5)).await; // TODO: Do this properly + let handle = InstantMultipartUpload::spawn( + app, + file_path.join("output").join("result.mp4"), + VideoUploadInfo { + id: s3_config.id.clone(), + link: link.clone(), + config: s3_config, // S3UploadMeta { id: s3_config.id }, + }, + file_path, + None, + ); + + let result = handle.handle.await; + println!("MULTIPART UPLOAD COMPLETE {link} {result:?}"); + }); + } + let output_path = match settings { ExportSettings::Mp4(settings) => { settings diff --git a/apps/desktop/src/routes/editor/ExportDialog.tsx b/apps/desktop/src/routes/editor/ExportDialog.tsx index 40d6a75c62..d1ab785a7c 100644 --- a/apps/desktop/src/routes/editor/ExportDialog.tsx +++ b/apps/desktop/src/routes/editor/ExportDialog.tsx @@ -160,8 +160,6 @@ export function ExportDialog() { const [outputPath, setOutputPath] = createSignal(null); - const selectedStyle = "bg-gray-7"; - const projectPath = editorInstance.path; const exportEstimates = createQuery(() => ({ @@ -327,16 +325,16 @@ export function ExportDialog() { } } - const uploadChannel = new Channel((progress) => { - console.log("Upload progress:", progress); - setExportState( - produce((state) => { - if (state.type !== "uploading") return; + // const uploadChannel = new Channel((progress) => { + // console.log("Upload progress:", progress); + // // setExportState( + // // produce((state) => { + // // if (state.type !== "uploading") return; - state.progress = Math.round(progress.progress * 100); - }), - ); - }); + // // state.progress = Math.round(progress.progress * 100); + // // }), + // // ); + // }); await exportWithSettings((progress) => setExportState({ type: "rendering", progress }), @@ -345,26 +343,26 @@ export function ExportDialog() { setExportState({ type: "uploading", progress: 0 }); // Now proceed with upload - const result = meta().sharing - ? await commands.uploadExportedVideo( - projectPath, - "Reupload", - uploadChannel, - ) - : await commands.uploadExportedVideo( - projectPath, - { - Initial: { pre_created_video: null }, - }, - uploadChannel, - ); - - if (result === "NotAuthenticated") - throw new Error("You need to sign in to share recordings"); - else if (result === "PlanCheckFailed") - throw new Error("Failed to verify your subscription status"); - else if (result === "UpgradeRequired") - throw new Error("This feature requires an upgraded plan"); + // const result = meta().sharing + // ? await commands.uploadExportedVideo( + // projectPath, + // "Reupload", + // uploadChannel, + // ) + // : await commands.uploadExportedVideo( + // projectPath, + // { + // Initial: { pre_created_video: null }, + // }, + // uploadChannel, + // ); + + // if (result === "NotAuthenticated") + // throw new Error("You need to sign in to share recordings"); + // else if (result === "PlanCheckFailed") + // throw new Error("Failed to verify your subscription status"); + // else if (result === "UpgradeRequired") + // throw new Error("This feature requires an upgraded plan"); }, onSuccess: async () => { const d = dialog(); diff --git a/apps/desktop/src/utils/export.ts b/apps/desktop/src/utils/export.ts index fd73dbc00e..447271db64 100644 --- a/apps/desktop/src/utils/export.ts +++ b/apps/desktop/src/utils/export.ts @@ -9,5 +9,5 @@ export async function exportVideo( const progress = new Channel((e) => { onProgress(e); }); - return await commands.exportVideo(projectPath, progress, settings); + return await commands.exportVideo(projectPath, progress, settings, true); // TODO: Fix this last param } diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 2fcc5423df..7c6b7e7bc9 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -71,8 +71,8 @@ async focusCapturesPanel() : Promise { async getCurrentRecording() : Promise> { return await TAURI_INVOKE("get_current_recording"); }, -async exportVideo(projectPath: string, progress: TAURI_CHANNEL, settings: ExportSettings) : Promise { - return await TAURI_INVOKE("export_video", { projectPath, progress, settings }); +async exportVideo(projectPath: string, progress: TAURI_CHANNEL, settings: ExportSettings, upload: boolean) : Promise { + return await TAURI_INVOKE("export_video", { projectPath, progress, settings, upload }); }, async getExportEstimates(path: string, resolution: XY, fps: number) : Promise { return await TAURI_INVOKE("get_export_estimates", { path, resolution, fps });