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)
|
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)
|
15. Typing Realtime - `PARTIAL` (typing start/stop done; voice/video typing signals limited)
|
||||||
16. Media & Attachments - `DONE` (upload/preview/download/gallery)
|
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)
|
18. Circle Video Messages - `PARTIAL` (send/play present, recording UX basic)
|
||||||
19. Stickers - `PARTIAL` (web sticker picker with preset pack + favorites)
|
19. Stickers - `PARTIAL` (web sticker picker with preset pack + favorites)
|
||||||
20. GIF - `PARTIAL` (web GIF picker with Tenor search + preset fallback + 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() {
|
export function MessageComposer() {
|
||||||
const activeChatId = useChatStore((s) => s.activeChatId);
|
const activeChatId = useChatStore((s) => s.activeChatId);
|
||||||
const chats = useChatStore((s) => s.chats);
|
const chats = useChatStore((s) => s.chats);
|
||||||
@@ -562,7 +580,8 @@ export function MessageComposer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
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;
|
recordingStreamRef.current = stream;
|
||||||
chunksRef.current = [];
|
chunksRef.current = [];
|
||||||
sendVoiceOnStopRef.current = true;
|
sendVoiceOnStopRef.current = true;
|
||||||
@@ -584,13 +603,19 @@ export function MessageComposer() {
|
|||||||
setUploadError("Voice message is too short. Minimum length is 1 second.");
|
setUploadError("Voice message is too short. Minimum length is 1 second.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const blob = new Blob(data, { type: "audio/webm" });
|
const outputMime = recorder.mimeType || mimeType || "audio/webm";
|
||||||
const file = new File([blob], `voice-${Date.now()}.webm`, { type: "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);
|
const waveform = await buildWaveformPoints(blob, 64);
|
||||||
await handleUpload(file, "voice", waveform);
|
await handleUpload(file, "voice", waveform);
|
||||||
};
|
};
|
||||||
recorderRef.current = recorder;
|
recorderRef.current = recorder;
|
||||||
recorder.start();
|
recorder.start(250);
|
||||||
recordingStartedAtRef.current = Date.now();
|
recordingStartedAtRef.current = Date.now();
|
||||||
setRecordSeconds(0);
|
setRecordSeconds(0);
|
||||||
setRecordingState("recording");
|
setRecordingState("recording");
|
||||||
|
|||||||
Reference in New Issue
Block a user