/** * useTtsPlayback — central hook for TTS audio playback. * * Plays base64-encoded audio (MP3), manages current playback state, * emits lifecycle events. Used by all features (CommCoach, Workspace, etc.). */ import { useCallback, useRef, useState } from 'react'; export type TtsEvent = 'playing' | 'paused' | 'ended' | 'error'; export interface TtsPlaybackCallbacks { onPlaying?: () => void; onPaused?: () => void; onEnded?: () => void; onError?: () => void; } export interface TtsPlaybackApi { isPlaying: boolean; isPaused: boolean; play: (base64Audio: string, format?: string) => void; pause: () => void; resume: () => void; stop: () => void; } export function useTtsPlayback(callbacks?: TtsPlaybackCallbacks): TtsPlaybackApi { const [isPlaying, setIsPlaying] = useState(false); const [isPaused, setIsPaused] = useState(false); const audioRef = useRef(null); const cbRef = useRef(callbacks); cbRef.current = callbacks; const _emit = useCallback((event: TtsEvent) => { if (event === 'playing') { setIsPlaying(true); setIsPaused(false); cbRef.current?.onPlaying?.(); } else if (event === 'paused') { setIsPaused(true); cbRef.current?.onPaused?.(); } else if (event === 'ended') { setIsPlaying(false); setIsPaused(false); cbRef.current?.onEnded?.(); } else if (event === 'error') { setIsPlaying(false); setIsPaused(false); cbRef.current?.onError?.(); } }, []); const stop = useCallback(() => { if (audioRef.current) { const audio = audioRef.current; audio.onpause = null; audio.onended = null; audioRef.current = null; audio.pause(); } setIsPlaying(false); setIsPaused(false); }, []); const play = useCallback((base64Audio: string, format?: string) => { if (!base64Audio) return; stop(); try { const mimeType = format === 'wav' ? 'audio/wav' : 'audio/mp3'; const audio = new Audio(`data:${mimeType};base64,${base64Audio}`); audioRef.current = audio; audio.onended = () => { audioRef.current = null; _emit('ended'); }; audio.onpause = () => { if (audioRef.current === audio && audio.currentTime < audio.duration) _emit('paused'); }; audio.play().then(() => _emit('playing')).catch(() => _emit('error')); } catch { _emit('error'); } }, [stop, _emit]); const pause = useCallback(() => { if (audioRef.current && !audioRef.current.paused) { audioRef.current.pause(); } }, []); const resume = useCallback(() => { if (audioRef.current && audioRef.current.paused) { audioRef.current.play().then(() => _emit('playing')).catch(() => _emit('error')); } }, [_emit]); return { isPlaying, isPaused, play, pause, resume, stop }; }