Files
ChatApp/apps/desktop/src/lib/screenSources.ts
T
byGalax 12e91c0bbe 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>
2026-04-22 22:16:30 +02:00

104 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 510× 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;
}