- 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
|
5. AAC
|
||||||
6. Browser default MediaRecorder format
|
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 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:
|
When FFmpeg is enabled:
|
||||||
@@ -427,7 +429,7 @@ The diagnostic endpoint exposes deployment details and should be restricted or r
|
|||||||
<script>
|
<script>
|
||||||
window.CYBERCHAT_BASE = '/chat';
|
window.CYBERCHAT_BASE = '/chat';
|
||||||
</script>
|
</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>
|
<script>
|
||||||
CyberChat.init('#my-chat');
|
CyberChat.init('#my-chat');
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+44
-15
@@ -862,12 +862,17 @@
|
|||||||
if (!feature('recording_status')) return;
|
if (!feature('recording_status')) return;
|
||||||
await api('messages.php', form({ action: 'recording', active: active ? '1' : '0' })).catch(() => {});
|
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() {
|
async function toggleVoice() {
|
||||||
if (state.voice.recorder) return stopVoice(false);
|
if (state.voice.recorder) return stopVoice(false);
|
||||||
discardVoice();
|
discardVoice();
|
||||||
if (!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder) return toast('Voice recording is not supported');
|
if (!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder) return toast('Voice recording is not supported');
|
||||||
|
let stream = null;
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
const candidates = [
|
const candidates = [
|
||||||
'audio/webm;codecs=opus', 'audio/webm',
|
'audio/webm;codecs=opus', 'audio/webm',
|
||||||
'audio/mp4;codecs=mp4a.40.2', 'audio/mp4', 'audio/aac'
|
'audio/mp4;codecs=mp4a.40.2', 'audio/mp4', 'audio/aac'
|
||||||
@@ -894,17 +899,37 @@
|
|||||||
catch { recorder = new MediaRecorder(stream); }
|
catch { recorder = new MediaRecorder(stream); }
|
||||||
}
|
}
|
||||||
state.voice.stream = stream;
|
state.voice.stream = stream;
|
||||||
state.voice.chunks = [];
|
const recordedChunks = [];
|
||||||
state.voice.started = Date.now();
|
state.voice.chunks = recordedChunks;
|
||||||
|
const startedAt = Date.now();
|
||||||
|
state.voice.started = startedAt;
|
||||||
const recordedMime = recorder.mimeType || mime || 'audio/webm';
|
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', () => {
|
recorder.addEventListener('stop', () => {
|
||||||
if (!recorder._discard) finishVoice(
|
releaseVoiceStream(stream);
|
||||||
new Blob(state.voice.chunks, { type: recordedMime }),
|
if (!recorder._discard) {
|
||||||
(Date.now() - state.voice.started) / 1000
|
const blob = recordedChunks.length === 1
|
||||||
);
|
? recordedChunks[0]
|
||||||
|
: new Blob(recordedChunks, { type: recordedMime });
|
||||||
|
finishVoice(blob, (Date.now() - startedAt) / 1000);
|
||||||
|
}
|
||||||
}, { once: true });
|
}, { 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;
|
state.voice.recorder = recorder;
|
||||||
$('#record').classList.add('recording');
|
$('#record').classList.add('recording');
|
||||||
$('#record-label').textContent = 'STOP';
|
$('#record-label').textContent = 'STOP';
|
||||||
@@ -912,7 +937,10 @@
|
|||||||
state.voice.heartbeat = setInterval(() => recording(true), 4000);
|
state.voice.heartbeat = setInterval(() => recording(true), 4000);
|
||||||
state.voice.timer = setInterval(voiceTimer, 200);
|
state.voice.timer = setInterval(voiceTimer, 200);
|
||||||
voiceTimer();
|
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() {
|
function voiceTimer() {
|
||||||
const max = state.config.voice?.max_duration_seconds || 60;
|
const max = state.config.voice?.max_duration_seconds || 60;
|
||||||
@@ -927,13 +955,14 @@
|
|||||||
clearInterval(state.voice.heartbeat);
|
clearInterval(state.voice.heartbeat);
|
||||||
state.voice.timer = state.voice.heartbeat = null;
|
state.voice.timer = state.voice.heartbeat = null;
|
||||||
recording(false);
|
recording(false);
|
||||||
if (state.voice.recorder && state.voice.recorder.state !== 'inactive') {
|
const recorder = state.voice.recorder;
|
||||||
state.voice.recorder._discard = discard;
|
if (recorder && recorder.state !== 'inactive') {
|
||||||
state.voice.recorder.stop();
|
recorder._discard = discard;
|
||||||
|
recorder.stop();
|
||||||
|
} else {
|
||||||
|
releaseVoiceStream(state.voice.stream);
|
||||||
}
|
}
|
||||||
state.voice.recorder = null;
|
state.voice.recorder = null;
|
||||||
state.voice.stream?.getTracks().forEach(track => track.stop());
|
|
||||||
state.voice.stream = null;
|
|
||||||
$('#record')?.classList.remove('recording');
|
$('#record')?.classList.remove('recording');
|
||||||
if ($('#record-label')) $('#record-label').textContent = 'RECORD';
|
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="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="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>
|
<span class="kw"><script></span>
|
||||||
window.CYBERCHAT_BASE = <span class="str">'/path/to/cyberchat'</span>; <span class="cm">// adjust to your install path</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>);
|
CyberChat.init(<span class="str">'#my-chat'</span>);
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window.CYBERCHAT_BASE = '';
|
window.CYBERCHAT_BASE = '';
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/js/cyberchat-app.js?v=20260609.7"></script>
|
<script src="assets/js/cyberchat-app.js?v=20260609.8"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
CyberChat.init('#chat-here');
|
CyberChat.init('#chat-here');
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window.CYBERCHAT_BASE = '';
|
window.CYBERCHAT_BASE = '';
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/js/cyberchat-app.js?v=20260609.7"></script>
|
<script src="assets/js/cyberchat-app.js?v=20260609.8"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
CyberChat.init('#app');
|
CyberChat.init('#app');
|
||||||
|
|||||||
Reference in New Issue
Block a user