Skip to content

Commit f9b00ef

Browse files
Merge pull request #1484 from CapSoftware/recording-benchmarks
Improved real-world recording benchmarks
2 parents 7205c99 + c9c337a commit f9b00ef

File tree

7 files changed

+1368
-36
lines changed

7 files changed

+1368
-36
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,6 +1442,24 @@ async fn create_editor_instance(window: Window) -> Result<SerializedEditorInstan
14421442
})
14431443
}
14441444

1445+
#[tauri::command]
1446+
#[specta::specta]
1447+
#[instrument(skip(window))]
1448+
async fn get_editor_project_path(window: Window) -> Result<PathBuf, String> {
1449+
let CapWindowId::Editor { id } = CapWindowId::from_str(window.label()).unwrap() else {
1450+
return Err("Invalid window".to_string());
1451+
};
1452+
1453+
let window_ids = EditorWindowIds::get(window.app_handle());
1454+
let window_ids = window_ids.ids.lock().unwrap();
1455+
1456+
let Some((path, _)) = window_ids.iter().find(|(_, _id)| *_id == id) else {
1457+
return Err("Editor instance not found".to_string());
1458+
};
1459+
1460+
Ok(path.clone())
1461+
}
1462+
14451463
#[tauri::command]
14461464
#[specta::specta]
14471465
#[instrument(skip(editor))]
@@ -2472,6 +2490,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
24722490
open_file_path,
24732491
get_video_metadata,
24742492
create_editor_instance,
2493+
get_editor_project_path,
24752494
get_mic_waveforms,
24762495
get_system_audio_waveforms,
24772496
start_playback,

apps/desktop/src/routes/editor/Editor.tsx

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Menu } from "@tauri-apps/api/menu";
1111
import {
1212
createEffect,
1313
createMemo,
14+
createResource,
1415
createSignal,
1516
Match,
1617
on,
@@ -41,6 +42,7 @@ import {
4142
useEditorContext,
4243
useEditorInstanceContext,
4344
} from "./context";
45+
import { EditorErrorScreen } from "./EditorErrorScreen";
4446
import { ExportPage } from "./ExportPage";
4547
import { Header } from "./Header";
4648
import { PlayerContent } from "./Player";
@@ -54,38 +56,61 @@ const RESIZE_HANDLE_HEIGHT = 8;
5456
const MIN_PLAYER_HEIGHT = MIN_PLAYER_CONTENT_HEIGHT + RESIZE_HANDLE_HEIGHT;
5557

5658
export function Editor() {
59+
const [projectPath] = createResource(() => commands.getEditorProjectPath());
60+
5761
return (
5862
<EditorInstanceContextProvider>
59-
<Show
60-
when={(() => {
61-
const ctx = useEditorInstanceContext();
62-
const editorInstance = ctx.editorInstance();
63-
64-
if (!editorInstance || !ctx.metaQuery.data) return;
65-
66-
return {
67-
editorInstance,
68-
meta() {
69-
const d = ctx.metaQuery.data;
70-
if (!d)
71-
throw new Error(
72-
"metaQuery.data is undefined - how did this happen?",
73-
);
74-
return d;
75-
},
76-
refetchMeta: async () => {
77-
await ctx.metaQuery.refetch();
78-
},
79-
};
80-
})()}
81-
>
63+
<EditorContent projectPath={projectPath()} />
64+
</EditorInstanceContextProvider>
65+
);
66+
}
67+
68+
function EditorContent(props: { projectPath: string | undefined }) {
69+
const ctx = useEditorInstanceContext();
70+
71+
const errorInfo = () => {
72+
const error = ctx.editorInstance.error;
73+
if (!error || !props.projectPath) return null;
74+
const errorMessage = error instanceof Error ? error.message : String(error);
75+
return { error: errorMessage, projectPath: props.projectPath };
76+
};
77+
78+
const readyData = () => {
79+
const editorInstance = ctx.editorInstance();
80+
if (!editorInstance || !ctx.metaQuery.data) return null;
81+
82+
return {
83+
editorInstance,
84+
meta() {
85+
const d = ctx.metaQuery.data;
86+
if (!d)
87+
throw new Error("metaQuery.data is undefined - how did this happen?");
88+
return d;
89+
},
90+
refetchMeta: async () => {
91+
await ctx.metaQuery.refetch();
92+
},
93+
};
94+
};
95+
96+
return (
97+
<Switch>
98+
<Match when={errorInfo()}>
99+
{(info) => (
100+
<EditorErrorScreen
101+
error={info().error}
102+
projectPath={info().projectPath}
103+
/>
104+
)}
105+
</Match>
106+
<Match when={readyData()}>
82107
{(values) => (
83108
<EditorContextProvider {...values()}>
84109
<Inner />
85110
</EditorContextProvider>
86111
)}
87-
</Show>
88-
</EditorInstanceContextProvider>
112+
</Match>
113+
</Switch>
89114
);
90115
}
91116

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { Button } from "@cap/ui-solid";
2+
import { createMutation } from "@tanstack/solid-query";
3+
import { revealItemInDir } from "@tauri-apps/plugin-opener";
4+
import { type as ostype } from "@tauri-apps/plugin-os";
5+
import { Show } from "solid-js";
6+
import CaptionControlsWindows11 from "~/components/titlebar/controls/CaptionControlsWindows11";
7+
import { commands } from "~/utils/tauri";
8+
import IconAlertTriangle from "~icons/lucide/alert-triangle";
9+
import IconFolder from "~icons/lucide/folder";
10+
import IconLoaderCircle from "~icons/lucide/loader-circle";
11+
import IconRefreshCw from "~icons/lucide/refresh-cw";
12+
13+
const NEEDS_RECOVERY_PATTERN = /may need to be recovered/i;
14+
15+
function isRecoveryNeededError(error: string): boolean {
16+
return NEEDS_RECOVERY_PATTERN.test(error);
17+
}
18+
19+
export function EditorErrorScreen(props: {
20+
error: string;
21+
projectPath: string;
22+
}) {
23+
const needsRecovery = () => isRecoveryNeededError(props.error);
24+
const isMac = () => ostype() === "macos";
25+
26+
const recoverMutation = createMutation(() => ({
27+
mutationFn: async () => {
28+
const result = await commands.recoverRecording(props.projectPath);
29+
await commands.showWindow({ Editor: { project_path: result } });
30+
return result;
31+
},
32+
onSuccess: () => {
33+
window.location.reload();
34+
},
35+
}));
36+
37+
const handleOpenFolder = () => {
38+
revealItemInDir(props.projectPath);
39+
};
40+
41+
return (
42+
<div class="flex flex-col flex-1 min-h-0">
43+
<div
44+
data-tauri-drag-region
45+
class="flex relative flex-row items-center w-full h-14 px-4"
46+
>
47+
{isMac() && <div class="h-full w-[4rem]" />}
48+
<div data-tauri-drag-region class="flex-1 h-full" />
49+
{ostype() === "windows" && <CaptionControlsWindows11 />}
50+
</div>
51+
52+
<div class="flex-1 flex items-center justify-center p-8">
53+
<div class="max-w-md w-full space-y-6">
54+
<div class="flex flex-col items-center text-center space-y-3">
55+
<div class="size-16 rounded-full bg-red-2 flex items-center justify-center">
56+
<IconAlertTriangle class="size-8 text-red-9" />
57+
</div>
58+
<h2 class="text-xl font-semibold text-gray-12">
59+
{needsRecovery()
60+
? "Recording Needs Recovery"
61+
: "Unable to Open Recording"}
62+
</h2>
63+
<p class="text-sm text-gray-11">{props.error}</p>
64+
</div>
65+
66+
<Show when={needsRecovery()}>
67+
<div class="bg-gray-2 border border-gray-4 rounded-xl p-4 space-y-4">
68+
<div class="space-y-2">
69+
<h3 class="font-medium text-gray-12 text-sm">
70+
Automatic Recovery
71+
</h3>
72+
<p class="text-xs text-gray-11">
73+
Cap can attempt to recover your recording automatically. This
74+
will reconstruct the recording from available segment data.
75+
</p>
76+
</div>
77+
78+
<Button
79+
onClick={() => recoverMutation.mutate()}
80+
disabled={recoverMutation.isPending}
81+
variant="primary"
82+
class="w-full"
83+
>
84+
<Show
85+
when={recoverMutation.isPending}
86+
fallback={
87+
<>
88+
<IconRefreshCw class="size-4 mr-2" />
89+
Recover Recording
90+
</>
91+
}
92+
>
93+
<IconLoaderCircle class="size-4 mr-2 animate-spin" />
94+
Recovering...
95+
</Show>
96+
</Button>
97+
98+
<Show when={recoverMutation.error}>
99+
<div class="bg-red-2 border border-red-6 rounded-lg p-3">
100+
<p class="text-red-11 text-xs">
101+
Recovery failed:{" "}
102+
{recoverMutation.error instanceof Error
103+
? recoverMutation.error.message
104+
: String(recoverMutation.error)}
105+
</p>
106+
</div>
107+
</Show>
108+
</div>
109+
</Show>
110+
111+
<div class="bg-gray-2 border border-gray-4 rounded-xl p-4 space-y-4">
112+
<div class="space-y-2">
113+
<h3 class="font-medium text-gray-12 text-sm">
114+
Manual Investigation
115+
</h3>
116+
<p class="text-xs text-gray-11">
117+
You can open the recording folder to inspect the raw files
118+
directly.
119+
</p>
120+
121+
<div class="bg-gray-3 rounded-lg p-3 space-y-2">
122+
<p class="text-xs font-mono text-gray-11 break-all">
123+
{props.projectPath}
124+
</p>
125+
<Show
126+
when={isMac()}
127+
fallback={
128+
<p class="text-xs text-gray-10 italic">
129+
Tip: Double-click inside the folder to browse the
130+
contents.
131+
</p>
132+
}
133+
>
134+
<p class="text-xs text-gray-10 italic">
135+
Tip: Right-click and select "Show Enclosing Folder" to see
136+
the .cap bundle contents.
137+
</p>
138+
</Show>
139+
</div>
140+
</div>
141+
142+
<Button onClick={handleOpenFolder} variant="outline" class="w-full">
143+
<IconFolder class="size-4 mr-2" />
144+
Open Folder
145+
</Button>
146+
</div>
147+
148+
<div class="flex justify-center">
149+
<button
150+
type="button"
151+
onClick={() => window.close()}
152+
class="text-sm text-gray-10 hover:text-gray-11 transition-colors"
153+
>
154+
Close Window
155+
</button>
156+
</div>
157+
</div>
158+
</div>
159+
</div>
160+
);
161+
}

apps/desktop/src/utils/tauri.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ async getVideoMetadata(path: string) : Promise<VideoRecordingMetadata> {
116116
async createEditorInstance() : Promise<SerializedEditorInstance> {
117117
return await TAURI_INVOKE("create_editor_instance");
118118
},
119+
async getEditorProjectPath() : Promise<string> {
120+
return await TAURI_INVOKE("get_editor_project_path");
121+
},
119122
async getMicWaveforms() : Promise<number[][]> {
120123
return await TAURI_INVOKE("get_mic_waveforms");
121124
},

0 commit comments

Comments
 (0)