Desktop editor startup and playback latency optimizations#1975
Conversation
| // so the clock below never runs ahead of audible audio. | ||
| let audio_spawn_start = Instant::now(); | ||
| let audio_generation = | ||
| if audio_segments.is_empty() || audio_segments[0].tracks.is_empty() { |
There was a problem hiding this comment.
This condition only looks at the first segment. If segment 0 is silent but later segments have audio, playback will incorrectly skip audio.
| if audio_segments.is_empty() || audio_segments[0].tracks.is_empty() { | |
| if audio_segments.is_empty() || audio_segments.iter().all(|s| s.tracks.is_empty()) { |
| return { | ||
| code: code.replace(/font-display:\s*swap;/g, "font-display: block;"), | ||
| map: null, | ||
| }; |
There was a problem hiding this comment.
Small thing: this returns a transformed result even when there are no font-display: swap matches, which can cause unnecessary downstream work.
| 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, | |
| }; |
| for segment in &segments { | ||
| segment.audio.get().await.map_err(Error::MediaLoad)?; | ||
| segment.system_audio.get().await.map_err(Error::MediaLoad)?; | ||
| } |
There was a problem hiding this comment.
Since both loaders are awaited just to validate decode success, these can be awaited concurrently per segment to shave a bit off export start on long recordings.
| for segment in &segments { | |
| segment.audio.get().await.map_err(Error::MediaLoad)?; | |
| segment.system_audio.get().await.map_err(Error::MediaLoad)?; | |
| } | |
| for segment in &segments { | |
| tokio::try_join!(segment.audio.get(), segment.system_audio.get()) | |
| .map_err(Error::MediaLoad)?; | |
| } |
|
hey @greptileai, please re-review the PR |
| import { resolveServerRequestPath } from "./server-url-routing"; | ||
|
|
||
| const isJsonContentType = (contentType: string | null) => | ||
| contentType?.toLowerCase().split(";")[0]?.trim() === "application/json"; |
There was a problem hiding this comment.
This only handles exactly application/json. If the server ever returns a structured JSON type (e.g. application/problem+json), we’ll currently parse it as text.
| contentType?.toLowerCase().split(";")[0]?.trim() === "application/json"; | |
| const isJsonContentType = (contentType: string | null) => { | |
| const normalized = contentType?.toLowerCase().split(";")[0]?.trim(); | |
| return normalized === "application/json" || normalized?.endsWith("+json"); | |
| }; |
| </div> | ||
| <SolidMarkdown | ||
| components={{ | ||
| a: (props) => <a {...props} target="_blank" />, |
There was a problem hiding this comment.
Minor security hardening: target="_blank" should generally include rel="noopener noreferrer" to prevent tabnabbing.
| a: (props) => <a {...props} target="_blank" />, | |
| a: (props) => ( | |
| <a {...props} target="_blank" rel="noreferrer noopener" /> | |
| ), |
|
hey @greptileai, please re-review the PR |
| self.resampled_buffer.vacant_len() <= 2 * Self::PROCESSING_SAMPLES_COUNT * self.channels | ||
| } | ||
|
|
||
| fn render_chunk(&mut self) -> bool { |
There was a problem hiding this comment.
Streaming fallback currently renders + resamples (and allocates typed_data) from inside the cpal callback path via fill(). On very long recordings this seems like the exact scenario where we want callback work to be minimal to avoid xruns (Bluetooth / small buffers especially).
Might be worth moving the streaming producer onto a background thread (producer fills the ringbuf; callback only pops), or at least reusing a scratch buffer to avoid per-chunk heap allocs.
Desktop editor open faster, start playback sooner, and feel more responsive — especially on long recordings and Bluetooth audio.
Editor playback & audio
AudioLoaderinstead of blocking editor open. Export still waits for decodes and fails loudly if a track is missing.AudioSegmentsResolved,AudioPipelineReady,ClockStarted) and--press-startsbenchmark mode for measuring press-to-clock latency.Desktop UX & window behavior
requestAnimationFrame.font-display: blockfor bundled fonts; idle font/emoji cache prewarm to avoid first-render jank.cursor-pointerclasses across editor, settings, and screenshot editor.Fixes
licenseQueryinstead ofbruh).charsetsuffix inContent-Typenow parse correctly.Dependencies
tauri-plugin-httpto2.5.2(Rust + npm + lockfile).Greptile Summary
This PR delivers a broad set of startup and playback latency improvements to the desktop editor, replacing per-press stream creation with a persistent
AudioOutputsession, background audio decoding viaAudioLoader, and progressive pre-rendering so the first play press no longer blocks on the full timeline mix. Alongside Rust changes, several long-standing frontend bugs are fixed (wronglicenseQuerykey,Content-Typecharset parsing, editor skeleton hang) and the macOS occlusion-suppression SPI that was wedging WindowServer is removed.AudioLoaderdecodes tracks in the background at editor open;AudioOutputkeeps a prewarmed cpal stream alive for the session;PrerenderedAudioBufferfills a progressive ring buffer on a background thread so audio starts within one callback period of clock start.createResourceon waveform fetches and a slowcustomDomainQueryis resolved by switching to signals withplaceholderData.queryKey: ["bruh"]→["licenseQuery"]restores license refetch;isJsonContentTypehandlescharset-suffixed content types; occlusion-detection SPI removed to prevent login-session soft-restarts on macOS 26.Confidence Score: 5/5
Safe to merge; the audio pipeline refactor, progressive pre-render, and macOS occlusion fix are all structurally sound and well-tested.
The new AudioLoader, AudioOutput, and PrerenderedAudioBuffer implementations are correctly designed — the watch-channel-backed decode cache, lock-free watermark scheme, and generation-token stop mechanism all hold up under concurrent access. Export validation, the macOS SPI removal, and the frontend bug fixes (license key, content-type, skeleton hang) are clean and correct. The only finding is a pre-existing first-segment-only audio check that was preserved unchanged from the old code.
No files require special attention; the Rust audio pipeline changes in audio_output.rs and audio.rs are the most complex but are well-structured.
Important Files Changed
Comments Outside Diff (1)
apps/desktop/src/routes/(window-chrome)/settings/changelog.tsx, line 28-56 (link)ErrorBoundaryreplaced byShow— render errors no longer containedThe old code wrapped the entry list in an
ErrorBoundary, which catches both async query errors and synchronous JavaScript exceptions thrown during SolidJS rendering (e.g., anulldereference on an unexpected entry shape). The newShow when={!changelog.isError}only reflects the TanStack Query error state; a runtime render exception inside theForloop would propagate up and crash the entire Settings panel instead of being contained to the changelog section. Adding a lightweightErrorBoundaryaround theForwould restore the isolation without undoing theSuspense → Showsimplification.Prompt To Fix With AI
Reviews (3): Last reviewed commit: "fix: bound long audio playback buffering" | Re-trigger Greptile