From 57f8f417954506a88e3e43034a1700df4d6cfa7c Mon Sep 17 00:00:00 2001 From: Milosz Filimowski Date: Tue, 15 Sep 2026 13:31:22 +0200 Subject: [PATCH] fix(background-image): load the image on React Native and report load failures React Native's Blob cannot be created from an ArrayBuffer, so decodeImage threw on every image and the effect silently published the camera without a background. Decode from the bytes when a Blob cannot be made, and report image load errors through onStatus. --- CHANGELOG.md | 5 +++++ src/core/backgroundSessions.ts | 11 +++++++++-- src/core/image.ts | 17 ++++++++++++----- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1fdfe8..c9ada3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +- Background image: the image loads on React Native. React Native's `Blob` cannot wrap an `ArrayBuffer`, so decoding threw and the effect silently stayed a passthrough; the bytes now go to `createImageBitmap` directly there. +- Background image: a failure to load the image is reported through `onStatus` as `"error"` instead of being swallowed. + ## 0.1.3 - `createCameraEffectMiddleware(effect)` for React Native: a camera-track middleware for `useCamera().setCameraTrackMiddleware`, so an effect can be switched on from any screen without a provider or a hook. diff --git a/src/core/backgroundSessions.ts b/src/core/backgroundSessions.ts index 9bc371a..9a73979 100644 --- a/src/core/backgroundSessions.ts +++ b/src/core/backgroundSessions.ts @@ -572,6 +572,8 @@ export async function createBackgroundImageSession( // which a worklet runtime holding its own copy will not observe. let imageSource: BackgroundImageOptions["image"] | null = null; let loadingImage: Promise | null = null; + let imageError: Error | null = null; + let isPrepared = false; const ensureImage = ( source: BackgroundImageOptions["image"], ): Promise => { @@ -591,8 +593,11 @@ export async function createBackgroundImageSession( ); previous?.texture.destroy(); }) - .catch(() => { - // The effect remains a passthrough until the caller supplies a valid image. + .catch((cause: unknown) => { + // The effect remains a passthrough until the caller supplies a valid image. Report the + // failure, but not before the segmentation status settles, or its "ready" would hide it. + imageError = asError(cause); + if (isPrepared) context.onStatus?.("error", imageError); }) .finally(() => { loadingImage = null; @@ -604,6 +609,8 @@ export async function createBackgroundImageSession( prepareSegmentation(state, context, getOptions().segmentation), ensureImage(getOptions().image), ]); + isPrepared = true; + if (imageError != null) context.onStatus?.("error", imageError); const frameKernel: VideoEffectFrameKernel = { state, offer: offerToSegmentation, diff --git a/src/core/image.ts b/src/core/image.ts index f42f223..ea69fa2 100644 --- a/src/core/image.ts +++ b/src/core/image.ts @@ -28,12 +28,19 @@ async function decodeImage( const createBitmap = globalThis.createImageBitmap as unknown as ( value: Blob | ArrayBuffer, ) => Promise; - if (typeof Blob !== "undefined") { - return createBitmap( - new Blob([bytes], { type: source.mimeType ?? "image/*" }), - ); + const blob = toBlob(bytes, source.mimeType); + return createBitmap(blob ?? bytes); +} + +// Browsers decode a Blob. React Native has a Blob global too, but it cannot wrap an +// ArrayBuffer (it throws), and react-native-webgpu's createImageBitmap takes the bytes directly. +function toBlob(bytes: ArrayBuffer, mimeType: string | undefined): Blob | null { + if (typeof Blob === "undefined") return null; + try { + return new Blob([bytes], { type: mimeType ?? "image/*" }); + } catch { + return null; } - return createBitmap(bytes); } function fetchRequired(uri: string | undefined): Promise {