Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
11 changes: 9 additions & 2 deletions src/core/backgroundSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> | null = null;
let imageError: Error | null = null;
let isPrepared = false;
const ensureImage = (
source: BackgroundImageOptions["image"],
): Promise<void> => {
Expand All @@ -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;
Expand All @@ -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<BackgroundImageFrameOptions> = {
state,
offer: offerToSegmentation,
Expand Down
17 changes: 12 additions & 5 deletions src/core/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,19 @@ async function decodeImage(
const createBitmap = globalThis.createImageBitmap as unknown as (
value: Blob | ArrayBuffer,
) => Promise<ImageBitmap>;
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<Response> {
Expand Down
Loading