@agent-native/core 0.175.2-nightly-20260826150235 → 0.175.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/clips/app/components/player/comments-panel.tsx +1 -1
- package/corpus/templates/clips/app/components/player/video-player.tsx +5 -3
- package/corpus/templates/clips/desktop/src/app.tsx +171 -51
- package/corpus/templates/clips/desktop/src/lib/pill-completion.ts +111 -0
- package/corpus/templates/clips/desktop/src/lib/pill-session.ts +34 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +181 -79
- package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +28 -4
- package/corpus/templates/clips/desktop/src/main.tsx +4 -4
- package/corpus/templates/clips/desktop/src/overlays/record-pill.tsx +1320 -0
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +1 -1
- package/corpus/templates/clips/desktop/src/styles.css +20 -308
- package/corpus/templates/clips/desktop/src/tailwind.css +85 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +153 -70
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +15 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +25 -7
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +3 -3
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +375 -24
- package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +22 -30
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +5 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +134 -14
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +233 -4
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +8 -5
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1 -1
- package/corpus/templates/design/.generated/bridge/embedded-wheel.generated.ts +3 -2
- package/corpus/templates/design/.generated/bridge/zoom.generated.ts +8 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +46 -10
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -63
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -1
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +8 -2
- package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +11 -2
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -4
- package/corpus/templates/design/app/components/design/multi-screen/zoom-gesture.ts +19 -63
- package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +2 -1
- package/corpus/templates/design/shared/canvas-math.ts +11 -1
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +44 -18
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +88 -23
- package/corpus/templates/slides/app/context/DeckContext.tsx +155 -86
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +7 -5
- package/corpus/templates/slides/changelog/2026-08-26-thumbnail-arrow-keys-now-navigate-slides-after-selection.md +6 -0
- package/dist/client/hooks/index.d.ts +1 -0
- package/dist/client/hooks/index.js +1 -0
- package/dist/client/use-pinch-zoom.js +17 -4
- package/dist/client/zoom-gesture.d.ts +74 -0
- package/dist/client/zoom-gesture.js +100 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/builder-browser.d.ts +13 -13
- package/dist/server/builder-browser.js +46 -15
- package/dist/server/builder-oauth.d.ts +12 -0
- package/dist/server/builder-oauth.js +13 -2
- package/dist/server/core-routes-plugin.js +54 -19
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/vite/client.js +2 -0
- package/docs/content/template-chat-developers.mdx +100 -13
- package/docs/content/template-chat-first-edits.mdx +5 -5
- package/docs/content/template-chat.mdx +84 -36
- package/package.json +4 -3
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +0 -458
|
@@ -733,6 +733,7 @@ pub(crate) fn start_segmented_custom_screencapturekit_backend_at(
|
|
|
733
733
|
capture_region,
|
|
734
734
|
defer_recording_output,
|
|
735
735
|
true,
|
|
736
|
+
None,
|
|
736
737
|
)
|
|
737
738
|
}
|
|
738
739
|
|
|
@@ -932,8 +933,14 @@ fn spawn_disk_monitor(app: AppHandle, recording_path: PathBuf) -> Arc<AtomicBool
|
|
|
932
933
|
stop
|
|
933
934
|
}
|
|
934
935
|
|
|
936
|
+
/// `recording_id` names the take this progress belongs to. The recording pill
|
|
937
|
+
/// reuses one window across takes, so an untagged progress event from an
|
|
938
|
+
/// earlier upload would move a newer take's completion card and refresh its
|
|
939
|
+
/// stall timeout. Required rather than optional so the compiler, not review,
|
|
940
|
+
/// is what catches a new emit site that forgets it.
|
|
935
941
|
fn emit_native_upload_progress(
|
|
936
942
|
app: &AppHandle,
|
|
943
|
+
recording_id: &str,
|
|
937
944
|
stage: &str,
|
|
938
945
|
message: impl Into<String>,
|
|
939
946
|
detail: Option<String>,
|
|
@@ -942,6 +949,7 @@ fn emit_native_upload_progress(
|
|
|
942
949
|
let _ = app.emit(
|
|
943
950
|
"clips:native-upload-progress",
|
|
944
951
|
serde_json::json!({
|
|
952
|
+
"recordingId": recording_id,
|
|
945
953
|
"stage": stage,
|
|
946
954
|
"message": message.into(),
|
|
947
955
|
"detail": detail,
|
|
@@ -1009,7 +1017,11 @@ async fn wait_for_native_upload_retry_cancel(recording_id: &str) {
|
|
|
1009
1017
|
}
|
|
1010
1018
|
}
|
|
1011
1019
|
|
|
1012
|
-
|
|
1020
|
+
/// Clear the take-once completion slot and the open-claim latch. Every path
|
|
1021
|
+
/// that starts a recording must call this: the slot has no expiry, and the
|
|
1022
|
+
/// pill drains it when a completion card opens, so a result left behind by an
|
|
1023
|
+
/// earlier take would surface on the next one's card.
|
|
1024
|
+
pub(crate) fn reset_native_upload_completion_state() {
|
|
1013
1025
|
if let Ok(mut last) = last_native_upload_finished().lock() {
|
|
1014
1026
|
*last = None;
|
|
1015
1027
|
}
|
|
@@ -1808,7 +1820,14 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1808
1820
|
has_camera: bool,
|
|
1809
1821
|
) -> Result<NativeFullscreenUploadResult, String> {
|
|
1810
1822
|
let upload_mode = NativeUploadMode::from_option(upload_mode);
|
|
1811
|
-
emit_native_upload_progress(
|
|
1823
|
+
emit_native_upload_progress(
|
|
1824
|
+
&app,
|
|
1825
|
+
&recording_id,
|
|
1826
|
+
"finalizing",
|
|
1827
|
+
"Optimizing clip",
|
|
1828
|
+
None,
|
|
1829
|
+
None,
|
|
1830
|
+
);
|
|
1812
1831
|
// The recorder's ScreenCaptureKit stream is now fully stopped and its moov
|
|
1813
1832
|
// atom is written (or has definitively failed). Signal the UI so it can tear
|
|
1814
1833
|
// down the separate live-transcription SCStream (system_audio.rs) now,
|
|
@@ -1861,7 +1880,7 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1861
1880
|
Ok(()) => merge_err.clone(),
|
|
1862
1881
|
});
|
|
1863
1882
|
write_saved_recording_metadata(&app, &saved)?;
|
|
1864
|
-
emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
|
|
1883
|
+
emit_native_upload_progress(&app, &recording_id, "failed", "Upload paused", None, None);
|
|
1865
1884
|
let error = format!(
|
|
1866
1885
|
"{merge_err}. The raw clip segments were saved locally and can be retried from the Clips menu."
|
|
1867
1886
|
);
|
|
@@ -1901,7 +1920,7 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1901
1920
|
if stop_err.starts_with(CAPTURE_FINALIZE_INCOMPLETE_PREFIX) {
|
|
1902
1921
|
saved.last_error = Some(stop_err.clone());
|
|
1903
1922
|
write_saved_recording_metadata(&app, &saved)?;
|
|
1904
|
-
emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
|
|
1923
|
+
emit_native_upload_progress(&app, &recording_id, "failed", "Upload paused", None, None);
|
|
1905
1924
|
let error = format!(
|
|
1906
1925
|
"{stop_err}. The clip was saved locally and can be retried from the Clips menu."
|
|
1907
1926
|
);
|
|
@@ -1936,7 +1955,14 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1936
1955
|
);
|
|
1937
1956
|
}
|
|
1938
1957
|
write_saved_recording_metadata(&app, &saved)?;
|
|
1939
|
-
emit_native_upload_progress(
|
|
1958
|
+
emit_native_upload_progress(
|
|
1959
|
+
&app,
|
|
1960
|
+
&recording_id,
|
|
1961
|
+
"failed",
|
|
1962
|
+
"Upload paused",
|
|
1963
|
+
None,
|
|
1964
|
+
None,
|
|
1965
|
+
);
|
|
1940
1966
|
let suffix = if saved.corrupt {
|
|
1941
1967
|
"The local file is incomplete and cannot be recovered. Discard it from the Clips menu and record again."
|
|
1942
1968
|
} else {
|
|
@@ -1974,7 +2000,7 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1974
2000
|
);
|
|
1975
2001
|
eprintln!("[clips-tray] recording missing moov after Ok stop outcome (likely finalize timeout) — saving as retryable, skipping upload");
|
|
1976
2002
|
write_saved_recording_metadata(&app, &saved)?;
|
|
1977
|
-
emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
|
|
2003
|
+
emit_native_upload_progress(&app, &recording_id, "failed", "Upload paused", None, None);
|
|
1978
2004
|
let error =
|
|
1979
2005
|
"Recorded MP4 is missing playback metadata. Please retry the recording.".to_string();
|
|
1980
2006
|
emit_native_upload_finished(
|
|
@@ -1988,7 +2014,14 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1988
2014
|
return Err(error);
|
|
1989
2015
|
}
|
|
1990
2016
|
write_saved_recording_metadata(&app, &saved)?;
|
|
1991
|
-
emit_native_upload_progress(
|
|
2017
|
+
emit_native_upload_progress(
|
|
2018
|
+
&app,
|
|
2019
|
+
&recording_id,
|
|
2020
|
+
"preparing",
|
|
2021
|
+
"Optimizing clip",
|
|
2022
|
+
None,
|
|
2023
|
+
None,
|
|
2024
|
+
);
|
|
1992
2025
|
|
|
1993
2026
|
#[cfg(target_os = "macos")]
|
|
1994
2027
|
eprintln!(
|
|
@@ -2022,7 +2055,14 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
2022
2055
|
saved.last_error = Some(error.clone());
|
|
2023
2056
|
saved.retry_count = saved.retry_count.saturating_add(1);
|
|
2024
2057
|
let _ = write_saved_recording_metadata(&app, &saved);
|
|
2025
|
-
emit_native_upload_progress(
|
|
2058
|
+
emit_native_upload_progress(
|
|
2059
|
+
&app,
|
|
2060
|
+
&recording_id,
|
|
2061
|
+
"failed",
|
|
2062
|
+
"Upload paused",
|
|
2063
|
+
None,
|
|
2064
|
+
None,
|
|
2065
|
+
);
|
|
2026
2066
|
let error = format!(
|
|
2027
2067
|
"{error} The clip was saved locally and can be retried from the Clips menu."
|
|
2028
2068
|
);
|
|
@@ -2046,7 +2086,14 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
2046
2086
|
saved.last_error = Some(error.clone());
|
|
2047
2087
|
saved.retry_count = saved.retry_count.saturating_add(1);
|
|
2048
2088
|
let _ = write_saved_recording_metadata(&app, &saved);
|
|
2049
|
-
emit_native_upload_progress(
|
|
2089
|
+
emit_native_upload_progress(
|
|
2090
|
+
&app,
|
|
2091
|
+
&recording_id,
|
|
2092
|
+
"failed",
|
|
2093
|
+
"Upload paused",
|
|
2094
|
+
None,
|
|
2095
|
+
None,
|
|
2096
|
+
);
|
|
2050
2097
|
let error = format!(
|
|
2051
2098
|
"{error} The clip was saved locally and can be retried from the Clips menu."
|
|
2052
2099
|
);
|
|
@@ -2064,7 +2111,14 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
2064
2111
|
eprintln!(
|
|
2065
2112
|
"[live-upload] stop: signalling finalize for {recording_id} (measured_duration_ms={verified_duration_ms})"
|
|
2066
2113
|
);
|
|
2067
|
-
emit_native_upload_progress(
|
|
2114
|
+
emit_native_upload_progress(
|
|
2115
|
+
&app,
|
|
2116
|
+
&recording_id,
|
|
2117
|
+
"uploading",
|
|
2118
|
+
"Uploading clip",
|
|
2119
|
+
None,
|
|
2120
|
+
None,
|
|
2121
|
+
);
|
|
2068
2122
|
live.ctrl
|
|
2069
2123
|
.duration_ms
|
|
2070
2124
|
.store(verified_duration_ms as u64, Ordering::SeqCst);
|
|
@@ -2100,7 +2154,14 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
2100
2154
|
saved.last_error = Some(err.clone());
|
|
2101
2155
|
saved.retry_count = saved.retry_count.saturating_add(1);
|
|
2102
2156
|
let _ = write_saved_recording_metadata(&app, &saved);
|
|
2103
|
-
emit_native_upload_progress(
|
|
2157
|
+
emit_native_upload_progress(
|
|
2158
|
+
&app,
|
|
2159
|
+
&recording_id,
|
|
2160
|
+
"failed",
|
|
2161
|
+
"Upload paused",
|
|
2162
|
+
None,
|
|
2163
|
+
None,
|
|
2164
|
+
);
|
|
2104
2165
|
let error = format!(
|
|
2105
2166
|
"{err}. The clip was saved locally and can be retried from the Clips menu."
|
|
2106
2167
|
);
|
|
@@ -2142,7 +2203,7 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
2142
2203
|
saved.last_error = Some(err.clone());
|
|
2143
2204
|
saved.retry_count = saved.retry_count.saturating_add(1);
|
|
2144
2205
|
let _ = write_saved_recording_metadata(&app, &saved);
|
|
2145
|
-
emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
|
|
2206
|
+
emit_native_upload_progress(&app, &recording_id, "failed", "Upload paused", None, None);
|
|
2146
2207
|
let error = format!(
|
|
2147
2208
|
"{err}. The clip was saved locally and can be retried from the Clips menu."
|
|
2148
2209
|
);
|
|
@@ -2189,7 +2250,7 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
2189
2250
|
saved.corrupt = true;
|
|
2190
2251
|
}
|
|
2191
2252
|
let _ = write_saved_recording_metadata(&app, &saved);
|
|
2192
|
-
emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
|
|
2253
|
+
emit_native_upload_progress(&app, &recording_id, "failed", "Upload paused", None, None);
|
|
2193
2254
|
let error = format!(
|
|
2194
2255
|
"{err}. The clip was saved locally and can be retried from the Clips menu."
|
|
2195
2256
|
);
|
|
@@ -2296,6 +2357,27 @@ pub(crate) fn kill_active_screencapture_child(state: &NativeFullscreenRecordingS
|
|
|
2296
2357
|
}
|
|
2297
2358
|
}
|
|
2298
2359
|
|
|
2360
|
+
/// The `screencapture` fallback backend owns an OS child process, and
|
|
2361
|
+
/// `kill_active_screencapture_child` can only find it while the session is in
|
|
2362
|
+
/// the state slot. A cancel takes the session out of that slot and hands it to
|
|
2363
|
+
/// a detached thread, so a quit landing before that thread reaches its
|
|
2364
|
+
/// kill/reap path would leave `screencapture` running after Clips is gone.
|
|
2365
|
+
/// Stop and reap it here, on the caller's thread, and detach only the file
|
|
2366
|
+
/// cleanup. Bounded by `stop_screencapture`'s discard grace (~250ms) and only
|
|
2367
|
+
/// reached by the fallback backend — the ScreenCaptureKit paths, which carry
|
|
2368
|
+
/// the restart latency this detach exists for, still tear down detached.
|
|
2369
|
+
fn terminate_screencapture_child_before_detach(session: &mut NativeFullscreenSession) {
|
|
2370
|
+
if !matches!(
|
|
2371
|
+
session.backend.as_ref(),
|
|
2372
|
+
Some(NativeFullscreenBackend::Screencapture { .. })
|
|
2373
|
+
) {
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
if let Err(err) = finalize_active_backend(session, false) {
|
|
2377
|
+
eprintln!("[clips-tray] cancel: screencapture stop before detach failed: {err}");
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2299
2381
|
#[tauri::command]
|
|
2300
2382
|
pub async fn native_fullscreen_recording_cancel(
|
|
2301
2383
|
app: AppHandle,
|
|
@@ -2312,7 +2394,12 @@ pub async fn native_fullscreen_recording_cancel(
|
|
|
2312
2394
|
guard.take()
|
|
2313
2395
|
};
|
|
2314
2396
|
if let Some(mut session) = session {
|
|
2315
|
-
|
|
2397
|
+
eprintln!(
|
|
2398
|
+
"[clips-tray] cancel: detaching discard for {}",
|
|
2399
|
+
session.path.display()
|
|
2400
|
+
);
|
|
2401
|
+
terminate_screencapture_child_before_detach(&mut session);
|
|
2402
|
+
spawn_detached_discard(session);
|
|
2316
2403
|
}
|
|
2317
2404
|
// An aborted start (countdown/warm cancelled before `begin`) never reaches
|
|
2318
2405
|
// `hide_recording_chrome`, so the picker's monitor override must also be
|
|
@@ -2612,6 +2699,7 @@ fn rotate_screencapturekit_segment(
|
|
|
2612
2699
|
restart.target_display_id,
|
|
2613
2700
|
restart.capture_region,
|
|
2614
2701
|
false,
|
|
2702
|
+
None,
|
|
2615
2703
|
);
|
|
2616
2704
|
|
|
2617
2705
|
let (backend, _, _) = match start_result {
|
|
@@ -2800,6 +2888,26 @@ fn recover_from_unusable_current_segment(
|
|
|
2800
2888
|
false
|
|
2801
2889
|
}
|
|
2802
2890
|
|
|
2891
|
+
/// Discard a cancelled session on a detached thread. Backend teardown blocks
|
|
2892
|
+
/// for however long the OS takes — the stock SCK path's `stop_capture()` can
|
|
2893
|
+
/// hang indefinitely — and the renderer awaits the cancel command before it
|
|
2894
|
+
/// continues its own discard chain, so the discard must not run on the
|
|
2895
|
+
/// command's thread. The session was already taken out of the state slot
|
|
2896
|
+
/// under the state lock, and nothing here re-locks it, so a new recording
|
|
2897
|
+
/// can start while the old stream tears down (the same concurrent-stream
|
|
2898
|
+
/// model pause/resume relies on). Tradeoff: if the process dies before this
|
|
2899
|
+
/// thread deletes the files, crash recovery can resurrect the cancelled take.
|
|
2900
|
+
fn spawn_detached_discard(mut session: NativeFullscreenSession) -> std::thread::JoinHandle<()> {
|
|
2901
|
+
std::thread::spawn(move || {
|
|
2902
|
+
let recording_path = session.path.clone();
|
|
2903
|
+
discard_session(&mut session);
|
|
2904
|
+
eprintln!(
|
|
2905
|
+
"[clips-tray] cancel: detached discard finished for {}",
|
|
2906
|
+
recording_path.display()
|
|
2907
|
+
);
|
|
2908
|
+
})
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2803
2911
|
/// Best-effort cleanup of a session being discarded (cancel, or a stale
|
|
2804
2912
|
/// session displaced by a new start). Finalizes any active backend and
|
|
2805
2913
|
/// deletes every on-disk artifact — segment files and the final path.
|
|
@@ -2811,7 +2919,9 @@ fn discard_session(session: &mut NativeFullscreenSession) {
|
|
|
2811
2919
|
if let Some(stop) = &session.disk_monitor_stop {
|
|
2812
2920
|
stop.store(true, Ordering::Relaxed);
|
|
2813
2921
|
}
|
|
2814
|
-
let
|
|
2922
|
+
if let Err(err) = finalize_active_backend(session, false) {
|
|
2923
|
+
eprintln!("[clips-tray] discard: backend finalize failed (continuing cleanup): {err}");
|
|
2924
|
+
}
|
|
2815
2925
|
for segment in &session.segments {
|
|
2816
2926
|
remove_recording_intent(segment);
|
|
2817
2927
|
let _ = std::fs::remove_file(segment);
|
|
@@ -2820,6 +2930,79 @@ fn discard_session(session: &mut NativeFullscreenSession) {
|
|
|
2820
2930
|
let _ = std::fs::remove_file(&session.path);
|
|
2821
2931
|
}
|
|
2822
2932
|
|
|
2933
|
+
#[cfg(test)]
|
|
2934
|
+
mod detached_discard_tests {
|
|
2935
|
+
use super::*;
|
|
2936
|
+
|
|
2937
|
+
/// A session with no live backend — the shape cancel sees after an
|
|
2938
|
+
/// aborted warm or a paused recording — so the discard path runs without
|
|
2939
|
+
/// capture hardware.
|
|
2940
|
+
fn hardware_free_session(path: PathBuf, segments: Vec<PathBuf>) -> NativeFullscreenSession {
|
|
2941
|
+
NativeFullscreenSession {
|
|
2942
|
+
backend: None,
|
|
2943
|
+
path,
|
|
2944
|
+
mime_type: MP4_RECORDING_MIME_TYPE,
|
|
2945
|
+
started_at: Instant::now(),
|
|
2946
|
+
width: None,
|
|
2947
|
+
height: None,
|
|
2948
|
+
segments,
|
|
2949
|
+
paused_total: Duration::ZERO,
|
|
2950
|
+
current_segment_started_at: Instant::now(),
|
|
2951
|
+
lost_segment_duration: Duration::ZERO,
|
|
2952
|
+
lost_segment_count: 0,
|
|
2953
|
+
paused_at: None,
|
|
2954
|
+
restart: RestartInfo {
|
|
2955
|
+
safe_id: "test".to_string(),
|
|
2956
|
+
include_audio: false,
|
|
2957
|
+
capture_system_audio: false,
|
|
2958
|
+
mic_captured_in_file: false,
|
|
2959
|
+
mic_device_id: None,
|
|
2960
|
+
mic_device_label: None,
|
|
2961
|
+
segment_counter: 1,
|
|
2962
|
+
target_display_id: None,
|
|
2963
|
+
capture_region: None,
|
|
2964
|
+
},
|
|
2965
|
+
pending_recording_output: false,
|
|
2966
|
+
custom_pipeline: false,
|
|
2967
|
+
audio_cleanup_applied: false,
|
|
2968
|
+
#[cfg(target_os = "macos")]
|
|
2969
|
+
live_upload: None,
|
|
2970
|
+
had_live_upload: false,
|
|
2971
|
+
disk_monitor_stop: None,
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
#[test]
|
|
2976
|
+
fn detached_discard_deletes_files_off_the_calling_thread() {
|
|
2977
|
+
let root = std::env::temp_dir().join(format!(
|
|
2978
|
+
"clips-detached-discard-test-{}-{}",
|
|
2979
|
+
std::process::id(),
|
|
2980
|
+
std::time::SystemTime::now()
|
|
2981
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
2982
|
+
.unwrap()
|
|
2983
|
+
.as_nanos()
|
|
2984
|
+
));
|
|
2985
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
2986
|
+
let path = root.join("recording.mp4");
|
|
2987
|
+
let segment = root.join("recording-seg2.mp4");
|
|
2988
|
+
std::fs::write(&path, b"video").unwrap();
|
|
2989
|
+
std::fs::write(&segment, b"video").unwrap();
|
|
2990
|
+
std::fs::write(recording_intent_path(&path), b"{}").unwrap();
|
|
2991
|
+
|
|
2992
|
+
let caller_thread = std::thread::current().id();
|
|
2993
|
+
let handle =
|
|
2994
|
+
spawn_detached_discard(hardware_free_session(path.clone(), vec![segment.clone()]));
|
|
2995
|
+
assert_ne!(handle.thread().id(), caller_thread);
|
|
2996
|
+
handle.join().unwrap();
|
|
2997
|
+
|
|
2998
|
+
assert!(!path.exists());
|
|
2999
|
+
assert!(!segment.exists());
|
|
3000
|
+
assert!(!recording_intent_path(&path).exists());
|
|
3001
|
+
|
|
3002
|
+
let _ = std::fs::remove_dir_all(root);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
|
|
2823
3006
|
/// Sibling path next to the original pending recording, numbered with
|
|
2824
3007
|
/// the segment counter so multiple resume cycles don't clobber each
|
|
2825
3008
|
/// other. Example: `clips-pending-recording-<id>-<pid>-seg2.mp4`.
|
|
@@ -2876,6 +3059,7 @@ fn start_segment_backend(
|
|
|
2876
3059
|
capture_region,
|
|
2877
3060
|
false,
|
|
2878
3061
|
false,
|
|
3062
|
+
None,
|
|
2879
3063
|
)
|
|
2880
3064
|
} else {
|
|
2881
3065
|
start_screencapturekit_backend_at(
|
|
@@ -2887,6 +3071,7 @@ fn start_segment_backend(
|
|
|
2887
3071
|
target_display_id,
|
|
2888
3072
|
capture_region,
|
|
2889
3073
|
false,
|
|
3074
|
+
None,
|
|
2890
3075
|
)
|
|
2891
3076
|
};
|
|
2892
3077
|
match sck_result {
|
|
@@ -2941,6 +3126,104 @@ fn start_segment_backend(
|
|
|
2941
3126
|
}
|
|
2942
3127
|
}
|
|
2943
3128
|
|
|
3129
|
+
/// How long a popover-open `SCShareableContent` prefetch stays usable for a
|
|
3130
|
+
/// recording start. Short on purpose: there is no display-configuration-change
|
|
3131
|
+
/// hook invalidating the snapshot, so the TTL is the staleness bound.
|
|
3132
|
+
#[cfg(target_os = "macos")]
|
|
3133
|
+
const SHAREABLE_CONTENT_PREFETCH_TTL: Duration = Duration::from_secs(15);
|
|
3134
|
+
|
|
3135
|
+
#[cfg(target_os = "macos")]
|
|
3136
|
+
static PREFETCHED_SHAREABLE_CONTENT: Mutex<Option<(Instant, SCShareableContent)>> =
|
|
3137
|
+
Mutex::new(None);
|
|
3138
|
+
|
|
3139
|
+
/// Set while a prefetch is running. The cache alone cannot debounce: it stays
|
|
3140
|
+
/// empty for the multi-second duration of `SCShareableContent::get`, so rapid
|
|
3141
|
+
/// popover reopens would each see "nothing cached" and pile up another
|
|
3142
|
+
/// blocking fetch.
|
|
3143
|
+
#[cfg(target_os = "macos")]
|
|
3144
|
+
static SHAREABLE_CONTENT_PREFETCH_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
|
|
3145
|
+
|
|
3146
|
+
/// Clears the in-flight flag however the prefetch ends, including a panic in
|
|
3147
|
+
/// the blocking task.
|
|
3148
|
+
#[cfg(target_os = "macos")]
|
|
3149
|
+
struct ShareableContentPrefetchGuard;
|
|
3150
|
+
|
|
3151
|
+
#[cfg(target_os = "macos")]
|
|
3152
|
+
impl Drop for ShareableContentPrefetchGuard {
|
|
3153
|
+
fn drop(&mut self) {
|
|
3154
|
+
SHAREABLE_CONTENT_PREFETCH_IN_FLIGHT.store(false, Ordering::SeqCst);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
/// Consume the popover-open prefetch for an initial recording start. Returns
|
|
3159
|
+
/// `None` (callers then do their own fresh fetch) when the snapshot is
|
|
3160
|
+
/// missing, expired, or does not contain the requested display — a monitor
|
|
3161
|
+
/// plugged in after the prefetch must not be resolved against stale content.
|
|
3162
|
+
/// Take-once so resume/rotation paths can never reuse an old snapshot.
|
|
3163
|
+
#[cfg(target_os = "macos")]
|
|
3164
|
+
fn take_prefetched_shareable_content(target_display_id: Option<u32>) -> Option<SCShareableContent> {
|
|
3165
|
+
let mut guard = PREFETCHED_SHAREABLE_CONTENT.lock().ok()?;
|
|
3166
|
+
let (fetched_at, content) = guard.take()?;
|
|
3167
|
+
if fetched_at.elapsed() > SHAREABLE_CONTENT_PREFETCH_TTL {
|
|
3168
|
+
return None;
|
|
3169
|
+
}
|
|
3170
|
+
if let Some(id) = target_display_id {
|
|
3171
|
+
if !content.displays().iter().any(|d| d.display_id() == id) {
|
|
3172
|
+
return None;
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
Some(content)
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
/// Fire-and-forget warm-up called when the recorder popover opens: fetch the
|
|
3179
|
+
/// multi-second `SCShareableContent` snapshot now so a recording start within
|
|
3180
|
+
/// the TTL skips it. Best-effort — failures are logged and the start paths
|
|
3181
|
+
/// fall back to their own fetch, so this can never fail a recording.
|
|
3182
|
+
#[tauri::command]
|
|
3183
|
+
pub async fn native_fullscreen_prefetch_capture_content() -> Result<(), String> {
|
|
3184
|
+
#[cfg(target_os = "macos")]
|
|
3185
|
+
{
|
|
3186
|
+
// Debounce rapid popover reopen toggles so blocking-pool fetches
|
|
3187
|
+
// don't pile up; a snapshot this young is fresh enough to keep.
|
|
3188
|
+
let recently_fetched = PREFETCHED_SHAREABLE_CONTENT
|
|
3189
|
+
.lock()
|
|
3190
|
+
.ok()
|
|
3191
|
+
.and_then(|guard| {
|
|
3192
|
+
guard
|
|
3193
|
+
.as_ref()
|
|
3194
|
+
.map(|(at, _)| at.elapsed() < SHAREABLE_CONTENT_PREFETCH_TTL / 3)
|
|
3195
|
+
})
|
|
3196
|
+
.unwrap_or(false);
|
|
3197
|
+
if recently_fetched {
|
|
3198
|
+
return Ok(());
|
|
3199
|
+
}
|
|
3200
|
+
// Coalesce with a prefetch that is already running rather than queueing
|
|
3201
|
+
// a second multi-second fetch behind it; the one in flight populates
|
|
3202
|
+
// the same cache this call would have.
|
|
3203
|
+
if SHAREABLE_CONTENT_PREFETCH_IN_FLIGHT
|
|
3204
|
+
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
|
3205
|
+
.is_err()
|
|
3206
|
+
{
|
|
3207
|
+
return Ok(());
|
|
3208
|
+
}
|
|
3209
|
+
let _in_flight = ShareableContentPrefetchGuard;
|
|
3210
|
+
match tauri::async_runtime::spawn_blocking(SCShareableContent::get).await {
|
|
3211
|
+
Ok(Ok(content)) => {
|
|
3212
|
+
if let Ok(mut guard) = PREFETCHED_SHAREABLE_CONTENT.lock() {
|
|
3213
|
+
*guard = Some((Instant::now(), content));
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
Ok(Err(err)) => {
|
|
3217
|
+
eprintln!("[clips-tray] shareable-content prefetch unavailable: {err:?}");
|
|
3218
|
+
}
|
|
3219
|
+
Err(join_err) => {
|
|
3220
|
+
eprintln!("[clips-tray] shareable-content prefetch task panicked: {join_err}");
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
Ok(())
|
|
3225
|
+
}
|
|
3226
|
+
|
|
2944
3227
|
/// Configure and start a fresh ScreenCaptureKit capture writing into
|
|
2945
3228
|
/// `output_path`. Shared by the initial start and the resume path.
|
|
2946
3229
|
#[cfg(target_os = "macos")]
|
|
@@ -2959,9 +3242,15 @@ pub(crate) fn start_screencapturekit_backend_at(
|
|
|
2959
3242
|
// the no-warm fallback), preserving the original record-immediately
|
|
2960
3243
|
// behavior.
|
|
2961
3244
|
defer_recording_output: bool,
|
|
3245
|
+
// Popover-open prefetch (initial start only — resume/rotation pass `None`
|
|
3246
|
+
// and keep their self-contained fresh fetch).
|
|
3247
|
+
prefetched_content: Option<SCShareableContent>,
|
|
2962
3248
|
) -> Result<(NativeFullscreenBackend, Option<u32>, Option<u32>), String> {
|
|
2963
|
-
let content =
|
|
2964
|
-
|
|
3249
|
+
let content = match prefetched_content {
|
|
3250
|
+
Some(content) => content,
|
|
3251
|
+
None => SCShareableContent::get()
|
|
3252
|
+
.map_err(|e| format!("shareable content lookup failed: {e:?}"))?,
|
|
3253
|
+
};
|
|
2965
3254
|
let displays = content.displays();
|
|
2966
3255
|
let display = target_display_id
|
|
2967
3256
|
.and_then(|id| displays.iter().find(|d| d.display_id() == id))
|
|
@@ -3682,7 +3971,7 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3682
3971
|
cookie: Option<String>,
|
|
3683
3972
|
) -> Result<NativeFullscreenUploadResult, String> {
|
|
3684
3973
|
if take_native_upload_retry_cancelled(&recording_id) {
|
|
3685
|
-
emit_native_upload_progress(&app, "paused", "Retry cancelled", None, None);
|
|
3974
|
+
emit_native_upload_progress(&app, &recording_id, "paused", "Retry cancelled", None, None);
|
|
3686
3975
|
return Err(NATIVE_UPLOAD_RETRY_CANCELLED.to_string());
|
|
3687
3976
|
}
|
|
3688
3977
|
let mut saved = read_saved_recording_metadata(&app, &recording_id)?;
|
|
@@ -3769,6 +4058,7 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3769
4058
|
NativeRetryUploadPlan::Resume(resume) => {
|
|
3770
4059
|
emit_native_upload_progress(
|
|
3771
4060
|
&app,
|
|
4061
|
+
&recording_id,
|
|
3772
4062
|
"uploading",
|
|
3773
4063
|
"Resuming upload",
|
|
3774
4064
|
None,
|
|
@@ -3787,6 +4077,7 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3787
4077
|
} => {
|
|
3788
4078
|
emit_native_upload_progress(
|
|
3789
4079
|
&app,
|
|
4080
|
+
&recording_id,
|
|
3790
4081
|
"uploading",
|
|
3791
4082
|
"Restarting upload",
|
|
3792
4083
|
None,
|
|
@@ -3929,14 +4220,21 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3929
4220
|
Err(err) => {
|
|
3930
4221
|
clear_native_upload_retry_cancelled(&recording_id);
|
|
3931
4222
|
if err == NATIVE_UPLOAD_RETRY_CANCELLED {
|
|
3932
|
-
emit_native_upload_progress(
|
|
4223
|
+
emit_native_upload_progress(
|
|
4224
|
+
&app,
|
|
4225
|
+
&recording_id,
|
|
4226
|
+
"paused",
|
|
4227
|
+
"Retry cancelled",
|
|
4228
|
+
None,
|
|
4229
|
+
None,
|
|
4230
|
+
);
|
|
3933
4231
|
return Err(err);
|
|
3934
4232
|
}
|
|
3935
4233
|
if is_moov_corrupt_error(&err) {
|
|
3936
4234
|
saved.corrupt = true;
|
|
3937
4235
|
}
|
|
3938
4236
|
persist_saved_recording_error(&app, &mut saved, &err);
|
|
3939
|
-
emit_native_upload_progress(&app, "failed", "Retry paused", None, None);
|
|
4237
|
+
emit_native_upload_progress(&app, &recording_id, "failed", "Retry paused", None, None);
|
|
3940
4238
|
let suffix = if saved.corrupt {
|
|
3941
4239
|
"The file is corrupted and cannot be recovered."
|
|
3942
4240
|
} else {
|
|
@@ -4920,6 +5218,7 @@ fn start_screencapturekit_recording(
|
|
|
4920
5218
|
capture_region,
|
|
4921
5219
|
defer_recording_output,
|
|
4922
5220
|
false,
|
|
5221
|
+
take_prefetched_shareable_content(target_display_id),
|
|
4923
5222
|
)?
|
|
4924
5223
|
} else {
|
|
4925
5224
|
start_screencapturekit_backend_at(
|
|
@@ -4931,6 +5230,7 @@ fn start_screencapturekit_recording(
|
|
|
4931
5230
|
target_display_id,
|
|
4932
5231
|
capture_region,
|
|
4933
5232
|
defer_recording_output,
|
|
5233
|
+
take_prefetched_shareable_content(target_display_id),
|
|
4934
5234
|
)?
|
|
4935
5235
|
};
|
|
4936
5236
|
let (fallback_width, fallback_height) = primary_monitor_size(app);
|
|
@@ -5216,7 +5516,9 @@ pub(crate) fn stop_native_recording(
|
|
|
5216
5516
|
wait_for_finalize: bool,
|
|
5217
5517
|
) -> Result<(), String> {
|
|
5218
5518
|
match backend {
|
|
5219
|
-
NativeFullscreenBackend::Screencapture { child } =>
|
|
5519
|
+
NativeFullscreenBackend::Screencapture { child } => {
|
|
5520
|
+
stop_screencapture(child, wait_for_finalize)
|
|
5521
|
+
}
|
|
5220
5522
|
#[cfg(target_os = "macos")]
|
|
5221
5523
|
NativeFullscreenBackend::CustomScreenCaptureKit {
|
|
5222
5524
|
stream,
|
|
@@ -5318,7 +5620,7 @@ pub(crate) fn stop_native_recording(
|
|
|
5318
5620
|
}
|
|
5319
5621
|
}
|
|
5320
5622
|
|
|
5321
|
-
fn stop_screencapture(child: &mut Child) -> Result<(), String> {
|
|
5623
|
+
fn stop_screencapture(child: &mut Child, wait_for_finalize: bool) -> Result<(), String> {
|
|
5322
5624
|
if child
|
|
5323
5625
|
.try_wait()
|
|
5324
5626
|
.map_err(|e| format!("screencapture status check failed: {e}"))?
|
|
@@ -5336,6 +5638,26 @@ fn stop_screencapture(child: &mut Child) -> Result<(), String> {
|
|
|
5336
5638
|
.stderr(Stdio::null())
|
|
5337
5639
|
.status();
|
|
5338
5640
|
|
|
5641
|
+
// Discard path: the movie file is deleted immediately after this returns,
|
|
5642
|
+
// so the long graceful wait for `screencapture` to finish writing it just
|
|
5643
|
+
// delays the cancel. Give SIGINT a short grace, then hard-kill and reap.
|
|
5644
|
+
if !wait_for_finalize {
|
|
5645
|
+
let grace_deadline = Instant::now() + Duration::from_millis(250);
|
|
5646
|
+
while Instant::now() < grace_deadline {
|
|
5647
|
+
if child
|
|
5648
|
+
.try_wait()
|
|
5649
|
+
.map_err(|e| format!("screencapture wait failed: {e}"))?
|
|
5650
|
+
.is_some()
|
|
5651
|
+
{
|
|
5652
|
+
return Ok(());
|
|
5653
|
+
}
|
|
5654
|
+
std::thread::sleep(Duration::from_millis(25));
|
|
5655
|
+
}
|
|
5656
|
+
let _ = child.kill();
|
|
5657
|
+
let _ = child.wait();
|
|
5658
|
+
return Ok(());
|
|
5659
|
+
}
|
|
5660
|
+
|
|
5339
5661
|
let deadline = Instant::now() + Duration::from_secs(15);
|
|
5340
5662
|
loop {
|
|
5341
5663
|
if child
|
|
@@ -5370,6 +5692,7 @@ pub(crate) async fn upload_finalized_native_artifact(
|
|
|
5370
5692
|
) -> Result<NativeFullscreenUploadResult, String> {
|
|
5371
5693
|
let prepared = prepare_recording_file(
|
|
5372
5694
|
app,
|
|
5695
|
+
&recording_id,
|
|
5373
5696
|
&artifact.path,
|
|
5374
5697
|
artifact.mime_type,
|
|
5375
5698
|
artifact.width,
|
|
@@ -5437,6 +5760,7 @@ fn prepare_saved_recording_file(
|
|
|
5437
5760
|
.to_path_buf();
|
|
5438
5761
|
let prepared = prepare_recording_file(
|
|
5439
5762
|
app,
|
|
5763
|
+
&saved.recording_id,
|
|
5440
5764
|
&source_path,
|
|
5441
5765
|
&saved.mime_type,
|
|
5442
5766
|
saved.width,
|
|
@@ -5593,6 +5917,7 @@ async fn upload_prepared_recording_file(
|
|
|
5593
5917
|
.unwrap_or(0);
|
|
5594
5918
|
emit_native_upload_progress(
|
|
5595
5919
|
app,
|
|
5920
|
+
&recording_id,
|
|
5596
5921
|
"uploading",
|
|
5597
5922
|
if streaming_resume.is_some() {
|
|
5598
5923
|
"Resuming upload"
|
|
@@ -5661,6 +5986,7 @@ async fn upload_prepared_recording_file(
|
|
|
5661
5986
|
}?;
|
|
5662
5987
|
emit_native_upload_progress(
|
|
5663
5988
|
app,
|
|
5989
|
+
&recording_id,
|
|
5664
5990
|
"uploading",
|
|
5665
5991
|
"Uploading clip",
|
|
5666
5992
|
None,
|
|
@@ -5676,6 +6002,7 @@ async fn upload_prepared_recording_file(
|
|
|
5676
6002
|
|
|
5677
6003
|
emit_native_upload_progress(
|
|
5678
6004
|
app,
|
|
6005
|
+
&recording_id,
|
|
5679
6006
|
"processing",
|
|
5680
6007
|
"Uploading clip",
|
|
5681
6008
|
None,
|
|
@@ -5747,6 +6074,7 @@ async fn upload_prepared_recording_file(
|
|
|
5747
6074
|
}?;
|
|
5748
6075
|
emit_native_upload_progress(
|
|
5749
6076
|
app,
|
|
6077
|
+
&recording_id,
|
|
5750
6078
|
"uploading",
|
|
5751
6079
|
"Uploading clip",
|
|
5752
6080
|
None,
|
|
@@ -5756,6 +6084,7 @@ async fn upload_prepared_recording_file(
|
|
|
5756
6084
|
|
|
5757
6085
|
emit_native_upload_progress(
|
|
5758
6086
|
app,
|
|
6087
|
+
&recording_id,
|
|
5759
6088
|
"processing",
|
|
5760
6089
|
"Uploading clip",
|
|
5761
6090
|
None,
|
|
@@ -5790,7 +6119,14 @@ async fn upload_prepared_recording_file(
|
|
|
5790
6119
|
}?;
|
|
5791
6120
|
}
|
|
5792
6121
|
|
|
5793
|
-
emit_native_upload_progress(
|
|
6122
|
+
emit_native_upload_progress(
|
|
6123
|
+
app,
|
|
6124
|
+
&recording_id,
|
|
6125
|
+
"opening",
|
|
6126
|
+
"Uploading clip",
|
|
6127
|
+
None,
|
|
6128
|
+
Some(1.0),
|
|
6129
|
+
);
|
|
5794
6130
|
Ok(NativeFullscreenUploadResult {
|
|
5795
6131
|
recording_id,
|
|
5796
6132
|
duration_ms: verified_local_duration_ms,
|
|
@@ -5853,6 +6189,7 @@ async fn get_native_retry_upload_plan(
|
|
|
5853
6189
|
if tokio::time::Instant::now() + delay <= deadline {
|
|
5854
6190
|
emit_native_upload_progress(
|
|
5855
6191
|
app,
|
|
6192
|
+
recording_id,
|
|
5856
6193
|
"uploading",
|
|
5857
6194
|
"Waiting for prior retry",
|
|
5858
6195
|
None,
|
|
@@ -7138,6 +7475,9 @@ fn verify_prepared_audio_signal(
|
|
|
7138
7475
|
|
|
7139
7476
|
fn prepare_recording_file(
|
|
7140
7477
|
app: &AppHandle,
|
|
7478
|
+
// Only so the compression progress this emits can name its take; the
|
|
7479
|
+
// preparation itself is per-file and knows nothing about the recording.
|
|
7480
|
+
recording_id: &str,
|
|
7141
7481
|
path: &Path,
|
|
7142
7482
|
mime_type: &str,
|
|
7143
7483
|
width: Option<u32>,
|
|
@@ -7189,7 +7529,14 @@ fn prepare_recording_file(
|
|
|
7189
7529
|
);
|
|
7190
7530
|
}
|
|
7191
7531
|
}
|
|
7192
|
-
emit_native_upload_progress(
|
|
7532
|
+
emit_native_upload_progress(
|
|
7533
|
+
app,
|
|
7534
|
+
recording_id,
|
|
7535
|
+
"preparing",
|
|
7536
|
+
"Optimizing clip",
|
|
7537
|
+
None,
|
|
7538
|
+
None,
|
|
7539
|
+
);
|
|
7193
7540
|
|
|
7194
7541
|
let original = PreparedRecordingFile {
|
|
7195
7542
|
path: path.to_path_buf(),
|
|
@@ -7289,6 +7636,7 @@ fn prepare_recording_file(
|
|
|
7289
7636
|
for (index, preset) in presets.iter().enumerate() {
|
|
7290
7637
|
emit_native_upload_progress(
|
|
7291
7638
|
app,
|
|
7639
|
+
&recording_id,
|
|
7292
7640
|
"compressing",
|
|
7293
7641
|
"Optimizing clip",
|
|
7294
7642
|
None,
|
|
@@ -7389,6 +7737,7 @@ fn prepare_recording_file(
|
|
|
7389
7737
|
}
|
|
7390
7738
|
emit_native_upload_progress(
|
|
7391
7739
|
app,
|
|
7740
|
+
&recording_id,
|
|
7392
7741
|
"compressing",
|
|
7393
7742
|
"Optimizing clip",
|
|
7394
7743
|
None,
|
|
@@ -7424,6 +7773,7 @@ fn prepare_recording_file(
|
|
|
7424
7773
|
for (index, preset) in presets.iter().enumerate() {
|
|
7425
7774
|
emit_native_upload_progress(
|
|
7426
7775
|
app,
|
|
7776
|
+
&recording_id,
|
|
7427
7777
|
"compressing",
|
|
7428
7778
|
"Optimizing clip",
|
|
7429
7779
|
None,
|
|
@@ -7510,6 +7860,7 @@ fn prepare_recording_file(
|
|
|
7510
7860
|
}
|
|
7511
7861
|
emit_native_upload_progress(
|
|
7512
7862
|
app,
|
|
7863
|
+
&recording_id,
|
|
7513
7864
|
"compressing",
|
|
7514
7865
|
"Optimizing clip",
|
|
7515
7866
|
None,
|