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 .changeset/name-failed-assets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/track-processors': patch
---

Reject with an `Error` naming the URL when the MediaPipe fileset or a background image fails to load, instead of a bare DOM `Event`
6 changes: 3 additions & 3 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ Chrome only for now. Firefox, WebKit and BrowserStack devices need their own cal

## What's covered

Seventeen cases across eleven concerns.
Twenty cases.

| Spec | IDs | Concern |
| --- | --- | --- |
| `capability.spec.ts` | A1, A1b, A3, A4 | Support predicates; both pipelines selected and working |
| `lifecycle.spec.ts` | B1, B2 | Attach produces blurred frames; detach restores them |
| `lifecycle.spec.ts` | B1, B2, B3, B4 | Attach produces blurred frames; detach restores them and releases the WebGL context; a missing fileset rejects with an `Error` naming it |
| `modes.spec.ts` | C1, C1b, C3 | Switching lands correctly, publishes no intermediate frame, and `disabled` is true passthrough |
| `background.spec.ts` | D1, D5, D6 | Image composited behind the subject; fetched once; a failing image at construction is logged |
| `background.spec.ts` | D1, D5, D6 | Image composited behind the subject; fetched once; a failing image at construction is logged by name |
| `geometry.spec.ts` | E1 | Output dimensions follow the source at three resolutions |
| `performance.spec.ts` | G1 | `onFrameProcessed` timings are coherent at a usable frame rate |

Expand Down
6 changes: 6 additions & 0 deletions e2e/specs/background.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,11 @@ test.describe('virtual background', () => {
// uncaught error with no message.
expect(app.pageErrors, 'uncaught page errors').toEqual([]);
expect(await page.evaluate(() => window.harness.lastError()), 'harness logged an error').toBeNull();

// #142: the logged failure names the image rather than printing a bare DOM Event.
expect(
app.consoleErrors.some((line) => line.includes('missing-bg.png')),
`no console error named the image: ${JSON.stringify(app.consoleErrors)}`,
).toBe(true);
});
});
30 changes: 30 additions & 0 deletions e2e/specs/lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,34 @@ test.describe('processor lifecycle', () => {
.poll(() => page.evaluate(() => window.harness.counters().webgl.contextsLost))
.toBeGreaterThanOrEqual(before + cycles);
});

// #142: MediaPipe rejects a failed wasm-loader script with its DOM Event, which has no message.
test('B4: a fileset that fails to load rejects with an Error naming its URL', async ({ app }) => {
const { page } = app;
await page.route('**/missing-wasm/**', (route) => route.fulfill({ status: 404 }));
await app.goto({ backdrop: 'checker', motion: 'static', wasm: '/missing-wasm' });
await startCamera(page);

const err = await page.evaluate(() =>
window.harness.media.setVideoProcessorEnabled(true).then(
() => null,
(e: unknown) => {
const cause = (e as { cause?: unknown }).cause;
return {
isError: e instanceof Error,
message: String((e as Error).message),
causeIsEvent: cause instanceof Event,
causeTarget:
cause instanceof Event ? (cause.target as Element | null)?.tagName : undefined,
};
},
),
);
expect(err, 'attaching with a missing fileset resolved').not.toBeNull();
expect(err?.isError, 'rejection value is an Error').toBe(true);
expect(err?.message).toContain('/missing-wasm/');
// The browser's original error stays reachable for callers that need more than the message.
expect(err?.causeIsEvent, 'cause is the original Event').toBe(true);
expect(err?.causeTarget).toBe('SCRIPT');
});
});
19 changes: 18 additions & 1 deletion src/transformers/BackgroundTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ export type BackgroundOptions = {
onFrameProcessed?: (stats: FrameProcessingStats) => void;
};

/**
* `cause` is assigned rather than passed to the constructor, because browsers without support for
* the `cause` option ignore it.
*/
function errorWithCause(message: string, cause: unknown) {
return Object.assign(new Error(message), { cause });
}

export default class BackgroundProcessor extends VideoTransformer<BackgroundOptions> {
static get isSupported() {
return (
Expand Down Expand Up @@ -78,6 +86,14 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
runningMode: 'VIDEO',
outputCategoryMask: true,
outputConfidenceMasks: false,
}).catch((err) => {
// MediaPipe loads its wasm loader through a <script> tag and rejects with that tag's error
// Event, which has no message. The script's URL is the only thing identifying the failure.
if (err instanceof Event) {
const src = err.target instanceof HTMLScriptElement ? err.target.src : 'unknown URL';
throw errorWithCause(`Failed to load the MediaPipe tasks-vision fileset from ${src}`, err);
}
throw err;
});

// Skip loading the image here if update already loaded the image below
Expand Down Expand Up @@ -112,7 +128,8 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
await new Promise((resolve, reject) => {
img.crossOrigin = 'Anonymous';
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.onerror = (event) =>
reject(errorWithCause(`Failed to load background image from ${path}`, event));
img.src = path;
});
const imageData = await createImageBitmap(img);
Expand Down
Loading