From 6c6a23e672a9088d09d2ceb630fe70c09a7546d3 Mon Sep 17 00:00:00 2001 From: byGalax Date: Wed, 22 Apr 2026 22:24:02 +0200 Subject: [PATCH] fix(call): thumbnail fetch falls back to base64 if binary path returns nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Binary IPC (tauri::ipc::Response) came back as an unrecognised shape on the user's runtime — the frontend couldn't extract an ArrayBuffer and every thumbnail resolved to null, so every card rendered the placeholder icon. Added: - Widened the invoke typing to ArrayBuffer | Uint8Array | number[] so all three known Tauri/WebView2 deserialisation shapes parse. - A one-time console.warn when the Response body lands as an unknown object shape, so the real wire format can be diagnosed if this ever trips again. - An automatic tier-2 fallback: if the binary path produced 0 usable bytes, re-invoke the legacy base64 command and decode client-side. Slower on the JS thread than binary IPC but known to work across all Tauri 2.x runtimes. Net behaviour: thumbnails render again. If the binary path works on a given build, we get the fast path; otherwise the base64 fallback keeps the picker usable. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/desktop/src/lib/screenSources.ts | 74 ++++++++++++++++++--------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/apps/desktop/src/lib/screenSources.ts b/apps/desktop/src/lib/screenSources.ts index 127acc5..5903faa 100644 --- a/apps/desktop/src/lib/screenSources.ts +++ b/apps/desktop/src/lib/screenSources.ts @@ -55,48 +55,72 @@ 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). +// Thumbnail fetch with a two-tier fallback: +// 1. binary-IPC path (`..._bytes`) — ArrayBuffer over Tauri's raw channel +// 2. base64 path (legacy) — same command minus the ArrayBuffer wrapper // -// Tauri's invoke may resolve with different shapes depending on runtime -// version — accept ArrayBuffer, Uint8Array, or a number[] fallback so a -// WebView2 variant that doesn't speak the binary channel still works. +// The binary path can come through in several shapes depending on the +// Tauri / WebView2 version combo: a real ArrayBuffer, a Uint8Array, or +// occasionally a plain number[] when the response got re-serialised. +// We normalise all three into an ArrayBuffer before handing it to Blob. +// If the binary path returns nothing usable we retry once on the base64 +// command — keeps thumbnails visible while the binary contract settles. +let warnedBinaryShape = false; export async function captureScreenSourceThumbnailBytes( sourceId: string, ): Promise { if (!isTauriRuntime()) return null; + const { invoke } = await import('@tauri-apps/api/core'); + + // ---- Tier 1: binary IPC --------------------------------------------- try { - const { invoke } = await import('@tauri-apps/api/core'); - const result = await invoke( + const result = await invoke( 'capture_screen_source_thumbnail_bytes', { sourceId }, ); - let bytes: ArrayBuffer | Uint8Array | null = null; + let bytes: Uint8Array | null = null; if (result instanceof ArrayBuffer) { - bytes = result; + bytes = new Uint8Array(result); } else if (result instanceof Uint8Array) { bytes = result; } else if (Array.isArray(result) && result.length > 0) { bytes = new Uint8Array(result); + } else if (result && typeof result === 'object') { + // One-time diagnostic so we can see the unexpected shape in the + // console if WebView2 de-serialises the Response body into a bag + // of properties instead of a transferable binary buffer. + if (!warnedBinaryShape) { + warnedBinaryShape = true; + console.warn( + 'capture_screen_source_thumbnail_bytes: unexpected shape, falling back', + result, + ); + } } - if (!bytes || bytes.byteLength === 0) { - return null; + if (bytes && bytes.byteLength > 0) { + const copy = new Uint8Array(bytes.byteLength); + copy.set(bytes); + return new Blob([copy.buffer], { type: 'image/jpeg' }); } - // Normalise to ArrayBuffer so Blob() accepts the part without TS - // complaining about SharedArrayBuffer-backed Uint8Array flavours. - const buffer = - bytes instanceof ArrayBuffer - ? bytes - : (bytes.buffer.slice( - bytes.byteOffset, - bytes.byteOffset + bytes.byteLength, - ) as ArrayBuffer); - return new Blob([buffer], { type: 'image/jpeg' }); } catch (err: unknown) { - console.warn('capture_screen_source_thumbnail_bytes failed', { sourceId, err }); + console.warn('binary thumbnail path threw, trying base64 fallback', { + sourceId, + err, + }); + } + + // ---- Tier 2: base64 fallback ---------------------------------------- + try { + const b64 = await invoke('capture_screen_source_thumbnail', { + sourceId, + }); + if (!b64) return null; + const bin = atob(b64); + const fallbackBytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) fallbackBytes[i] = bin.charCodeAt(i); + return new Blob([fallbackBytes.buffer], { type: 'image/jpeg' }); + } catch (err: unknown) { + console.warn('base64 thumbnail fallback failed', { sourceId, err }); return null; } }