/** * 语音通话组合式函数 * * 功能: * - 整合 WebRTC 管理和信令服务 * - 管理通话状态 * - 处理语音转文字 */ import { ref, computed, onUnmounted } from 'vue' import { ElMessage } from 'element-plus' import { WebRTCManager, createWebRTCManager, type ConnectionState } from '@/utils/webrtc' import request from '@/api/request' export type VoiceCallState = 'idle' | 'requesting' | 'ringing' | 'connecting' | 'connected' | 'ended' export interface UseVoiceCallOptions { roomCode: string onTranscript?: (text: string, isFinal: boolean) => void onRemoteTranscript?: (text: string) => void } export function useVoiceCall(options: UseVoiceCallOptions) { const { roomCode, onTranscript, onRemoteTranscript } = options // ==================== 状态 ==================== const callState = ref('idle') const connectionState = ref('idle') const isMuted = ref(false) const isRemoteMuted = ref(false) const localAudioLevel = ref(0) const remoteAudioLevel = ref(0) const callDuration = ref(0) const errorMessage = ref(null) // 语音识别相关 const isTranscribing = ref(false) const currentTranscript = ref('') // 内部状态 let webrtcManager: WebRTCManager | null = null let recognition: any = null // SpeechRecognition let callTimer: number | null = null let audioLevelTimer: number | null = null // ==================== 计算属性 ==================== const isCallActive = computed(() => ['requesting', 'ringing', 'connecting', 'connected'].includes(callState.value) ) const canStartCall = computed(() => callState.value === 'idle') const canEndCall = computed(() => isCallActive.value) // ==================== 信令 API ==================== async function sendSignal(signalType: string, payload: any) { try { await request.post(`/api/v1/practice/rooms/${roomCode}/signal`, { signal_type: signalType, payload }) } catch (error) { console.error('[VoiceCall] 发送信令失败:', error) throw error } } // ==================== 通话控制 ==================== /** * 发起语音通话 */ async function startCall() { if (!canStartCall.value) { console.warn('[VoiceCall] 无法发起通话,当前状态:', callState.value) return } try { callState.value = 'requesting' errorMessage.value = null // 创建 WebRTC 管理器 webrtcManager = createWebRTCManager({ onConnectionStateChange: handleConnectionStateChange, onIceCandidate: handleIceCandidate, onRemoteStream: handleRemoteStream, onError: handleError }) // 创建 Offer const offer = await webrtcManager.createOffer() // 发送开始信令 await sendSignal('voice_start', {}) // 发送 Offer await sendSignal('voice_offer', { type: offer.type, sdp: offer.sdp }) callState.value = 'ringing' ElMessage.info('正在呼叫对方...') } catch (error: any) { console.error('[VoiceCall] 发起通话失败:', error) errorMessage.value = error.message || '发起通话失败' callState.value = 'idle' webrtcManager?.close() webrtcManager = null ElMessage.error(errorMessage.value) } } /** * 接听语音通话 */ async function answerCall(offer: RTCSessionDescriptionInit) { try { callState.value = 'connecting' errorMessage.value = null // 创建 WebRTC 管理器 webrtcManager = createWebRTCManager({ onConnectionStateChange: handleConnectionStateChange, onIceCandidate: handleIceCandidate, onRemoteStream: handleRemoteStream, onError: handleError }) // 处理 Offer 并创建 Answer const answer = await webrtcManager.handleOffer(offer) // 发送 Answer await sendSignal('voice_answer', { type: answer.type, sdp: answer.sdp }) ElMessage.success('已接听通话') } catch (error: any) { console.error('[VoiceCall] 接听通话失败:', error) errorMessage.value = error.message || '接听通话失败' callState.value = 'idle' webrtcManager?.close() webrtcManager = null ElMessage.error(errorMessage.value) } } /** * 拒绝来电 */ async function rejectCall() { try { await sendSignal('voice_end', { reason: 'rejected' }) callState.value = 'idle' } catch (error) { console.error('[VoiceCall] 拒绝通话失败:', error) } } /** * 结束通话 */ async function endCall() { try { await sendSignal('voice_end', { reason: 'ended' }) } catch (error) { console.error('[VoiceCall] 发送结束信令失败:', error) } cleanup() callState.value = 'ended' // 延迟恢复到 idle 状态 setTimeout(() => { callState.value = 'idle' }, 1000) } /** * 切换静音 */ function toggleMute() { if (webrtcManager) { isMuted.value = !isMuted.value webrtcManager.setMuted(isMuted.value) } } // ==================== 信令处理 ==================== /** * 处理接收到的信令消息 */ async function handleSignal(signalType: string, payload: any, fromUserId: number) { console.log('[VoiceCall] 收到信令:', signalType) switch (signalType) { case 'voice_start': // 收到通话请求 if (callState.value === 'idle') { callState.value = 'ringing' ElMessage.info('收到语音通话请求') } break case 'voice_offer': // 收到 Offer,自动接听 if (callState.value === 'ringing' || callState.value === 'idle') { await answerCall({ type: payload.type, sdp: payload.sdp }) } break case 'voice_answer': // 收到 Answer if (webrtcManager && callState.value === 'ringing') { await webrtcManager.handleAnswer({ type: payload.type, sdp: payload.sdp }) } break case 'ice_candidate': // 收到 ICE 候选 if (webrtcManager && payload.candidate) { await webrtcManager.addIceCandidate(payload) } break case 'voice_end': // 对方结束通话 cleanup() callState.value = 'ended' ElMessage.info('通话已结束') setTimeout(() => { callState.value = 'idle' }, 1000) break } } // ==================== WebRTC 回调 ==================== function handleConnectionStateChange(state: ConnectionState) { connectionState.value = state if (state === 'connected') { callState.value = 'connected' startCallTimer() startAudioLevelMonitor() startSpeechRecognition() ElMessage.success('语音通话已连接') } else if (state === 'failed' || state === 'disconnected') { if (callState.value === 'connected') { ElMessage.warning('通话连接断开') } } } async function handleIceCandidate(candidate: RTCIceCandidate) { try { await sendSignal('ice_candidate', candidate.toJSON()) } catch (error) { console.error('[VoiceCall] 发送 ICE 候选失败:', error) } } function handleRemoteStream(stream: MediaStream) { console.log('[VoiceCall] 收到远程音频流') // 播放远程音频 const audio = new Audio() audio.srcObject = stream audio.play().catch(e => console.error('[VoiceCall] 播放远程音频失败:', e)) } function handleError(error: Error) { console.error('[VoiceCall] WebRTC 错误:', error) errorMessage.value = error.message } // ==================== 语音识别 ==================== function startSpeechRecognition() { // 检查浏览器支持 const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition if (!SpeechRecognition) { console.warn('[VoiceCall] 浏览器不支持语音识别') return } recognition = new SpeechRecognition() recognition.continuous = true recognition.interimResults = true recognition.lang = 'zh-CN' recognition.onstart = () => { isTranscribing.value = true console.log('[VoiceCall] 语音识别已启动') } recognition.onresult = (event: any) => { let interimTranscript = '' let finalTranscript = '' for (let i = event.resultIndex; i < event.results.length; i++) { const transcript = event.results[i][0].transcript if (event.results[i].isFinal) { finalTranscript += transcript } else { interimTranscript += transcript } } currentTranscript.value = interimTranscript || finalTranscript if (finalTranscript) { onTranscript?.(finalTranscript, true) } else if (interimTranscript) { onTranscript?.(interimTranscript, false) } } recognition.onerror = (event: any) => { console.error('[VoiceCall] 语音识别错误:', event.error) if (event.error !== 'no-speech') { isTranscribing.value = false } } recognition.onend = () => { // 如果通话还在进行,重新启动识别 if (callState.value === 'connected' && !isMuted.value) { recognition.start() } else { isTranscribing.value = false } } try { recognition.start() } catch (error) { console.error('[VoiceCall] 启动语音识别失败:', error) } } function stopSpeechRecognition() { if (recognition) { recognition.stop() recognition = null } isTranscribing.value = false } // ==================== 辅助功能 ==================== function startCallTimer() { callDuration.value = 0 callTimer = window.setInterval(() => { callDuration.value++ }, 1000) } function stopCallTimer() { if (callTimer) { clearInterval(callTimer) callTimer = null } } function startAudioLevelMonitor() { audioLevelTimer = window.setInterval(async () => { if (webrtcManager) { const localStream = webrtcManager.getLocalStream() const remoteStream = webrtcManager.getRemoteStream() if (localStream) { localAudioLevel.value = await webrtcManager.getAudioLevel(localStream) } if (remoteStream) { remoteAudioLevel.value = await webrtcManager.getAudioLevel(remoteStream) } } }, 100) } function stopAudioLevelMonitor() { if (audioLevelTimer) { clearInterval(audioLevelTimer) audioLevelTimer = null } } function formatDuration(seconds: number): string { const mins = Math.floor(seconds / 60) const secs = seconds % 60 return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` } // ==================== 清理 ==================== function cleanup() { stopCallTimer() stopAudioLevelMonitor() stopSpeechRecognition() webrtcManager?.close() webrtcManager = null isMuted.value = false isRemoteMuted.value = false localAudioLevel.value = 0 remoteAudioLevel.value = 0 currentTranscript.value = '' } // 组件卸载时清理 onUnmounted(() => { if (isCallActive.value) { endCall() } cleanup() }) // ==================== 返回 ==================== return { // 状态 callState, connectionState, isMuted, isRemoteMuted, localAudioLevel, remoteAudioLevel, callDuration, errorMessage, isTranscribing, currentTranscript, // 计算属性 isCallActive, canStartCall, canEndCall, // 方法 startCall, answerCall, rejectCall, endCall, toggleMute, handleSignal, formatDuration } }