fix: robust mic toggle + Tailscale MediaRecorder fallback — v0.50.94 (PR #715)
Fixes and extends PR #683 (MatzAgent). recognition.start() is now a real call. _isRecording race guard added with correct reset in all paths. localStorage persistence of fallback flag. Closes #683. Co-authored-by: MatzAgent <MatzAgent@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Hermes Web UI -- Changelog
|
# Hermes Web UI -- Changelog
|
||||||
|
|
||||||
|
## [v0.50.94] — 2026-04-19
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Mic toggle is now race-safe and works over Tailscale** — rapid click/toggle no longer leaves recording in inconsistent state (`_isRecording` flag with proper reset in all paths). `recognition.start()` is now correctly called (was previously only present in a comment string, so SpeechRecognition never started and the Tailscale fallback never fired). Falls back to `MediaRecorder` when `speech.googleapis.com` is unreachable. Browser capability preference persisted in `localStorage` across reloads. (PR #683 by @MatzAgent)
|
||||||
|
|
||||||
## [v0.50.93] — 2026-04-19
|
## [v0.50.93] — 2026-04-19
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -185,18 +185,23 @@ $('btnAttach').onclick=()=>$('fileInput').click();
|
|||||||
const _canRecordAudio=!!(navigator.mediaDevices&&navigator.mediaDevices.getUserMedia&&window.MediaRecorder);
|
const _canRecordAudio=!!(navigator.mediaDevices&&navigator.mediaDevices.getUserMedia&&window.MediaRecorder);
|
||||||
if(!SpeechRecognition&&!_canRecordAudio) return; // Browser unsupported — mic button stays hidden
|
if(!SpeechRecognition&&!_canRecordAudio) return; // Browser unsupported — mic button stays hidden
|
||||||
|
|
||||||
|
// Persist SR failure across reloads (e.g. Tailscale/network error)
|
||||||
|
const _micForceMediaRecorderKey='mic_force_mediarecorder';
|
||||||
|
let _forceMediaRecorder=!SpeechRecognition||localStorage.getItem(_micForceMediaRecorderKey)==='1';
|
||||||
|
|
||||||
const btn=$('btnMic');
|
const btn=$('btnMic');
|
||||||
const status=$('micStatus');
|
const status=$('micStatus');
|
||||||
const ta=$('msg');
|
const ta=$('msg');
|
||||||
const statusText=status?status.querySelector('.status-text'):null;
|
const statusText=status?status.querySelector('.status-text'):null;
|
||||||
btn.style.display=''; // Show button — browser supports speech recognition or recording fallback
|
btn.style.display=''; // Show button — browser supports speech recognition or recording fallback
|
||||||
|
|
||||||
let recognition=SpeechRecognition?new SpeechRecognition():null;
|
let recognition=(!_forceMediaRecorder&&SpeechRecognition)?new SpeechRecognition():null;
|
||||||
let mediaRecorder=null;
|
let mediaRecorder=null;
|
||||||
let mediaStream=null;
|
let mediaStream=null;
|
||||||
let audioChunks=[];
|
let audioChunks=[];
|
||||||
let _finalText='';
|
let _finalText='';
|
||||||
let _prefix='';
|
let _prefix='';
|
||||||
|
let _isRecording=false;
|
||||||
|
|
||||||
function _setRecording(on){
|
function _setRecording(on){
|
||||||
window._micActive=on;
|
window._micActive=on;
|
||||||
@@ -261,7 +266,7 @@ $('btnAttach').onclick=()=>$('fileInput').click();
|
|||||||
}
|
}
|
||||||
window._stopMic=_stopMic; // expose for send-guard above
|
window._stopMic=_stopMic; // expose for send-guard above
|
||||||
|
|
||||||
if(recognition){
|
if(recognition && !_forceMediaRecorder){
|
||||||
recognition.continuous=false;
|
recognition.continuous=false;
|
||||||
recognition.interimResults=true;
|
recognition.interimResults=true;
|
||||||
recognition.lang=(typeof _locale!=='undefined'&&_locale._speech)||'en-US';
|
recognition.lang=(typeof _locale!=='undefined'&&_locale._speech)||'en-US';
|
||||||
@@ -298,6 +303,13 @@ $('btnAttach').onclick=()=>$('fileInput').click();
|
|||||||
recognition.onerror=(event)=>{
|
recognition.onerror=(event)=>{
|
||||||
_setRecording(false);
|
_setRecording(false);
|
||||||
window._micPendingSend=false;
|
window._micPendingSend=false;
|
||||||
|
_isRecording=false;
|
||||||
|
if(event.error==='network'||event.error==='not-allowed'){
|
||||||
|
// Persist SR failure: next reload will skip SpeechRecognition
|
||||||
|
localStorage.setItem(_micForceMediaRecorderKey,'1');
|
||||||
|
_forceMediaRecorder=true;
|
||||||
|
recognition=null;
|
||||||
|
}
|
||||||
const msgs={
|
const msgs={
|
||||||
'not-allowed':t('mic_denied'),
|
'not-allowed':t('mic_denied'),
|
||||||
'no-speech':t('mic_no_speech'),
|
'no-speech':t('mic_no_speech'),
|
||||||
@@ -308,18 +320,26 @@ $('btnAttach').onclick=()=>$('fileInput').click();
|
|||||||
}
|
}
|
||||||
|
|
||||||
btn.onclick=async()=>{
|
btn.onclick=async()=>{
|
||||||
|
// Race-condition guard: ignore rapid double-clicks
|
||||||
|
if(_isRecording){
|
||||||
|
_stopMic();
|
||||||
|
_isRecording=false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(window._micActive){
|
if(window._micActive){
|
||||||
_stopMic();
|
_stopMic();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_isRecording=true;
|
||||||
_finalText='';
|
_finalText='';
|
||||||
_prefix=ta.value;
|
_prefix=ta.value;
|
||||||
if(recognition){
|
if(recognition && !_forceMediaRecorder){
|
||||||
recognition.start();
|
recognition.start();
|
||||||
_setRecording(true);
|
_setRecording(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(!_canRecordAudio){
|
if(!_canRecordAudio){
|
||||||
|
_isRecording=false;
|
||||||
showToast(t('mic_network'));
|
showToast(t('mic_network'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -331,12 +351,14 @@ $('btnAttach').onclick=()=>$('fileInput').click();
|
|||||||
audioChunks=[];
|
audioChunks=[];
|
||||||
mediaRecorder.ondataavailable=e=>{if(e.data&&e.data.size)audioChunks.push(e.data);};
|
mediaRecorder.ondataavailable=e=>{if(e.data&&e.data.size)audioChunks.push(e.data);};
|
||||||
mediaRecorder.onerror=()=>{
|
mediaRecorder.onerror=()=>{
|
||||||
|
_isRecording=false;
|
||||||
_setRecording(false);
|
_setRecording(false);
|
||||||
window._micPendingSend=false;
|
window._micPendingSend=false;
|
||||||
_stopTracks();
|
_stopTracks();
|
||||||
showToast(t('mic_network'));
|
showToast(t('mic_network'));
|
||||||
};
|
};
|
||||||
mediaRecorder.onstop=async()=>{
|
mediaRecorder.onstop=async()=>{
|
||||||
|
_isRecording=false;
|
||||||
const blob=new Blob(audioChunks,{type:mediaRecorder.mimeType||mimeType||'audio/webm'});
|
const blob=new Blob(audioChunks,{type:mediaRecorder.mimeType||mimeType||'audio/webm'});
|
||||||
_setRecording(false);
|
_setRecording(false);
|
||||||
_stopTracks();
|
_stopTracks();
|
||||||
@@ -348,6 +370,7 @@ $('btnAttach').onclick=()=>$('fileInput').click();
|
|||||||
mediaRecorder.start();
|
mediaRecorder.start();
|
||||||
_setRecording(true);
|
_setRecording(true);
|
||||||
}catch(err){
|
}catch(err){
|
||||||
|
_isRecording=false;
|
||||||
window._micPendingSend=false;
|
window._micPendingSend=false;
|
||||||
_stopTracks();
|
_stopTracks();
|
||||||
showToast(t('mic_denied'));
|
showToast(t('mic_denied'));
|
||||||
|
|||||||
Reference in New Issue
Block a user