- 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:
@@ -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
@@ -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
@@ -94,7 +94,7 @@
|
||||
<span class="kw"><div</span> id=<span class="str">"my-chat"</span> style=<span class="str">"width:100%; height:600px"</span><span class="kw">></div></span>
|
||||
|
||||
<span class="cm"><!-- 4. Script + init --></span>
|
||||
<span class="kw"><script</span> src=<span class="str">"assets/js/cyberchat-app.js?v=20260609.7"</span><span class="kw">></script></span>
|
||||
<span class="kw"><script</span> src=<span class="str">"assets/js/cyberchat-app.js?v=20260609.8"</span><span class="kw">></script></span>
|
||||
<span class="kw"><script></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
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user