diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue index dc362a7..a1fecb7 100644 --- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue +++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue @@ -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) => { diff --git a/meshchatx/src/frontend/js/MicrophoneRecorder.js b/meshchatx/src/frontend/js/MicrophoneRecorder.js index b96272a..d568ec3 100644 --- a/meshchatx/src/frontend/js/MicrophoneRecorder.js +++ b/meshchatx/src/frontend/js/MicrophoneRecorder.js @@ -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, diff --git a/tests/frontend/MicrophoneRecorder.test.js b/tests/frontend/MicrophoneRecorder.test.js index 0f5421b..470a279 100644 --- a/tests/frontend/MicrophoneRecorder.test.js +++ b/tests/frontend/MicrophoneRecorder.test.js @@ -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",