diff --git a/docs/core-checklist-status.md b/docs/core-checklist-status.md index 8780d15..8997bdb 100644 --- a/docs/core-checklist-status.md +++ b/docs/core-checklist-status.md @@ -23,7 +23,7 @@ Legend: 14. Delivery Status - `DONE` (sent/delivered/read + reconnect reconciliation after backend restarts) 15. Typing Realtime - `DONE` (typing start/stop + recording voice start/stop + recording video start/stop in circle-video send flow) 16. Media & Attachments - `DONE` (upload/preview/download/gallery; sticker/GIF inline media no longer opens photo viewer on click) -17. Voice Messages - `PARTIAL` (record/send/play/seek + global speed 1x/1.5x/2x; recorder uses mime fallback + chunked capture; websocket send/recorder stop race on fast chat switch is guarded; UX still being polished) +17. Voice Messages - `PARTIAL` (record/send/play/seek + global speed 1x/1.5x/2x; recorder uses improved mime priority for better duration metadata; audio store tracks duration via `durationchange/seekable` fallback; websocket send/recorder stop race on fast chat switch is guarded; 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) diff --git a/web/src/components/MessageComposer.tsx b/web/src/components/MessageComposer.tsx index cdf6ea9..551d112 100644 --- a/web/src/components/MessageComposer.tsx +++ b/web/src/components/MessageComposer.tsx @@ -64,10 +64,10 @@ function pickSupportedAudioMimeType(): string | undefined { return undefined; } const candidates = [ + "audio/ogg;codecs=opus", + "audio/mp4", "audio/webm;codecs=opus", "audio/webm", - "audio/mp4", - "audio/ogg;codecs=opus", ]; for (const mime of candidates) { if (MediaRecorder.isTypeSupported(mime)) { diff --git a/web/src/store/audioPlayerStore.ts b/web/src/store/audioPlayerStore.ts index 79575e0..b5fa69c 100644 --- a/web/src/store/audioPlayerStore.ts +++ b/web/src/store/audioPlayerStore.ts @@ -47,6 +47,23 @@ function ensureAudio(): HTMLAudioElement { return globalAudioEl; } +function getPlayableDuration(audio: HTMLAudioElement): number { + if (Number.isFinite(audio.duration) && audio.duration > 0) { + return audio.duration; + } + try { + if (audio.seekable && audio.seekable.length > 0) { + const end = audio.seekable.end(audio.seekable.length - 1); + if (Number.isFinite(end) && end > 0) { + return end; + } + } + } catch { + return 0; + } + return 0; +} + export const useAudioPlayerStore = create((set, get) => ({ track: null, audioEl: ensureAudio(), @@ -59,10 +76,13 @@ export const useAudioPlayerStore = create((set, get) => ({ const audio = ensureAudio(); if (!isBound) { audio.addEventListener("timeupdate", () => { - set({ position: audio.currentTime || 0 }); + set({ position: audio.currentTime || 0, duration: getPlayableDuration(audio) }); }); audio.addEventListener("loadedmetadata", () => { - set({ duration: Number.isFinite(audio.duration) ? audio.duration : 0 }); + set({ duration: getPlayableDuration(audio) }); + }); + audio.addEventListener("durationchange", () => { + set({ duration: getPlayableDuration(audio) }); }); audio.addEventListener("play", () => set({ isPlaying: true })); audio.addEventListener("pause", () => set({ isPlaying: false }));