dramaling-vocab-learning/frontend/hooks/useAudio.ts

228 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useRef, useCallback } from 'react';
export interface TTSRequest {
text: string;
accent?: 'us' | 'uk';
speed?: number;
voice?: string;
}
export interface TTSResponse {
audioUrl: string;
duration: number;
cacheHit: boolean;
error?: string;
}
export interface AudioState {
isPlaying: boolean;
isLoading: boolean;
error: string | null;
currentAudio: string | null;
}
export function useAudio() {
const [state, setState] = useState<AudioState>({
isPlaying: false,
isLoading: false,
error: null,
currentAudio: null
});
const audioRef = useRef<HTMLAudioElement>(null);
const currentRequestRef = useRef<AbortController | null>(null);
// 更新狀態的輔助函數
const updateState = useCallback((updates: Partial<AudioState>) => {
setState(prev => ({ ...prev, ...updates }));
}, []);
// 生成音頻
const generateAudio = useCallback(async (request: TTSRequest): Promise<string | null> => {
try {
// 取消之前的請求
if (currentRequestRef.current) {
currentRequestRef.current.abort();
}
const controller = new AbortController();
currentRequestRef.current = controller;
updateState({ isLoading: true, error: null });
const token = localStorage.getItem('token');
if (!token) {
throw new Error('Authentication required');
}
const response = await fetch('/api/audio/tts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
text: request.text,
accent: request.accent || 'us',
speed: request.speed || 1.0,
voice: request.voice || ''
}),
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: TTSResponse = await response.json();
if (data.error) {
throw new Error(data.error);
}
updateState({ currentAudio: data.audioUrl });
return data.audioUrl;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return null; // 請求被取消
}
const errorMessage = error instanceof Error ? error.message : 'Failed to generate audio';
updateState({ error: errorMessage });
return null;
} finally {
updateState({ isLoading: false });
currentRequestRef.current = null;
}
}, [updateState]);
// 播放音頻
const playAudio = useCallback(async (audioUrl?: string, request?: TTSRequest) => {
try {
let urlToPlay = audioUrl;
// 如果沒有提供 URL嘗試生成
if (!urlToPlay && request) {
const generatedUrl = await generateAudio(request);
urlToPlay = generatedUrl || undefined;
if (!urlToPlay) return false;
}
if (!urlToPlay) {
updateState({ error: 'No audio URL provided' });
return false;
}
// 創建新的音頻元素或使用現有的
let audio = audioRef.current;
if (!audio) {
audio = new Audio();
audioRef.current = audio;
}
// 設置音頻事件監聽器
const handleEnded = () => {
updateState({ isPlaying: false });
audio?.removeEventListener('ended', handleEnded);
audio?.removeEventListener('error', handleError);
};
const handleError = () => {
updateState({ isPlaying: false, error: 'Audio playback failed' });
audio?.removeEventListener('ended', handleEnded);
audio?.removeEventListener('error', handleError);
};
audio.addEventListener('ended', handleEnded);
audio.addEventListener('error', handleError);
// 設置音頻源並播放
audio.src = urlToPlay;
await audio.play();
updateState({ isPlaying: true, error: null });
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to play audio';
updateState({ error: errorMessage, isPlaying: false });
return false;
}
}, [generateAudio, updateState]);
// 暫停音頻
const pauseAudio = useCallback(() => {
const audio = audioRef.current;
if (audio) {
audio.pause();
updateState({ isPlaying: false });
}
}, [updateState]);
// 停止音頻
const stopAudio = useCallback(() => {
const audio = audioRef.current;
if (audio) {
audio.pause();
audio.currentTime = 0;
updateState({ isPlaying: false });
}
}, [updateState]);
// 切換播放/暫停
const togglePlayPause = useCallback(async (audioUrl?: string, request?: TTSRequest) => {
if (state.isPlaying) {
pauseAudio();
} else {
await playAudio(audioUrl, request);
}
}, [state.isPlaying, playAudio, pauseAudio]);
// 設置音量
const setVolume = useCallback((volume: number) => {
const audio = audioRef.current;
if (audio) {
audio.volume = Math.max(0, Math.min(1, volume));
}
}, []);
// 設置播放速度
const setPlaybackRate = useCallback((rate: number) => {
const audio = audioRef.current;
if (audio) {
audio.playbackRate = Math.max(0.25, Math.min(4, rate));
}
}, []);
// 清除錯誤
const clearError = useCallback(() => {
updateState({ error: null });
}, [updateState]);
// 清理函數
const cleanup = useCallback(() => {
if (currentRequestRef.current) {
currentRequestRef.current.abort();
}
stopAudio();
}, [stopAudio]);
return {
// 狀態
...state,
// 操作方法
generateAudio,
playAudio,
pauseAudio,
stopAudio,
togglePlayPause,
setVolume,
setPlaybackRate,
clearError,
cleanup
};
}