Skip to content

Commit ac57488

Browse files
committed
refactor(web): 明确错误处理逻辑
1 parent 643809a commit ac57488

8 files changed

Lines changed: 226 additions & 69 deletions

File tree

crates/ffmpeg_wasm/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ fn main() {
2727
"_wasm_decoder_set_compute_peaks",
2828
"_wasm_decoder_get_frame_min",
2929
"_wasm_decoder_get_frame_max",
30+
"_wasm_get_last_error",
3031
];
3132
let exports_json = serde_json::to_string(&exports).unwrap();
3233

web/src/audio-core/core/audio-renderer.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ export class AudioRenderer {
77
private gainNode: GainNode | null;
88
private _isWorkletLoaded = false;
99
private initPromise: Promise<void> | null = null;
10-
private stWasmBytes: ArrayBuffer | null = null;
1110

1211
constructor(
1312
private audioCtx: AudioContext,
1413
private workletUrl: string,
15-
private stWasmUrl: string,
1614
gainNode?: GainNode,
1715
) {
1816
this.gainNode = gainNode || null;
@@ -35,11 +33,6 @@ export class AudioRenderer {
3533
return;
3634
}
3735

38-
if (!this.stWasmBytes) {
39-
const resp = await fetch(this.stWasmUrl);
40-
this.stWasmBytes = await resp.arrayBuffer();
41-
}
42-
4336
if (!this.initPromise) {
4437
this.initPromise = this.audioCtx.audioWorklet.addModule(this.workletUrl);
4538
}
@@ -68,16 +61,13 @@ export class AudioRenderer {
6861
tempo: number,
6962
pitch: number,
7063
rate: number,
64+
stWasmBytes: ArrayBuffer,
7165
): Promise<void> {
7266
return new Promise((resolve, reject) => {
7367
if (!this._isWorkletLoaded || !this.workletNode) {
7468
return reject(new Error("Worklet not loaded"));
7569
}
7670

77-
if (!this.stWasmBytes) {
78-
return reject(new Error("SoundTouch Wasm binary not loaded"));
79-
}
80-
8171
const currentInitId = ++this.initCounter;
8272
this.workletNode.port.onmessage = (event: MessageEvent<WorkletEvent>) => {
8373
const data = event.data;
@@ -87,6 +77,11 @@ export class AudioRenderer {
8777
data.payload.initId === currentInitId
8878
) {
8979
resolve();
80+
} else if (
81+
data.type === "INIT_ERROR" &&
82+
data.payload.initId === currentInitId
83+
) {
84+
reject(new Error(`Worklet INIT failed: ${data.payload.message}`));
9085
} else {
9186
this.onMessage?.(data);
9287
}
@@ -99,7 +94,7 @@ export class AudioRenderer {
9994
payload: {
10095
sharedBuffer,
10196
channels,
102-
wasmBytes: this.stWasmBytes,
97+
wasmBytes: stWasmBytes,
10398
initId: currentInitId,
10499
tempo,
105100
pitch,

web/src/audio-core/engine.ts

Lines changed: 150 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
type PlayerCover,
1515
type QueueConfig,
1616
} from "./types";
17-
import { TypedEventTarget } from "./utils";
17+
import { getErrorMessage, TypedEventTarget } from "./utils";
1818

1919
const TIMEUPDATE_INTERVAL_MS = 250;
2020

@@ -33,6 +33,12 @@ export class FFmpegAudioEngine extends TypedEventTarget<EngineEventMap> {
3333
private audioController: MainAudioController | null = null;
3434
private sharedBuffer: SharedArrayBuffer | null = null;
3535

36+
private _assetsPreloaded = false;
37+
private ffmpegWasmBlobUrl: string | null = null;
38+
private soundtouchWasmBuffer: ArrayBuffer | null = null;
39+
private preloadPromise: Promise<void> | null = null;
40+
private abortController: AbortController | null = null;
41+
3642
private _state: EngineState = "idle";
3743
private _duration = 0;
3844
private _metadata: Record<string, string> = {};
@@ -64,7 +70,6 @@ export class FFmpegAudioEngine extends TypedEventTarget<EngineEventMap> {
6470
this.renderer = new AudioRenderer(
6571
config.audioContext,
6672
config.assets.workletUrl,
67-
config.assets.soundtouchWasmUrl,
6873
config.gainNode,
6974
);
7075

@@ -211,10 +216,68 @@ export class FFmpegAudioEngine extends TypedEventTarget<EngineEventMap> {
211216
this.renderer.setRate(this._rate);
212217
}
213218

219+
public async preloadAssets(): Promise<void> {
220+
if (this._assetsPreloaded) {
221+
return;
222+
}
223+
224+
if (this.preloadPromise) {
225+
return this.preloadPromise;
226+
}
227+
228+
this.abortController = new AbortController();
229+
const signal = this.abortController.signal;
230+
231+
this.preloadPromise = (async () => {
232+
try {
233+
const [ffmpegWasmBuffer, stWasmBuffer] = await Promise.all([
234+
this.fetchAndValidateWasm(this.config.assets.ffmpegWasmUrl, signal),
235+
this.fetchAndValidateWasm(
236+
this.config.assets.soundtouchWasmUrl,
237+
signal,
238+
),
239+
this.pingResource(this.config.assets.workerUrl, signal),
240+
this.pingResource(this.config.assets.workletUrl, signal),
241+
]);
242+
243+
const blob = new Blob([ffmpegWasmBuffer], { type: "application/wasm" });
244+
this.ffmpegWasmBlobUrl = URL.createObjectURL(blob);
245+
246+
this.soundtouchWasmBuffer = stWasmBuffer;
247+
this._assetsPreloaded = true;
248+
} catch (e) {
249+
if (e instanceof DOMException && e.name === "AbortError") {
250+
return;
251+
}
252+
const msg = getErrorMessage(e);
253+
this.handleError(
254+
EngineErrorCode.Network,
255+
`Asset preload failed: ${msg}`,
256+
);
257+
throw e;
258+
} finally {
259+
this.preloadPromise = null;
260+
}
261+
})();
262+
263+
return this.preloadPromise;
264+
}
265+
214266
/**
215267
* Loads a file, prepares the multithreading environment, and extracts metadata.
216268
*/
217269
public async loadFile(file: File): Promise<void> {
270+
if (!this._assetsPreloaded) {
271+
await this.preloadAssets();
272+
}
273+
274+
if (!this.ffmpegWasmBlobUrl || !this.soundtouchWasmBuffer) {
275+
const msg =
276+
"Assets are missing even after preload was called. Engine cannot proceed.";
277+
this.handleError(EngineErrorCode.Network, msg);
278+
throw new Error(msg);
279+
}
280+
218281
const currentSessionId = ++this.loadSessionId;
219282
this.reset();
220283

@@ -223,46 +286,56 @@ export class FFmpegAudioEngine extends TypedEventTarget<EngineEventMap> {
223286
const channels = this.renderer.maxChannels;
224287
const sampleRate = this.renderer.sampleRate;
225288

226-
await this.renderer.initialize(channels);
289+
try {
290+
await this.renderer.initialize(channels);
227291

228-
if (this.loadSessionId !== currentSessionId) {
229-
return;
230-
}
292+
if (this.loadSessionId !== currentSessionId) {
293+
return;
294+
}
231295

232-
this.sharedBuffer = allocateAudioQueueMemory(
233-
sampleRate,
234-
channels,
235-
this.queueConfig,
236-
);
296+
this.sharedBuffer = allocateAudioQueueMemory(
297+
sampleRate,
298+
channels,
299+
this.queueConfig,
300+
);
237301

238-
this.audioController = createMainController(this.sharedBuffer);
302+
this.audioController = createMainController(this.sharedBuffer);
239303

240-
await this.renderer.bindQueue(
241-
this.sharedBuffer,
242-
channels,
243-
this._tempo,
244-
this._pitch,
245-
this._rate,
246-
);
304+
await this.renderer.bindQueue(
305+
this.sharedBuffer,
306+
channels,
307+
this._tempo,
308+
this._pitch,
309+
this._rate,
310+
this.soundtouchWasmBuffer,
311+
);
247312

248-
if (this.loadSessionId !== currentSessionId) {
249-
return;
250-
}
313+
if (this.loadSessionId !== currentSessionId) {
314+
return;
315+
}
251316

252-
const loadPromise = new Promise<void>((resolve, reject) => {
253-
this.loadResolve = resolve;
254-
this.loadReject = reject;
255-
});
317+
const loadPromise = new Promise<void>((resolve, reject) => {
318+
this.loadResolve = resolve;
319+
this.loadReject = reject;
320+
});
256321

257-
this.workerClient.init(
258-
file,
259-
sampleRate,
260-
channels,
261-
this.sharedBuffer,
262-
this.config.assets.ffmpegWasmUrl,
263-
);
322+
this.workerClient.init(
323+
file,
324+
sampleRate,
325+
channels,
326+
this.sharedBuffer,
327+
this.ffmpegWasmBlobUrl,
328+
);
264329

265-
await loadPromise;
330+
await loadPromise;
331+
} catch (e) {
332+
const msg = getErrorMessage(e);
333+
this.handleError(
334+
EngineErrorCode.Decode,
335+
`Engine initialization failed: ${msg}`,
336+
);
337+
throw e;
338+
}
266339
}
267340

268341
public async play(): Promise<void> {
@@ -301,13 +374,23 @@ export class FFmpegAudioEngine extends TypedEventTarget<EngineEventMap> {
301374
}
302375

303376
public destroy(): void {
377+
if (this.abortController) {
378+
this.abortController.abort();
379+
this.abortController = null;
380+
}
304381
this.stopTimeupdate();
305382
this.workerClient.destroy();
306383
this.renderer.destroyNode();
307384
this.sharedBuffer = null;
308385
this.audioController = null;
309386
this.resetState();
310387
this._state = "idle";
388+
if (this.ffmpegWasmBlobUrl) {
389+
URL.revokeObjectURL(this.ffmpegWasmBlobUrl);
390+
this.ffmpegWasmBlobUrl = null;
391+
}
392+
this.soundtouchWasmBuffer = null;
393+
this._assetsPreloaded = false;
311394
}
312395

313396
private reset(): void {
@@ -322,6 +405,39 @@ export class FFmpegAudioEngine extends TypedEventTarget<EngineEventMap> {
322405
//#endregion
323406

324407
//#region Internal Callbacks & Utils
408+
private async fetchAndValidateWasm(
409+
url: string,
410+
signal: AbortSignal,
411+
): Promise<ArrayBuffer> {
412+
const resp = await fetch(url, { signal });
413+
if (!resp.ok) {
414+
throw new Error(`HTTP ${resp.status} - ${resp.statusText}`);
415+
}
416+
417+
const buffer = await resp.arrayBuffer();
418+
419+
if (buffer.byteLength < 4) {
420+
throw new Error(`File too small to be a valid WASM: ${url}`);
421+
}
422+
const view = new DataView(buffer);
423+
const magic = view.getUint32(0, false);
424+
if (magic !== 0x0061736d /* \0asm */) {
425+
throw new Error(
426+
`Invalid WASM magic number detected. Server might have returned a 404 HTML page for: ${url}`,
427+
);
428+
}
429+
430+
return buffer;
431+
}
432+
433+
private async pingResource(url: string, signal: AbortSignal): Promise<void> {
434+
const resp = await fetch(url, { signal });
435+
if (!resp.ok) {
436+
throw new Error(`Failed to load resource (HTTP ${resp.status}): ${url}`);
437+
}
438+
await resp.arrayBuffer();
439+
}
440+
325441
private handleWorkerInitDone(payload: {
326442
duration: number;
327443
metadata: Record<string, string>;

web/src/audio-core/utils/error.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export function getErrorMessage(e: unknown): string {
2+
if (e instanceof Error) {
3+
return e.message;
4+
}
5+
6+
if (typeof e === "string") {
7+
return e;
8+
}
9+
10+
if (e != null && typeof e === "object") {
11+
if ("message" in e && typeof e.message === "string") {
12+
return e.message;
13+
}
14+
15+
try {
16+
return JSON.stringify(e);
17+
} catch {
18+
return String(e);
19+
}
20+
}
21+
22+
return String(e);
23+
}

web/src/audio-core/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from "./error";
12
export * from "./TypedEventTarget";

0 commit comments

Comments
 (0)