- Playback: Fixed future Chrome recordings in cyberchat-app.js (line 865):

Records one finalized WebM instead of 250ms fragments.
Waits for final audio data before releasing the microphone.
Handles startup/finalization failures cleanly.
Bumped frontend cache version to .8.
Verified JavaScript syntax, JSON, browser rendering, and no console 
errors. Existing uploaded clips remain unchanged. A real microphone test 
wasn’t available in the browser runner.
References: MDN MediaRecorder, Chromium WebM metadata issue.
This commit is contained in:
Ty Clifford
2026-06-09 11:19:28 -04:00
parent eb57cde99f
commit 5ca55bf7c1
4 changed files with 50 additions and 19 deletions
+3 -1
View File
@@ -237,6 +237,8 @@ Recording selection is browser-driven:
5. AAC
6. Browser default MediaRecorder format
The recorder collects one finalized blob at stop instead of concatenating short live WebM slices. This avoids Chrome-generated fragment metadata that other browsers may reject.
When FFmpeg is disabled, the accepted browser recording is stored unchanged. WebM remains the preferred default when the browser supports it. Safari and iPhone can upload MP4/AAC directly.
When FFmpeg is enabled:
@@ -427,7 +429,7 @@ The diagnostic endpoint exposes deployment details and should be restricted or r
<script>
window.CYBERCHAT_BASE = '/chat';
</script>
<script src="/chat/assets/js/cyberchat-app.js?v=20260609.7"></script>
<script src="/chat/assets/js/cyberchat-app.js?v=20260609.8"></script>
<script>
CyberChat.init('#my-chat');
</script>
+44 -15
View File
@@ -862,12 +862,17 @@
if (!feature('recording_status')) return;
await api('messages.php', form({ action: 'recording', active: active ? '1' : '0' })).catch(() => {});
}
function releaseVoiceStream(stream) {
stream?.getTracks().forEach(track => track.stop());
if (state.voice.stream === stream) state.voice.stream = null;
}
async function toggleVoice() {
if (state.voice.recorder) return stopVoice(false);
discardVoice();
if (!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder) return toast('Voice recording is not supported');
let stream = null;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const candidates = [
'audio/webm;codecs=opus', 'audio/webm',
'audio/mp4;codecs=mp4a.40.2', 'audio/mp4', 'audio/aac'
@@ -894,17 +899,37 @@
catch { recorder = new MediaRecorder(stream); }
}
state.voice.stream = stream;
state.voice.chunks = [];
state.voice.started = Date.now();
const recordedChunks = [];
state.voice.chunks = recordedChunks;
const startedAt = Date.now();
state.voice.started = startedAt;
const recordedMime = recorder.mimeType || mime || 'audio/webm';
recorder.addEventListener('dataavailable', event => { if (event.data.size) state.voice.chunks.push(event.data); });
recorder.addEventListener('dataavailable', event => { if (event.data.size) recordedChunks.push(event.data); });
recorder.addEventListener('stop', () => {
if (!recorder._discard) finishVoice(
new Blob(state.voice.chunks, { type: recordedMime }),
(Date.now() - state.voice.started) / 1000
);
releaseVoiceStream(stream);
if (!recorder._discard) {
const blob = recordedChunks.length === 1
? recordedChunks[0]
: new Blob(recordedChunks, { type: recordedMime });
finishVoice(blob, (Date.now() - startedAt) / 1000);
}
}, { once: true });
recorder.start(250);
recorder.addEventListener('error', () => {
clearInterval(state.voice.timer);
clearInterval(state.voice.heartbeat);
state.voice.timer = state.voice.heartbeat = null;
void recording(false);
recorder._discard = true;
releaseVoiceStream(stream);
if (state.voice.recorder === recorder) state.voice.recorder = null;
$('#record')?.classList.remove('recording');
if ($('#record-label')) $('#record-label').textContent = 'RECORD';
if ($('#voice-status')) {
$('#voice-status').textContent = `VOICE MAX ${state.config.voice?.max_duration_seconds || 60}s · ${voiceBitrate()} KBPS`;
}
toast('The browser could not finalize this voice clip');
}, { once: true });
recorder.start();
state.voice.recorder = recorder;
$('#record').classList.add('recording');
$('#record-label').textContent = 'STOP';
@@ -912,7 +937,10 @@
state.voice.heartbeat = setInterval(() => recording(true), 4000);
state.voice.timer = setInterval(voiceTimer, 200);
voiceTimer();
} catch (e) { toast(e.name === 'NotAllowedError' ? 'Microphone permission was denied' : 'Could not access microphone'); }
} catch (e) {
releaseVoiceStream(stream);
toast(e.name === 'NotAllowedError' ? 'Microphone permission was denied' : 'Could not access microphone');
}
}
function voiceTimer() {
const max = state.config.voice?.max_duration_seconds || 60;
@@ -927,13 +955,14 @@
clearInterval(state.voice.heartbeat);
state.voice.timer = state.voice.heartbeat = null;
recording(false);
if (state.voice.recorder && state.voice.recorder.state !== 'inactive') {
state.voice.recorder._discard = discard;
state.voice.recorder.stop();
const recorder = state.voice.recorder;
if (recorder && recorder.state !== 'inactive') {
recorder._discard = discard;
recorder.stop();
} else {
releaseVoiceStream(state.voice.stream);
}
state.voice.recorder = null;
state.voice.stream?.getTracks().forEach(track => track.stop());
state.voice.stream = null;
$('#record')?.classList.remove('recording');
if ($('#record-label')) $('#record-label').textContent = 'RECORD';
}
+2 -2
View File
@@ -94,7 +94,7 @@
<span class="kw">&lt;div</span> id=<span class="str">"my-chat"</span> style=<span class="str">"width:100%; height:600px"</span><span class="kw">&gt;&lt;/div&gt;</span>
<span class="cm">&lt;!-- 4. Script + init --&gt;</span>
<span class="kw">&lt;script</span> src=<span class="str">"assets/js/cyberchat-app.js?v=20260609.7"</span><span class="kw">&gt;&lt;/script&gt;</span>
<span class="kw">&lt;script</span> src=<span class="str">"assets/js/cyberchat-app.js?v=20260609.8"</span><span class="kw">&gt;&lt;/script&gt;</span>
<span class="kw">&lt;script&gt;</span>
window.CYBERCHAT_BASE = <span class="str">'/path/to/cyberchat'</span>; <span class="cm">// adjust to your install path</span>
CyberChat.init(<span class="str">'#my-chat'</span>);
@@ -104,7 +104,7 @@
<script>
window.CYBERCHAT_BASE = '';
</script>
<script src="assets/js/cyberchat-app.js?v=20260609.7"></script>
<script src="assets/js/cyberchat-app.js?v=20260609.8"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
CyberChat.init('#chat-here');
+1 -1
View File
@@ -17,7 +17,7 @@
<script>
window.CYBERCHAT_BASE = '';
</script>
<script src="assets/js/cyberchat-app.js?v=20260609.7"></script>
<script src="assets/js/cyberchat-app.js?v=20260609.8"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
CyberChat.init('#app');