mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-03 00:27:30 -05:00
Tolerate frameError
This commit is contained in:
@@ -175,6 +175,9 @@ export function LiveView() {
|
||||
</div>
|
||||
<div className={styles.notes}>
|
||||
{uploadNote} · {clipsNote}
|
||||
{live.failedFrames > 0
|
||||
? ` · ${live.failedFrames} frame${live.failedFrames === 1 ? "" : "s"} skipped (analysis failed)`
|
||||
: null}
|
||||
</div>
|
||||
{live.error ? (
|
||||
<div className={styles.error}>{live.error}</div>
|
||||
|
||||
@@ -108,6 +108,8 @@ export interface LiveSnapshot {
|
||||
/** what the clip encoder is getting from that track; null while clips are off */
|
||||
audioSignal: AudioSignal | null;
|
||||
clips: ClipsState;
|
||||
/** frames whose analysis threw this capture; each is skipped and the capture carries on */
|
||||
failedFrames: number;
|
||||
}
|
||||
|
||||
const IDLE: LiveSnapshot = {
|
||||
@@ -119,6 +121,7 @@ const IDLE: LiveSnapshot = {
|
||||
audioError: null,
|
||||
audioSignal: null,
|
||||
clips: "off",
|
||||
failedFrames: 0,
|
||||
};
|
||||
|
||||
let snapshot = IDLE;
|
||||
@@ -239,16 +242,18 @@ export async function startCapture({
|
||||
await video.play();
|
||||
|
||||
// the worker first: a failed init must not leave the camera on
|
||||
client = new AnalyzerClient(onResult, onWorkerError, undefined, {
|
||||
const starting = new AnalyzerClient(onResult, onWorkerError, undefined, {
|
||||
frameQueueLimit: FRAME_QUEUE_LIMIT,
|
||||
webgpu: settings.webgpu,
|
||||
attachFrames: saveFrames,
|
||||
onFrameError,
|
||||
});
|
||||
client = starting;
|
||||
try {
|
||||
await client.whenReady();
|
||||
await starting.whenReady();
|
||||
} catch (error) {
|
||||
client.dispose();
|
||||
client = null;
|
||||
starting.dispose();
|
||||
if (client === starting) client = null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -310,10 +315,15 @@ export async function startCapture({
|
||||
/** Ends the capture: the session's clips roll into history and unsent matches get one last send. */
|
||||
export function stopCapture(): void {
|
||||
if (snapshot.status === "idle") return;
|
||||
endCapture({ ...IDLE });
|
||||
}
|
||||
|
||||
/** Tears the capture down into `next`, rolling the session's clips and sending unsent matches as Stop does. */
|
||||
function endCapture(next: LiveSnapshot): void {
|
||||
const stream = snapshot.stream;
|
||||
release();
|
||||
if (stream) stopTracks(stream);
|
||||
set({ ...IDLE });
|
||||
set(next);
|
||||
// the scan ending is the last match boundary — flush what's unsent
|
||||
// (partials are safe: the server merges them into fuller resends)
|
||||
if (uploadEnabled()) void sendLive(unsentMatches, newestSessionKey());
|
||||
@@ -379,11 +389,29 @@ function stopTracks(stream: MediaStream): void {
|
||||
for (const track of stream.getTracks()) track.stop();
|
||||
}
|
||||
|
||||
/** The worker itself is gone (init failed or it crashed), so the capture cannot go on. */
|
||||
function onWorkerError(message: string): void {
|
||||
const failed: LiveSnapshot = {
|
||||
...IDLE,
|
||||
status: "error",
|
||||
error: describeError(new Error(message)),
|
||||
};
|
||||
if (snapshot.status === "running") {
|
||||
endCapture(failed);
|
||||
return;
|
||||
}
|
||||
const stream = snapshot.stream;
|
||||
release();
|
||||
if (stream) stopTracks(stream);
|
||||
set({ ...IDLE, status: "error", error: describeError(new Error(message)) });
|
||||
set(failed);
|
||||
}
|
||||
|
||||
function onFrameError(message: string): void {
|
||||
if (snapshot.failedFrames === 0) {
|
||||
// biome-ignore lint/suspicious/noConsole: the capture carries on, so the console is where a failing frame shows why
|
||||
console.warn("scanner: frame analysis failed", message);
|
||||
}
|
||||
set({ failedFrames: snapshot.failedFrames + 1 });
|
||||
}
|
||||
|
||||
function onResult(
|
||||
|
||||
@@ -245,7 +245,11 @@ async function analyze({ bitmap, t }: AnalyzeRequest): Promise<void> {
|
||||
try {
|
||||
await analyzeFrame(bitmap, t);
|
||||
} catch (error) {
|
||||
post({ kind: "error", message: `analyze failed: ${String(error)}` });
|
||||
post({
|
||||
kind: "frameError",
|
||||
t,
|
||||
message: `analyze failed: ${String(error)}`,
|
||||
});
|
||||
}
|
||||
post({ kind: "done", t, calm: scheduler!.calm(t), telemetry });
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export class AnalyzerClient {
|
||||
#busy = false;
|
||||
readonly #onResult: ResultHandler;
|
||||
readonly #onError: ErrorHandler;
|
||||
readonly #onFrameError: ErrorHandler;
|
||||
readonly #onDone: DoneHandler | undefined;
|
||||
readonly #readyPromise: Promise<void>;
|
||||
#rejectReady: ((error: Error) => void) | undefined;
|
||||
@@ -70,11 +71,14 @@ export class AnalyzerClient {
|
||||
webgpu?: boolean;
|
||||
/** ship the analyzed frame's PNG on results (default true) */
|
||||
attachFrames?: boolean;
|
||||
/** one frame's analysis threw; the worker carries on (default onError) */
|
||||
onFrameError?: ErrorHandler;
|
||||
} = {},
|
||||
) {
|
||||
this.#frameQueueLimit = options.frameQueueLimit ?? 0;
|
||||
this.#onResult = onResult;
|
||||
this.#onError = onError;
|
||||
this.#onFrameError = options.onFrameError ?? onError;
|
||||
this.#onDone = onDone;
|
||||
this.#worker = new Worker(
|
||||
new URL("./analyzer.worker.ts", import.meta.url),
|
||||
@@ -97,6 +101,8 @@ export class AnalyzerClient {
|
||||
resolveReady();
|
||||
} else if (msg.kind === "result") {
|
||||
this.#onResult(msg);
|
||||
} else if (msg.kind === "frameError") {
|
||||
this.#onFrameError(msg.message);
|
||||
} else if (msg.kind === "done") {
|
||||
this.#settle();
|
||||
this.#onDone?.(msg.t, { calm: msg.calm, telemetry: msg.telemetry });
|
||||
|
||||
@@ -62,6 +62,8 @@ export type WorkerResponse =
|
||||
/** lossless PNG of the exact frame that was analyzed; present when events fired */
|
||||
frame?: Blob;
|
||||
}
|
||||
/** frame t threw mid-analysis; the worker is still usable and a "done" follows */
|
||||
| { kind: "frameError"; t: number; message: string }
|
||||
/** all due detectors have reported for frame t (per-frame path only) */
|
||||
| {
|
||||
kind: "done";
|
||||
|
||||
Reference in New Issue
Block a user