diff --git a/README.md b/README.md
index b1903ad..d3e503a 100644
--- a/README.md
+++ b/README.md
@@ -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
-
+
diff --git a/assets/js/cyberchat-app.js b/assets/js/cyberchat-app.js
index 1502660..2e5c842 100644
--- a/assets/js/cyberchat-app.js
+++ b/assets/js/cyberchat-app.js
@@ -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';
}
diff --git a/embed-example.html b/embed-example.html
index 9a7819a..3ebb276 100644
--- a/embed-example.html
+++ b/embed-example.html
@@ -94,7 +94,7 @@
<div id="my-chat" style="width:100%; height:600px"></div>
<!-- 4. Script + init -->
-<script src="assets/js/cyberchat-app.js?v=20260609.7"></script>
+<script src="assets/js/cyberchat-app.js?v=20260609.8"></script>
<script>
window.CYBERCHAT_BASE = '/path/to/cyberchat'; // adjust to your install path
CyberChat.init('#my-chat');
@@ -104,7 +104,7 @@
-
+
-
+