feat(microphone): update audio recording error handling and update worklet import method

This commit is contained in:
Ivan
2026-04-23 17:46:08 -05:00
parent 546a5e8e16
commit 4b9c16f6c3
3 changed files with 51 additions and 6 deletions
@@ -5167,12 +5167,33 @@ export default {
},
buildAudioRecordingFailureMessage() {
if (!navigator?.mediaDevices || typeof navigator.mediaDevices.getUserMedia !== "function") {
return `${this.$t("messages.failed_start_recording")} (microphone API unavailable in this webview context)`;
return `${this.$t("messages.failed_start_recording")}. ${this.$t("messages.failed_start_recording_help_mediadevices")}`;
}
if (typeof MediaRecorder !== "function") {
return `${this.$t("messages.failed_start_recording")} (MediaRecorder not supported on this device)`;
const AudioContextCtor = globalThis.AudioContext || globalThis.webkitAudioContext;
if (typeof AudioContextCtor !== "function") {
return `${this.$t("messages.failed_start_recording")}. ${this.$t("messages.failed_start_recording_help_web_audio")}`;
}
return this.$t("messages.failed_start_recording");
let probe = null;
try {
probe = new AudioContextCtor();
if (!probe.audioWorklet || typeof probe.audioWorklet.addModule !== "function") {
return `${this.$t("messages.failed_start_recording")}. ${this.$t("messages.failed_start_recording_help_audio_worklet")}`;
}
} catch {
return `${this.$t("messages.failed_start_recording")}. ${this.$t("messages.failed_start_recording_help_web_audio")}`;
} finally {
try {
if (probe && typeof probe.close === "function") {
const closed = probe.close();
if (closed && typeof closed.catch === "function") {
void closed.catch(() => {});
}
}
} catch {
// ignore
}
}
return `${this.$t("messages.failed_start_recording")}. ${this.$t("messages.failed_start_recording_help_permission")}`;
},
removeFileAttachment: function (file) {
this.newMessageFiles = this.newMessageFiles.filter((newMessageFile) => {
@@ -6,7 +6,7 @@
* encode the audio to OGG/Opus using LXST's OpusFileSink without relying
* on ffmpeg or any browser MediaRecorder container output.
*/
import microphoneRecorderWorkletUrl from "./MicrophoneRecorder.worklet.js?url";
import microphoneRecorderWorkletSource from "./MicrophoneRecorder.worklet.js?raw";
class MicrophoneRecorder {
constructor() {
@@ -18,6 +18,7 @@ class MicrophoneRecorder {
this.pcmChunks = [];
this.sampleRate = 0;
this.channels = 1;
this._workletBlobUrl = null;
}
cleanupMediaStream() {
@@ -57,6 +58,14 @@ class MicrophoneRecorder {
} catch {
// ignore disconnect failures
}
if (this._workletBlobUrl) {
try {
URL.revokeObjectURL(this._workletBlobUrl);
} catch {
// ignore
}
this._workletBlobUrl = null;
}
if (this.audioContext && typeof this.audioContext.close === "function") {
try {
const closeResult = this.audioContext.close();
@@ -110,7 +119,21 @@ class MicrophoneRecorder {
return false;
}
await this.audioContext.audioWorklet.addModule(microphoneRecorderWorkletUrl);
const workletBlob = new Blob([microphoneRecorderWorkletSource], {
type: "application/javascript",
});
this._workletBlobUrl = URL.createObjectURL(workletBlob);
try {
await this.audioContext.audioWorklet.addModule(this._workletBlobUrl);
} catch {
try {
URL.revokeObjectURL(this._workletBlobUrl);
} catch {
// ignore
}
this._workletBlobUrl = null;
throw new Error("AudioWorklet addModule failed");
}
this.processorNode = new AudioWorkletNode(this.audioContext, "microphone-pcm-float", {
numberOfInputs: 1,
numberOfOutputs: 1,
@@ -133,6 +133,7 @@ describe("MicrophoneRecorder", () => {
expect(audio.ctx.createMediaStreamSource).toHaveBeenCalledTimes(1);
expect(audio.ctx.audioWorklet.addModule).toHaveBeenCalledTimes(1);
expect(audio.ctx.audioWorklet.addModule.mock.calls[0][0]).toMatch(/^blob:/);
expect(globalThis.AudioWorkletNode).toHaveBeenCalledWith(
audio.ctx,
"microphone-pcm-float",