82 lines
2.7 KiB
TypeScript
82 lines
2.7 KiB
TypeScript
/**
|
|
* 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<HTMLAudioElement | null>(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 };
|
|
}
|