Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 104 additions & 5 deletions apps/desktop/src-tauri/src/export.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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<FramesRendered>,
settings: ExportSettings,
upload: bool,
) -> Result<PathBuf, String> {
let exporter_base = ExporterBase::builder(project_path)
let exporter_base = ExporterBase::builder(project_path.clone())
.build()
.await
.map_err(|e| {
Expand All @@ -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
Expand Down
60 changes: 29 additions & 31 deletions apps/desktop/src/routes/editor/ExportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,6 @@ export function ExportDialog() {

const [outputPath, setOutputPath] = createSignal<string | null>(null);

const selectedStyle = "bg-gray-7";

const projectPath = editorInstance.path;

const exportEstimates = createQuery(() => ({
Expand Down Expand Up @@ -327,16 +325,16 @@ export function ExportDialog() {
}
}

const uploadChannel = new Channel<UploadProgress>((progress) => {
console.log("Upload progress:", progress);
setExportState(
produce((state) => {
if (state.type !== "uploading") return;
// const uploadChannel = new Channel<UploadProgress>((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 }),
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/utils/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export async function exportVideo(
const progress = new Channel<FramesRendered>((e) => {
onProgress(e);
});
return await commands.exportVideo(projectPath, progress, settings);
return await commands.exportVideo(projectPath, progress, settings, true); // TODO: Fix this last param
}
4 changes: 2 additions & 2 deletions apps/desktop/src/utils/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ async focusCapturesPanel() : Promise<void> {
async getCurrentRecording() : Promise<JsonValue<CurrentRecording | null>> {
return await TAURI_INVOKE("get_current_recording");
},
async exportVideo(projectPath: string, progress: TAURI_CHANNEL<FramesRendered>, settings: ExportSettings) : Promise<string> {
return await TAURI_INVOKE("export_video", { projectPath, progress, settings });
async exportVideo(projectPath: string, progress: TAURI_CHANNEL<FramesRendered>, settings: ExportSettings, upload: boolean) : Promise<string> {
return await TAURI_INVOKE("export_video", { projectPath, progress, settings, upload });
},
async getExportEstimates(path: string, resolution: XY<number>, fps: number) : Promise<ExportEstimates> {
return await TAURI_INVOKE("get_export_estimates", { path, resolution, fps });
Expand Down
Loading