如何彻底销毁AudioContext?解决WebAudio录制上下文泄漏问题
WebAudio录制方案的AudioContext资源泄漏问题
我实现了一个基于WebAudio的录制方案,使用AudioContext进行解码。该方案仅将MediaStreamSourceNode连接至AudioWorklet,后者会将输入缓冲区发送给调用方。
但我遇到了录制结束后上下文泄漏的问题:多次启停录制后,开发者工具会创建新的源上下文,性能分析面板显示多个AudioContext线程仍在执行少量任务,本应被清理。
停止录制的流程如下:
- 向AudioWorkletProcessor发送
Finish消息。 - AudioWorkletProcessor刷新内部缓冲区,返回
Finished消息,并从process()返回false。 - 调用方收到
Finished后,停止MediaStreamTrack,断开节点连接,并调用AudioContext的close()方法。
目前不清楚还能采取哪些措施让浏览器彻底清理这些资源。
调用方代码
const context = new AudioContext({ sampleRate: sampleRate }); const modulePromise = context.audioWorklet.addModule(recorderModule); const stream = await prewarmedMediaStream.getMediaStream(); const audioTrack = stream.getAudioTracks()[0]; const source = mono(context.createMediaStreamSource(stream)); // Audio worklet nodes run in a separate thread and communicate via message passing. // This seems to be the only way to convince the browser to give us PCM audio. await modulePromise; const recorder = mono(new AudioWorkletNode(context, "PcmRecorder")); source.connect(recorder); const recordingStopped = es6WithResolvers<void>(); let recordingEndedError: AsyncError | null; recorder.port.onmessage = async (event) => { const msg = event.data as IncomingMessage; if (msg.type == "Audio") listener.onDataAvailable?.(msg.data); if (msg.type == "Finished") { audioTrack.stop(); source.disconnect(); recorder.disconnect(); if (context.state == "running") { await context.close(); } if (recordingEndedError) { listener?.onRecordingTerminated?.(recordingEndedError); } else { try { listener?.onRecordingCompleted?.(); } catch (e) { } } recordingStopped.resolve(); } }; recorder.onprocessorerror = console.log; const stopRecording = (error?: AsyncError | null) => { recorder.port.postMessage({ type: "Finish" }); recordingEndedError = error; return recordingStopped.promise; }; audioTrack.onended = () => { stopRecording(new AudioEndedError()); }; await context.resume(); return { stopRecording: stopRecording };
录制节点代码
class PcmRecorder extends AudioWorkletProcessor { constructor() { super(); this.buffers = []; this.finished = false; this.port.onmessage = event => { const message = event.data; if (message.type == "Finish") this.finish = true; }; } process(inputs) { const floats = inputs[0][0]; const samples = Int16Array.from(floats, this.convertSample); this.buffers.push(samples); const total = this.buffers.reduce((total, arr) => total + arr.length, 0); if (total > 1600 || this.finish) { const mergedBuffer = new Int16Array(total); let writeLocation = 0; for (const source of this.buffers) { mergedBuffer.set(source, writeLocation); writeLocation += source.length; } this.port.postMessage({ type: "Audio", data: new Uint8Array(mergedBuffer.buffer) }); this.buffers = []; } if (this.finish) this.port.postMessage({ type: "Finished" }); return !this.finish; } convertSample(floatSample) { return Math.max(-32768, Math.min(Math.round(floatSample * 32768), 32767)); } }
内容的提问来源于stack exchange,提问作者Mark Boyall
相关产品推荐
相关产品推荐

