12e91c0bbe
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>
104 lines
4.1 KiB
TypeScript
104 lines
4.1 KiB
TypeScript
// Frontend wrapper for the Rust `enumerate_screen_sources` command. Falls
|
||
// back to an empty list outside the Tauri runtime so a browser-only dev
|
||
// build (pnpm vite:dev in Chrome without Tauri) degrades gracefully to
|
||
// "nothing to show" rather than throwing.
|
||
|
||
import { isTauriRuntime } from './globalShortcut';
|
||
|
||
export type ScreenSourceKind = 'screen' | 'window';
|
||
|
||
export interface ScreenSource {
|
||
/** Chromium-format source id ("screen:<id>:0" / "window:<hwnd>:0"). */
|
||
id: string;
|
||
name: string;
|
||
kind: ScreenSourceKind;
|
||
/** Base64-encoded JPEG without a data-URL prefix. Null when capture failed.
|
||
* Kept under `thumbnailPng` key for rollout stability — the server-side
|
||
* format switched from PNG to JPEG for payload size, but the field name
|
||
* preserves the wire contract during the transition. */
|
||
thumbnailPng: string | null;
|
||
width: number;
|
||
height: number;
|
||
}
|
||
|
||
// Fast, metadata-only list. The picker uses this first so names show up
|
||
// immediately; thumbnails stream in via captureScreenSourceThumbnail below.
|
||
export async function listScreenSources(): Promise<ScreenSource[]> {
|
||
if (!isTauriRuntime()) return [];
|
||
try {
|
||
const { invoke } = await import('@tauri-apps/api/core');
|
||
const raw = await invoke<ScreenSource[]>('list_screen_sources');
|
||
return raw ?? [];
|
||
} catch (err: unknown) {
|
||
console.warn('list_screen_sources failed', err);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// 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> {
|
||
if (!isTauriRuntime()) return null;
|
||
try {
|
||
const { invoke } = await import('@tauri-apps/api/core');
|
||
const result = await invoke<string | null>('capture_screen_source_thumbnail', {
|
||
sourceId,
|
||
});
|
||
return result ?? null;
|
||
} catch (err: unknown) {
|
||
console.warn('capture_screen_source_thumbnail failed', { sourceId, err });
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 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 5–10× more responsive in practice.
|
||
export async function enumerateScreenSources(): Promise<ScreenSource[]> {
|
||
if (!isTauriRuntime()) return [];
|
||
try {
|
||
const { invoke } = await import('@tauri-apps/api/core');
|
||
const raw = await invoke<ScreenSource[]>('enumerate_screen_sources');
|
||
return raw ?? [];
|
||
} catch (err: unknown) {
|
||
console.warn('enumerate_screen_sources failed', err);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// Data-URL helper — picker tiles bind `src={thumbnailDataUrl(src)}` so the
|
||
// thumbnail bytes never leave the component's render pass. Rust encodes
|
||
// JPEG now (smaller payload, faster decode); the mime type here must
|
||
// match or the <img> element silently fails to paint.
|
||
export function thumbnailDataUrl(src: ScreenSource): string | null {
|
||
if (!src.thumbnailPng) return null;
|
||
return 'data:image/jpeg;base64,' + src.thumbnailPng;
|
||
}
|