Skip to content

Commit a65d871

Browse files
cursed realtime upload progress
1 parent c813c07 commit a65d871

File tree

7 files changed

+118
-66
lines changed

7 files changed

+118
-66
lines changed

apps/desktop/src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1969,6 +1969,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
19691969
RecordingDeleted,
19701970
target_select_overlay::TargetUnderCursor,
19711971
hotkeys::OnEscapePress,
1972+
upload::UploadProgressEvent,
19721973
])
19731974
.error_handling(tauri_specta::ErrorHandlingMode::Throw)
19741975
.typ::<ProjectConfiguration>()

apps/desktop/src-tauri/src/recording.rs

Lines changed: 54 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ pub async fn start_recording(
436436
recording_dir.join("content/output.mp4"),
437437
video_upload_info.clone(),
438438
Some(finish_upload_rx),
439+
recording_dir.clone(),
439440
);
440441

441442
let mut builder = instant_recording::Actor::builder(
@@ -685,7 +686,7 @@ async fn handle_recording_end(
685686
// we delay reporting errors here so that everything else happens first
686687
Ok(recording) => Some(handle_recording_finish(&handle, recording).await),
687688
Err(error) => {
688-
// TODO: Error handling
689+
// TODO: Error handling -> Can we reuse `RecordingMeta` too?
689690
let mut project_meta = RecordingMeta::load_for_project(&recording_dir).unwrap();
690691
project_meta.inner = RecordingMetaInner::Failed { error };
691692
project_meta.save_for_project().unwrap();
@@ -805,58 +806,58 @@ async fn handle_recording_finish(
805806

806807
let _ = screenshot_task.await;
807808

808-
// if video_upload_succeeded {
809-
// if let Ok(result) =
810-
// compress_image(display_screenshot).await
811-
// .map_err(|err|
812-
// error!("Error compressing thumbnail for instant mode progressive upload: {err}")
813-
// ) {
814-
// let (stream, total_size) = bytes_into_stream(result);
815-
// do_presigned_upload(
816-
// &app,
817-
// stream,
818-
// total_size,
819-
// crate::upload::PresignedS3PutRequest {
820-
// video_id: video_upload_info.id.clone(),
821-
// subpath: "screenshot/screen-capture.jpg".to_string(),
822-
// method: PresignedS3PutRequestMethod::Put,
823-
// meta: None,
824-
// },
825-
// |p| {} // TODO: Progress reporting
826-
// )
827-
// .await
828-
// .map_err(|err| {
829-
// error!("Error updating thumbnail for instant mode progressive upload: {err}")
830-
// })
831-
// .ok();
832-
// }
833-
// } else {
834-
// if let Ok(meta) = build_video_meta(&output_path)
835-
// .map_err(|err| error!("Error getting video metdata: {}", err))
836-
// {
837-
// // The upload_video function handles screenshot upload, so we can pass it along
838-
// match upload_video(
839-
// &app,
840-
// video_upload_info.id.clone(),
841-
// output_path,
842-
// display_screenshot.clone(),
843-
// video_upload_info.config.clone(),
844-
// meta,
845-
// None,
846-
// )
847-
// .await
848-
// {
849-
// Ok(_) => {
850-
// info!(
851-
// "Final video upload with screenshot completed successfully"
852-
// )
853-
// }
854-
// Err(e) => {
855-
// error!("Error in final upload with screenshot: {}", e)
856-
// }
857-
// }
858-
// }
859-
// }
809+
if video_upload_succeeded {
810+
if let Ok(result) =
811+
compress_image(display_screenshot).await
812+
.map_err(|err|
813+
error!("Error compressing thumbnail for instant mode progressive upload: {err}")
814+
) {
815+
let (stream, total_size) = bytes_into_stream(result);
816+
do_presigned_upload(
817+
&app,
818+
stream,
819+
total_size,
820+
crate::upload::PresignedS3PutRequest {
821+
video_id: video_upload_info.id.clone(),
822+
subpath: "screenshot/screen-capture.jpg".to_string(),
823+
method: PresignedS3PutRequestMethod::Put,
824+
meta: None,
825+
},
826+
|p| {} // TODO: Progress reporting
827+
)
828+
.await
829+
.map_err(|err| {
830+
error!("Error updating thumbnail for instant mode progressive upload: {err}")
831+
})
832+
.ok();
833+
}
834+
} else {
835+
if let Ok(meta) = build_video_meta(&output_path)
836+
.map_err(|err| error!("Error getting video metdata: {}", err))
837+
{
838+
// The upload_video function handles screenshot upload, so we can pass it along
839+
match upload_video(
840+
&app,
841+
video_upload_info.id.clone(),
842+
output_path,
843+
display_screenshot.clone(),
844+
video_upload_info.config.clone(),
845+
meta,
846+
None,
847+
)
848+
.await
849+
{
850+
Ok(_) => {
851+
info!(
852+
"Final video upload with screenshot completed successfully"
853+
)
854+
}
855+
Err(e) => {
856+
error!("Error in final upload with screenshot: {}", e)
857+
}
858+
}
859+
}
860+
}
860861
}
861862
});
862863

apps/desktop/src-tauri/src/upload.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::web_api::ManagerExt;
44
use crate::{UploadProgress, VideoUploadInfo};
55
use axum::body::Body;
66
use bytes::Bytes;
7+
use cap_project::{RecordingMeta, RecordingMetaInner, UploadState};
78
use cap_utils::spawn_actor;
89
use ffmpeg::ffi::AV_TIME_BASE;
910
use flume::Receiver;
@@ -24,6 +25,7 @@ use std::{
2425
};
2526
use tauri::{AppHandle, ipc::Channel};
2627
use tauri_plugin_clipboard_manager::ClipboardExt;
28+
use tauri_specta::Event;
2729
use tokio::fs::File;
2830
use tokio::io::{AsyncReadExt, AsyncSeekExt};
2931
use tokio::sync::watch;
@@ -531,9 +533,16 @@ pub fn bytes_into_stream(
531533
(stream, total_size as u64)
532534
}
533535

536+
#[derive(Clone, Serialize, Type, tauri_specta::Event)]
537+
pub struct UploadProgressEvent {
538+
video_id: String,
539+
// TODO: Account for different states -> Eg. uploading video vs thumbnail
540+
uploaded: String,
541+
total: String,
542+
}
543+
534544
// a typical recommended chunk size is 5MB (AWS min part size).
535545
const CHUNK_SIZE: u64 = 5 * 1024 * 1024; // 5MB
536-
// const MIN_PART_SIZE: u64 = 5 * 1024 * 1024; // For non-final parts
537546

538547
#[derive(Serialize)]
539548
#[serde(rename_all = "camelCase")]
@@ -558,6 +567,7 @@ impl InstantMultipartUpload {
558567
file_path: PathBuf,
559568
pre_created_video: VideoUploadInfo,
560569
realtime_upload_done: Option<Receiver<()>>,
570+
recording_dir: PathBuf,
561571
) -> Self {
562572
Self {
563573
handle: spawn_actor(Self::run(
@@ -566,6 +576,7 @@ impl InstantMultipartUpload {
566576
file_path,
567577
pre_created_video,
568578
realtime_upload_done,
579+
recording_dir,
569580
)),
570581
}
571582
}
@@ -576,7 +587,13 @@ impl InstantMultipartUpload {
576587
file_path: PathBuf,
577588
pre_created_video: VideoUploadInfo,
578589
realtime_video_done: Option<Receiver<()>>,
590+
recording_dir: PathBuf,
579591
) -> Result<(), String> {
592+
// TODO: Reuse this + error handling
593+
let mut project_meta = RecordingMeta::load_for_project(&recording_dir).unwrap();
594+
project_meta.upload = Some(UploadState::MultipartUpload);
595+
project_meta.save_for_project().unwrap();
596+
580597
// --------------------------------------------
581598
// basic constants and info for chunk approach
582599
// --------------------------------------------
@@ -751,6 +768,11 @@ impl InstantMultipartUpload {
751768
}
752769
}
753770

771+
// TODO: Reuse this + error handling
772+
let mut project_meta = RecordingMeta::load_for_project(&recording_dir).unwrap();
773+
project_meta.upload = Some(UploadState::Complete);
774+
project_meta.save_for_project().unwrap();
775+
754776
// Copy link to clipboard early
755777
let _ = app.clipboard().write_text(pre_created_video.link.clone());
756778

@@ -882,8 +904,6 @@ impl InstantMultipartUpload {
882904
}
883905
};
884906

885-
progress.update(expected_pos, file_size);
886-
887907
if !presign_response.status().is_success() {
888908
let status = presign_response.status();
889909
let error_body = presign_response
@@ -993,6 +1013,15 @@ impl InstantMultipartUpload {
9931013
(*last_uploaded_position as f64 / file_size as f64 * 100.0) as u32
9941014
);
9951015

1016+
progress.update(expected_pos, file_size);
1017+
UploadProgressEvent {
1018+
video_id: video_id.to_string(),
1019+
uploaded: last_uploaded_position.to_string(),
1020+
total: file_size.to_string(),
1021+
}
1022+
.emit(app)
1023+
.ok();
1024+
9961025
let part = UploadedPart {
9971026
part_number: *part_number,
9981027
etag,

apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
type ParentProps,
2121
Show,
2222
} from "solid-js";
23+
import { createStore } from "solid-js/store";
2324
import { trackEvent } from "~/utils/analytics";
2425
import { createTauriEventListener } from "~/utils/createEventListener";
2526
import {
@@ -117,6 +118,17 @@ export default function Recordings() {
117118
});
118119
};
119120

121+
const [uploadProgress, setUploadProgress] = createStore<
122+
Record<string, number>
123+
>({});
124+
// TODO: Cleanup subscription
125+
events.uploadProgressEvent.listen((e) => {
126+
setUploadProgress(
127+
e.payload.video_id,
128+
Number(e.payload.uploaded) / Number(e.payload.total),
129+
);
130+
});
131+
120132
return (
121133
<div class="flex relative flex-col p-4 space-y-4 w-full h-full">
122134
<div class="flex flex-col">
@@ -133,6 +145,8 @@ export default function Recordings() {
133145
</p>
134146
}
135147
>
148+
<pre>{JSON.stringify(uploadProgress)}</pre>
149+
136150
<div class="flex gap-3 items-center pb-4 w-full border-b border-gray-2">
137151
<For each={Tabs}>
138152
{(tab) => (
@@ -211,6 +225,8 @@ function RecordingItem(props: {
211225
</Show>
212226
<div class="flex flex-col gap-2">
213227
{"recording" in props.recording.meta ? "TRUE" : "FALSE"}
228+
{"error" in props.recording.meta ? props.recording.meta.error : ""}
229+
{JSON.stringify(props.recording.meta?.upload || {})}
214230

215231
<span>{props.recording.prettyName}</span>
216232
<div

apps/desktop/src/utils/tauri.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,8 @@ requestNewScreenshot: RequestNewScreenshot,
291291
requestOpenRecordingPicker: RequestOpenRecordingPicker,
292292
requestOpenSettings: RequestOpenSettings,
293293
requestStartRecording: RequestStartRecording,
294-
targetUnderCursor: TargetUnderCursor
294+
targetUnderCursor: TargetUnderCursor,
295+
uploadProgressEvent: UploadProgressEvent
295296
}>({
296297
audioInputLevelChange: "audio-input-level-change",
297298
authenticationInvalid: "authentication-invalid",
@@ -312,7 +313,8 @@ requestNewScreenshot: "request-new-screenshot",
312313
requestOpenRecordingPicker: "request-open-recording-picker",
313314
requestOpenSettings: "request-open-settings",
314315
requestStartRecording: "request-start-recording",
315-
targetUnderCursor: "target-under-cursor"
316+
targetUnderCursor: "target-under-cursor",
317+
uploadProgressEvent: "upload-progress-event"
316318
})
317319

318320
/** user-defined constants **/
@@ -451,8 +453,9 @@ export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments:
451453
export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number }
452454
export type UploadMode = { Initial: { pre_created_video: VideoUploadInfo | null } } | "Reupload"
453455
export type UploadProgress = { progress: number }
456+
export type UploadProgressEvent = { video_id: string; uploaded: string; total: string }
454457
export type UploadResult = { Success: string } | "NotAuthenticated" | "PlanCheckFailed" | "UpgradeRequired"
455-
export type UploadState = "Uploading" | { Failed: string } | "Complete"
458+
export type UploadState = "MultipartUpload" | "SinglePartUpload" | { Failed: string } | "Complete"
456459
export type Video = { duration: number; width: number; height: number; fps: number; start_time: number }
457460
export type VideoMeta = { path: string; fps?: number;
458461
/**

crates/project/src/meta.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ pub struct RecordingMeta {
7575

7676
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
7777
pub enum UploadState {
78-
Uploading,
78+
// TODO: Do we care about what sort of upload it is???
79+
MultipartUpload,
80+
SinglePartUpload,
7981
Failed(String),
8082
Complete,
8183
}
@@ -146,8 +148,8 @@ impl RecordingMeta {
146148
RecordingMetaInner::Studio(_) => {
147149
Some(self.project_path.join("output").join("result.mp4"))
148150
}
149-
RecordingMetaInner::InProgress { recording } => None,
150-
RecordingMetaInner::Failed { error } => None,
151+
RecordingMetaInner::InProgress { .. } => None,
152+
RecordingMetaInner::Failed { .. } => None,
151153
}
152154
}
153155

crates/recording/src/instant_recording.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,10 @@ pub async fn spawn_instant_recording_actor(
210210
RecordingError,
211211
> {
212212
// TODO: Remove
213-
return Err(RecordingError::Io(std::io::Error::new(
214-
std::io::ErrorKind::Other,
215-
format!("Bruh"),
216-
)));
213+
// return Err(RecordingError::Io(std::io::Error::new(
214+
// std::io::ErrorKind::Other,
215+
// format!("Bruh"),
216+
// )));
217217

218218
ensure_dir(&recording_dir)?;
219219

0 commit comments

Comments
 (0)