diff --git a/apps/desktop/src-tauri/src/screen_sources.rs b/apps/desktop/src-tauri/src/screen_sources.rs index 95a0184..55873bb 100644 --- a/apps/desktop/src-tauri/src/screen_sources.rs +++ b/apps/desktop/src-tauri/src/screen_sources.rs @@ -83,22 +83,23 @@ pub fn capture_screen_source_thumbnail(source_id: String) -> Result`): +// a Result wrapper forces Tauri to JSON-serialise the variant so the +// frontend gets a JSON object instead of raw bytes. Failures — bad id +// format, capture errors, source vanished — all funnel into an empty +// byte buffer; the caller treats `byteLength === 0` as the "no thumbnail" +// signal. #[tauri::command] -pub fn capture_screen_source_thumbnail_bytes( - source_id: String, -) -> Result { +pub fn capture_screen_source_thumbnail_bytes(source_id: String) -> tauri::ipc::Response { let bytes = if let Some(raw) = source_id.strip_prefix("screen:").and_then(strip_zero_suffix) { capture_monitor_bytes_by_id(raw).unwrap_or_default() } else if let Some(raw) = source_id.strip_prefix("window:").and_then(strip_zero_suffix) { capture_window_bytes_by_id(raw).unwrap_or_default() } else { - return Err(format!("unknown source id format: {source_id}")); + eprintln!("capture_screen_source_thumbnail_bytes: unknown id format: {source_id}"); + Vec::new() }; - Ok(tauri::ipc::Response::new(bytes)) + tauri::ipc::Response::new(bytes) } fn strip_zero_suffix(s: &str) -> Option<&str> { diff --git a/apps/desktop/src/lib/screenSources.ts b/apps/desktop/src/lib/screenSources.ts index c6828ff..127acc5 100644 --- a/apps/desktop/src/lib/screenSources.ts +++ b/apps/desktop/src/lib/screenSources.ts @@ -60,18 +60,41 @@ export async function captureScreenSourceThumbnail( // 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). +// +// 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. export async function captureScreenSourceThumbnailBytes( sourceId: string, ): Promise { if (!isTauriRuntime()) return null; try { const { invoke } = await import('@tauri-apps/api/core'); - const result = await invoke( + const result = await invoke( 'capture_screen_source_thumbnail_bytes', { sourceId }, ); - if (!result || result.byteLength === 0) return null; - return new Blob([result], { type: 'image/jpeg' }); + let bytes: ArrayBuffer | Uint8Array | null = null; + if (result instanceof ArrayBuffer) { + bytes = result; + } else if (result instanceof Uint8Array) { + bytes = result; + } else if (Array.isArray(result) && result.length > 0) { + bytes = new Uint8Array(result); + } + if (!bytes || bytes.byteLength === 0) { + return null; + } + // 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 }); return null;