frontend_nyla/src/hooks/useTtsPlayback.ts
2026-05-06 23:28:15 +02:00

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 };
}