siyuan-note/siyuan · error · Error
Recorder has been disposed
Error message
Recorder has been disposed
What it means
startRecording() checks the disposed flag at entry and throws 'Recorder has been disposed' if dispose() was already called. The recorder cannot be reused once disposed — context, recorder, worker, and tracks have all been torn down.
Source
Thrown at app/src/protyle/util/RecordMedia.ts:114
}
const contextOptions = {} as AudioContextOptions & { sinkId?: { type: "none" } };
if ((isInAndroid() || isInHarmony()) && "setSinkId" in AudioContextConstructor.prototype) {
contextOptions.sinkId = {type: "none"};
}
this.context = new AudioContextConstructor(contextOptions);
if (!this.context.audioWorklet || typeof AudioWorkletNode === "undefined") {
throw new Error("AudioWorklet is not supported");
}
this.audioInput = this.context.createMediaStreamSource(mediaStream);
this.mediaStream.getAudioTracks().forEach((track) => {
track.addEventListener("ended", this.handleTrackEnded);
});
}
public async startRecording() {
if (this.disposed) {
throw new Error("Recorder has been disposed");
}
if (this.isRecording) {
return;
}
this.chunks = [];
this.readyPromise = new Promise<void>((resolve, reject) => {
this.resolveReady = resolve;
this.rejectReady = reject;
});
// Webpack 通过 import.meta.url 将录音编码器打包为按需加载的独立 Worker。
// @ts-ignore TypeScript 的 CommonJS 类型检查不识别由 Webpack 转换的 import.meta.url。
this.worker = new Worker(new URL("./RecordMediaWorker.ts", import.meta.url));
this.worker.onmessage = (event: MessageEvent<EncoderMessage>) => {
this.handleWorkerMessage(event.data);
};
this.worker.onerror = (event: ErrorEvent) => {
this.handleWorkerError(new Error(event.message || "MP3 encoder failed"));View on GitHub (pinned to 251596fc0d)
Solutions
- Treat RecordMedia as single-use: build a new instance (new RecordMedia(stream)) for every recording session.
- Track a started flag in the UI and disable the start button after dispose().
- Reset the reference to null after dispose() so accidental reuse fails fast at the call site.
- Audit dialog close/confirm handlers to ensure they do not dispose a recorder that the start handler still references.
Example fix
// before
recordMedia.dispose();
await recordMedia.startRecording(); // throws
// after
recordMedia.dispose();
recordMedia = null;
// caller constructs a fresh instance when needed
recordMedia = new RecordMedia(await navigator.mediaDevices.getUserMedia({audio: true})); Defensive patterns
Strategy: validation
Validate before calling
if (!recordMedia || recordMedia['disposed']) throw new Error('Recorder already disposed; create a new instance'); Try / catch
try { await recordMedia.startRecording(); }
catch (e) { if (/disposed/i.test(e.message)) { recordMedia = null; /* build new */ } else throw e; } Prevention
- Treat RecordMedia as single-use; construct a new instance per recording.
- Set the reference to null after dispose() so reuse fails fast at the call site.
- Disable the start button once dispose() runs.
- Audit dialog teardown so it does not dispose a recorder still referenced by a start handler.
When it happens
Trigger: Calling startRecording() after dispose() — for example, a UI that lets the user start a new recording on the same RecordMedia instance after stop/dispose, or a race where the close handler disposes before the start button handler runs.
Common situations: Single-use instance accidentally reused; stop then immediately start without constructing a new RecordMedia; dialog teardown disposes the recorder while a pending start is queued.
Related errors
- MP3 encoder is not initialized
- AudioContext is not supported
- AudioWorklet is not supported
- Failed to save agent session
- Failed to remove agent session
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/de978832e38328dc.
Report an issue: GitHub.