voice: harden recorder capture with mime fallback and chunked start
All checks were successful
CI / test (push) Successful in 26s
All checks were successful
CI / test (push) Successful in 26s
This commit is contained in:
@@ -23,7 +23,7 @@ Legend:
|
||||
14. Delivery Status - `DONE` (sent/delivered/read + reconnect reconciliation after backend restarts)
|
||||
15. Typing Realtime - `PARTIAL` (typing start/stop done; voice/video typing signals limited)
|
||||
16. Media & Attachments - `DONE` (upload/preview/download/gallery)
|
||||
17. Voice Messages - `PARTIAL` (record/send/play/seek + global speed 1x/1.5x/2x; UX still being polished)
|
||||
17. Voice Messages - `PARTIAL` (record/send/play/seek + global speed 1x/1.5x/2x; recorder now uses mime fallback + chunked capture; UX still being polished)
|
||||
18. Circle Video Messages - `PARTIAL` (send/play present, recording UX basic)
|
||||
19. Stickers - `PARTIAL` (web sticker picker with preset pack + favorites)
|
||||
20. GIF - `PARTIAL` (web GIF picker with Tenor search + preset fallback + favorites)
|
||||
|
||||
@@ -59,6 +59,24 @@ function saveFavorites(key: string, values: Set<string>): void {
|
||||
}
|
||||
}
|
||||
|
||||
function pickSupportedAudioMimeType(): string | undefined {
|
||||
if (typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") {
|
||||
return undefined;
|
||||
}
|
||||
const candidates = [
|
||||
"audio/webm;codecs=opus",
|
||||
"audio/webm",
|
||||
"audio/mp4",
|
||||
"audio/ogg;codecs=opus",
|
||||
];
|
||||
for (const mime of candidates) {
|
||||
if (MediaRecorder.isTypeSupported(mime)) {
|
||||
return mime;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function MessageComposer() {
|
||||
const activeChatId = useChatStore((s) => s.activeChatId);
|
||||
const chats = useChatStore((s) => s.chats);
|
||||
@@ -562,7 +580,8 @@ export function MessageComposer() {
|
||||
}
|
||||
}
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const recorder = new MediaRecorder(stream);
|
||||
const mimeType = pickSupportedAudioMimeType();
|
||||
const recorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
|
||||
recordingStreamRef.current = stream;
|
||||
chunksRef.current = [];
|
||||
sendVoiceOnStopRef.current = true;
|
||||
@@ -584,13 +603,19 @@ export function MessageComposer() {
|
||||
setUploadError("Voice message is too short. Minimum length is 1 second.");
|
||||
return;
|
||||
}
|
||||
const blob = new Blob(data, { type: "audio/webm" });
|
||||
const file = new File([blob], `voice-${Date.now()}.webm`, { type: "audio/webm" });
|
||||
const outputMime = recorder.mimeType || mimeType || "audio/webm";
|
||||
const blob = new Blob(data, { type: outputMime });
|
||||
if (blob.size < 512) {
|
||||
setUploadError("Voice message is empty. Please try recording again.");
|
||||
return;
|
||||
}
|
||||
const ext = outputMime.includes("ogg") ? "ogg" : outputMime.includes("mp4") ? "m4a" : "webm";
|
||||
const file = new File([blob], `voice-${Date.now()}.${ext}`, { type: outputMime });
|
||||
const waveform = await buildWaveformPoints(blob, 64);
|
||||
await handleUpload(file, "voice", waveform);
|
||||
};
|
||||
recorderRef.current = recorder;
|
||||
recorder.start();
|
||||
recorder.start(250);
|
||||
recordingStartedAtRef.current = Date.now();
|
||||
setRecordSeconds(0);
|
||||
setRecordingState("recording");
|
||||
|
||||
Reference in New Issue
Block a user