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 {