Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
75df302
perf(rendering): initialize screen and camera decoders concurrently
richiemcilroy Jul 3, 2026
a3a3e7d
feat(editor): add AudioLoader for background audio decoding
richiemcilroy Jul 3, 2026
c358628
feat(export): await background audio decodes before export
richiemcilroy Jul 3, 2026
972d3f0
feat(editor): add persistent AudioOutput session stream
richiemcilroy Jul 3, 2026
660f13a
perf(editor): progressive audio pre-render for faster play start
richiemcilroy Jul 3, 2026
24a3bea
refactor(editor): route playback through persistent audio output
richiemcilroy Jul 3, 2026
5be7c2a
feat(editor): add play-start latency telemetry events
richiemcilroy Jul 3, 2026
8ddc65c
test(editor): extend playback benchmark with press-start metrics
richiemcilroy Jul 3, 2026
ce13433
docs(editor): document play-start latency optimization findings
richiemcilroy Jul 3, 2026
c7464ab
fix(macos): stop disabling window occlusion for liquid glass
richiemcilroy Jul 3, 2026
eac5c35
feat(desktop): show windows early with native background color
richiemcilroy Jul 3, 2026
4b61f56
feat(desktop): pre-render first frame and lazy-load waveforms
richiemcilroy Jul 3, 2026
e7ce872
test(desktop): wire AudioOutput into display transport benchmark
richiemcilroy Jul 3, 2026
5019ce5
fix(desktop): load waveforms without suspending editor UI
richiemcilroy Jul 3, 2026
a2ce530
fix(desktop): reveal transparent editor without throttled rAF
richiemcilroy Jul 3, 2026
7cb2323
fix(desktop): add custom domain query placeholder data
richiemcilroy Jul 3, 2026
d499458
fix(desktop): parse JSON content-type with charset suffix
richiemcilroy Jul 3, 2026
56d49e8
fix(desktop): use licenseQuery key when refetching license state
richiemcilroy Jul 3, 2026
640d5b9
refactor(desktop): simplify changelog settings page loading
richiemcilroy Jul 3, 2026
e6a890b
perf(desktop): use font-display block for bundled fonts
richiemcilroy Jul 3, 2026
f2447fb
perf(desktop): prewarm font and emoji caches on app mount
richiemcilroy Jul 3, 2026
6f11977
style(desktop): remove redundant cursor-pointer from editor UI
richiemcilroy Jul 3, 2026
ca49eed
style(desktop): remove redundant cursor-pointer from settings UI
richiemcilroy Jul 3, 2026
0e3bce3
style(desktop): remove redundant cursor-pointer from screenshot editor
richiemcilroy Jul 3, 2026
7089fb2
style(desktop): remove fade-in animations on window open
richiemcilroy Jul 3, 2026
581c9bf
build(desktop): pin tauri-plugin-http to 2.5.2
richiemcilroy Jul 3, 2026
9f7e1e3
chore(desktop): regenerate tauri specta bindings
richiemcilroy Jul 3, 2026
0a38ed1
lockfile fix
richiemcilroy Jul 3, 2026
f26fb5d
fix: avoid duplicate audio decode warnings
richiemcilroy Jul 3, 2026
1157ee5
fix: derive editor preview pre-render size
richiemcilroy Jul 3, 2026
b519947
fix: restore changelog render error boundary
richiemcilroy Jul 3, 2026
2b0a911
fix: bound long audio playback buffering
richiemcilroy Jul 3, 2026
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
23 changes: 23 additions & 0 deletions apps/desktop/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ import tsconfigPaths from "vite-tsconfig-paths";

const enableSolidDevtools = !!process.env.VITE_SOLID_DEVTOOLS;

// Bundled fonts load from local assets near-instantly, so `block` is safe and
// avoids the fallback-font flash (FOUT) that `swap` causes on every window
// open. Desktop-only: web keeps `swap` for slow networks.
// No `enforce`: must run after vite:css has inlined the virtual module's
// @import statements (a `pre` transform only sees the un-inlined imports).
const fontDisplayBlock = {
name: "cap:font-display-block",
transform(code: string, id: string) {
// unplugin-fonts inlines the @fontsource CSS into its virtual
// "unfonts.css" module, so match that as well as direct imports.
const [file] = id.split("?");
const isFontCss =
file.endsWith("unfonts.css") ||
(file.includes("@fontsource") && file.endsWith(".css"));
if (!isFontCss) return;
return {
code: code.replace(/font-display:\s*swap;/g, "font-display: block;"),
map: null,
};
Comment on lines +25 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing: this returns a transformed result even when there are no font-display: swap matches, which can cause unnecessary downstream work.

Suggested change
return {
code: code.replace(/font-display:\s*swap;/g, "font-display: block;"),
map: null,
};
const updated = code.replace(/font-display:\s*swap;/g, "font-display: block;");
if (updated === code) return;
return {
code: updated,
map: null,
};

},
};

export default defineConfig({
ssr: false,
server: { preset: "static" },
Expand All @@ -31,6 +53,7 @@ export default defineConfig({
assetsInclude: ["**/*.riv"],
plugins: [
...(enableSolidDevtools ? [devtools({ autoname: true })] : []),
fontDisplayBlock,
wasm(),
topLevelAwait(),
capUIPlugin,
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@
"@tauri-apps/plugin-deep-link": "^2.4.1",
"@tauri-apps/plugin-dialog": "2.4.2",
"@tauri-apps/plugin-fs": "2.4.2",
"@tauri-apps/plugin-http": "^2.5.1",
"@tauri-apps/plugin-http": "2.5.2",
"@tauri-apps/plugin-notification": "^2.3.0",
"@tauri-apps/plugin-opener": "^2.5.0",
"@tauri-apps/plugin-os": "^2.3.0",
"@tauri-apps/plugin-process": "2.3.0",
"@tauri-apps/plugin-shell": "^2.3.0",
"@tauri-apps/plugin-store": "^2.4.0",
"@tauri-apps/plugin-updater": "^2.9.0",
"@tauri-apps/plugin-updater": "2.9.0",
"@ts-rest/core": "^3.52.1",
"@types/react-tooltip": "^4.2.4",
"cva": "npm:class-variance-authority@^0.7.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ tauri-specta = { version = "=2.0.0-rc.20", features = ["derive", "typescript"] }
tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.0"
tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-http = "2.2.0"
tauri-plugin-http = "=2.5.2"
tauri-plugin-notification = "2.2.0"
tauri-plugin-os = "2.2.0"
tauri-plugin-process = "2.2.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ impl Summary {
PlaybackTelemetryEvent::RendererSendFailed { .. } => {
self.send_failures += 1;
}
PlaybackTelemetryEvent::AudioSegmentsResolved { .. }
| PlaybackTelemetryEvent::AudioPipelineReady { .. }
| PlaybackTelemetryEvent::ClockStarted { .. } => {}
}
}

Expand Down Expand Up @@ -307,13 +310,15 @@ async fn main() {
};

let (_project_tx, project_rx) = watch::channel(project);
let audio_output = Arc::new(cap_editor::AudioOutput::new());
let playback = Playback {
renderer: renderer.clone(),
render_constants,
start_frame_number: 0,
project: project_rx,
segment_medias,
music: cap_editor::MusicTracks::new(),
audio_output,
telemetry: Some(telemetry),
};

Expand Down
91 changes: 79 additions & 12 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,65 @@ use tauri::menu::{
type FinalizingRecordingsMap =
std::collections::HashMap<PathBuf, (watch::Sender<bool>, watch::Receiver<bool>)>;

const EDITOR_PREVIEW_FPS: u32 = 60;
const EDITOR_OUTPUT_SIZE: XY<u32> = XY::new(1920, 1080);
const DEFAULT_EDITOR_PREVIEW_SCALE_NUMERATOR: u32 = 65;
const DEFAULT_EDITOR_PREVIEW_SCALE_DENOMINATOR: u32 = 100;

fn default_editor_preview_resolution() -> XY<u32> {
scaled_editor_preview_resolution(
EDITOR_OUTPUT_SIZE,
DEFAULT_EDITOR_PREVIEW_SCALE_NUMERATOR,
DEFAULT_EDITOR_PREVIEW_SCALE_DENOMINATOR,
)
}

fn scaled_editor_preview_resolution(
output_size: XY<u32>,
numerator: u32,
denominator: u32,
) -> XY<u32> {
XY::new(
scaled_editor_preview_dimension(output_size.x, numerator, denominator, 4, 4),
scaled_editor_preview_dimension(output_size.y, numerator, denominator, 2, 2),
)
}

fn scaled_editor_preview_dimension(
value: u32,
numerator: u32,
denominator: u32,
minimum: u32,
alignment: u32,
) -> u32 {
let denominator = denominator.max(1);
let alignment = alignment.max(1);
let scaled = ((u64::from(value) * u64::from(numerator)) + (u64::from(denominator) / 2))
/ u64::from(denominator);
let rounded = u32::try_from(scaled).unwrap_or(u32::MAX).max(minimum);
let aligned = u64::from(rounded).div_ceil(u64::from(alignment)) * u64::from(alignment);

u32::try_from(aligned).unwrap_or(u32::MAX)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn default_editor_preview_resolution_matches_frontend_defaults() {
assert_eq!(default_editor_preview_resolution(), XY::new(1248, 702));
}

#[test]
fn scaled_editor_preview_resolution_rounds_like_frontend() {
assert_eq!(
scaled_editor_preview_resolution(XY::new(1919, 1079), 65, 100),
XY::new(1248, 702)
);
}
}

#[derive(Default)]
pub struct FinalizingRecordings {
recordings: std::sync::Mutex<FinalizingRecordingsMap>,
Expand Down Expand Up @@ -2850,10 +2909,6 @@ async fn create_editor_instance(window: Window) -> Result<SerializedEditorInstan

let editor_instance = EditorInstances::get_or_create(&window, path).await?;

let meta = editor_instance.meta();

println!("Pretty name: {}", meta.pretty_name);

Ok(SerializedEditorInstance {
frames_socket_url: format!("ws://localhost:{}", editor_instance.ws_port),
recording_duration: editor_instance.recordings.duration(),
Expand Down Expand Up @@ -4104,10 +4159,15 @@ async fn get_mic_waveforms(editor_instance: WindowEditorInstance) -> Result<Vec<
let mut out = Vec::new();

for segment in editor_instance.segment_medias.iter() {
if let Some(audio) = &segment.audio {
out.push(audio::get_waveform(audio));
} else {
out.push(Vec::new());
// Waits for the background decode; a failed track just renders as an
// empty waveform (playback/export surface the actual error).
match segment.audio.get().await {
Ok(Some(audio)) => out.push(audio::get_waveform(&audio)),
Ok(None) => out.push(Vec::new()),
Err(error) => {
warn!(%error, "Mic audio failed to load; returning empty waveform");
out.push(Vec::new());
}
}
}

Expand All @@ -4123,10 +4183,13 @@ async fn get_system_audio_waveforms(
let mut out = Vec::new();

for segment in editor_instance.segment_medias.iter() {
if let Some(audio) = &segment.system_audio {
out.push(audio::get_waveform(audio));
} else {
out.push(Vec::new());
match segment.system_audio.get().await {
Ok(Some(audio)) => out.push(audio::get_waveform(&audio)),
Ok(None) => out.push(Vec::new()),
Err(error) => {
warn!(%error, "System audio failed to load; returning empty waveform");
out.push(Vec::new());
}
}
}

Expand Down Expand Up @@ -6135,6 +6198,10 @@ async fn create_editor_instance_impl(
}
});

instance
.preview_tx
.send_modify(|v| *v = Some((0, EDITOR_PREVIEW_FPS, default_editor_preview_resolution())));

Ok((instance, event_id))
}

Expand Down
Loading
Loading