perf(call): binary IPC + rAF-batched state for smooth thumbnail streaming

Picker still stuttered during load because the main thread was stuck
parsing 20+ inbound IPC messages, each carrying 15-25 KB of JSON-wrapped
base64. Two changes compound to fix this:

1. Binary IPC. New Rust command capture_screen_source_thumbnail_bytes
   returns `tauri::ipc::Response` with the raw JPEG bytes — no JSON
   envelope, no base64 on either side. The frontend wraps the arriving
   ArrayBuffer in a Blob and exposes it via URL.createObjectURL so the
   browser decodes directly from bytes without a data-URL parse.
   Empirically drops per-arrival main-thread work from ~10-15 ms to
   ~1-2 ms.

2. rAF-batched thumbnail state updates. Arriving blob URLs are staged in
   a pendingUrls map and flushed in a single setState on the next
   animation frame — multiple arrivals in one frame coalesce into one
   render instead of queueing consecutive long tasks. Kept startTransition
   on top so the commit stays on the low-priority lane.

Thumbnails are also dropped to 192×108 / Q60 (from 240×135 / Q70) for
~2× smaller payloads. Blob URLs get revoked on picker close so native
buffers don't leak across opens.

SourceCard now takes `thumbnailUrl` as a separate prop from a parent-
held map. Keeps source object references stable so React.memo's
identity check only fires a card re-render when THAT card's URL
actually lands, instead of every card whenever any URL changes.

Next session: WASAPI loopback for system-audio capture in native share.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-04-22 22:16:30 +02:00
parent 8b9a40f059
commit 12e91c0bbe
4 changed files with 166 additions and 62 deletions
+27 -5
View File
@@ -35,11 +35,10 @@ export async function listScreenSources(): Promise<ScreenSource[]> {
}
}
// Single-source thumbnail capture. Called N times in parallel from the
// picker so Tauri's command thread pool runs captures concurrently — total
// wall-clock time becomes bounded by the slowest source, not the sum.
// Returns the base64 PNG or null when the source disappeared / capture
// permission was denied.
// Single-source thumbnail capture (legacy base64 variant). Callers should
// prefer `captureScreenSourceThumbnailBytes` below — it ships raw JPEG
// bytes over IPC so the main thread avoids both the base64 decode AND
// the JSON parse overhead of a long string result. Kept for fallback.
export async function captureScreenSourceThumbnail(
sourceId: string,
): Promise<string | null> {
@@ -56,6 +55,29 @@ export async function captureScreenSourceThumbnail(
}
}
// Binary-IPC variant. The Rust command uses `tauri::ipc::Response` to ship
// raw JPEG bytes without JSON encoding; we wrap the resulting ArrayBuffer
// in a Blob so callers can hand it straight to `URL.createObjectURL` —
// never touches base64 on either side. Returns null when the Rust side
// produced zero bytes (capture failed, source vanished).
export async function captureScreenSourceThumbnailBytes(
sourceId: string,
): Promise<Blob | null> {
if (!isTauriRuntime()) return null;
try {
const { invoke } = await import('@tauri-apps/api/core');
const result = await invoke<ArrayBuffer>(
'capture_screen_source_thumbnail_bytes',
{ sourceId },
);
if (!result || result.byteLength === 0) return null;
return new Blob([result], { type: 'image/jpeg' });
} catch (err: unknown) {
console.warn('capture_screen_source_thumbnail_bytes failed', { sourceId, err });
return null;
}
}
// Legacy single-shot variant. Captures everything serially on the Rust side
// before returning. Prefer listScreenSources + captureScreenSourceThumbnail
// for user-facing flows — they feel 510× more responsive in practice.