2025-12-03 15:58:57 +08:00
|
|
|
import { reactive, shallowRef, ref } from 'vue'
|
2025-12-03 13:25:02 +08:00
|
|
|
import * as systemApi from '@/api/modules/system'
|
|
|
|
|
import * as messageApi from '@/api/modules/message'
|
|
|
|
|
import { wsManager } from '@/api/websocket'
|
2025-12-03 15:33:48 +08:00
|
|
|
import { useToastStore } from '@/stores/toast'
|
2025-12-03 15:58:57 +08:00
|
|
|
import { useChatStore } from '@/stores/chat'
|
2025-12-03 13:25:02 +08:00
|
|
|
import type { ChatMessage } from '@/types/api'
|
|
|
|
|
import type { CallStatus } from '@/types/message'
|
|
|
|
|
|
|
|
|
|
export interface CallState {
|
|
|
|
|
active: boolean
|
|
|
|
|
minimized: boolean
|
|
|
|
|
type: 'audio' | 'video'
|
|
|
|
|
status: 'idle' | 'outgoing' | 'incoming' | 'connected'
|
|
|
|
|
statusText: string
|
|
|
|
|
id: string | null
|
|
|
|
|
muted: boolean
|
|
|
|
|
camOff: boolean
|
2025-12-03 15:58:57 +08:00
|
|
|
remoteCamOff: boolean // 新增:对方是否关闭摄像头
|
2025-12-03 15:33:48 +08:00
|
|
|
duration: number
|
|
|
|
|
startTime: number | null
|
2025-12-03 13:25:02 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
// 铃声资源 (使用在线资源作为示例)
|
|
|
|
|
const RINGTONE_INCOMING = 'https://assets.mixkit.co/active_storage/sfx/2860/2860-64-bit.mp3' // 电话铃声
|
|
|
|
|
const RINGTONE_DIALING = 'https://assets.mixkit.co/active_storage/sfx/2864/2864-64-bit.mp3' // 嘟嘟声
|
|
|
|
|
|
2025-12-03 15:33:48 +08:00
|
|
|
export function useWebRTC(userId: string, onIncomingCall?: (senderUserId: string) => void) {
|
|
|
|
|
const toastStore = useToastStore()
|
2025-12-03 15:58:57 +08:00
|
|
|
const chatStore = useChatStore()
|
2025-12-03 15:33:48 +08:00
|
|
|
|
2025-12-03 13:25:02 +08:00
|
|
|
const call = reactive<CallState>({
|
|
|
|
|
active: false,
|
|
|
|
|
minimized: false,
|
|
|
|
|
type: 'video',
|
|
|
|
|
status: 'idle',
|
|
|
|
|
statusText: '',
|
|
|
|
|
id: null,
|
|
|
|
|
muted: false,
|
|
|
|
|
camOff: false,
|
2025-12-03 15:58:57 +08:00
|
|
|
remoteCamOff: false,
|
2025-12-03 15:33:48 +08:00
|
|
|
duration: 0,
|
|
|
|
|
startTime: null,
|
2025-12-03 13:25:02 +08:00
|
|
|
})
|
|
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
const isCaller = ref(false)
|
2025-12-03 15:33:48 +08:00
|
|
|
const localStream = shallowRef<MediaStream | null>(null)
|
|
|
|
|
const remoteStream = shallowRef<MediaStream | null>(null)
|
|
|
|
|
let durationTimer: number | null = null
|
2025-12-03 13:25:02 +08:00
|
|
|
let pc: RTCPeerConnection | null = null
|
2025-12-03 15:33:48 +08:00
|
|
|
const pendingCandidates: RTCIceCandidate[] = []
|
|
|
|
|
let currentReceiverUserId = ''
|
2025-12-03 13:25:02 +08:00
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
// 音频对象
|
|
|
|
|
const audioIncoming = new Audio(RINGTONE_INCOMING)
|
|
|
|
|
const audioDialing = new Audio(RINGTONE_DIALING)
|
|
|
|
|
audioIncoming.loop = true
|
|
|
|
|
audioDialing.loop = true
|
|
|
|
|
|
|
|
|
|
// --- 铃声控制 ---
|
|
|
|
|
function playRingtone(type: 'incoming' | 'dialing') {
|
|
|
|
|
stopRingtone() // 先停止当前的
|
|
|
|
|
const audio = type === 'incoming' ? audioIncoming : audioDialing
|
|
|
|
|
audio.currentTime = 0
|
|
|
|
|
audio.play().catch(e => console.warn('Autoplay prevented:', e))
|
|
|
|
|
}
|
2025-12-03 15:33:48 +08:00
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
function stopRingtone() {
|
|
|
|
|
audioIncoming.pause()
|
|
|
|
|
audioDialing.pause()
|
|
|
|
|
audioIncoming.currentTime = 0
|
|
|
|
|
audioDialing.currentTime = 0
|
|
|
|
|
}
|
2025-12-03 15:33:48 +08:00
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
// --- 系统通知 ---
|
|
|
|
|
function sendSystemNotification(title: string, body: string) {
|
|
|
|
|
if (!('Notification' in window)) return
|
|
|
|
|
|
|
|
|
|
if (Notification.permission === 'granted') {
|
|
|
|
|
new Notification(title, { body, icon: '/favicon.ico' })
|
|
|
|
|
} else if (Notification.permission !== 'denied') {
|
|
|
|
|
Notification.requestPermission().then(permission => {
|
|
|
|
|
if (permission === 'granted') {
|
|
|
|
|
new Notification(title, { body, icon: '/favicon.ico' })
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-12-03 13:25:02 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
// --- 辅助函数 ---
|
|
|
|
|
function getRoomId(uid1: string, uid2: string): string {
|
|
|
|
|
return [uid1, uid2].sort().join('_')
|
2025-12-03 13:25:02 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
function formatTextDuration(seconds: number): string {
|
|
|
|
|
if (seconds <= 0) return '0秒'
|
|
|
|
|
const h = Math.floor(seconds / 3600)
|
|
|
|
|
const m = Math.floor((seconds % 3600) / 60)
|
|
|
|
|
const s = seconds % 60
|
|
|
|
|
let str = ''
|
|
|
|
|
if (h > 0) str += `${h}小时`
|
|
|
|
|
if (m > 0) str += `${m}分钟`
|
|
|
|
|
if (s > 0) str += `${s}秒`
|
|
|
|
|
return str
|
|
|
|
|
}
|
2025-12-03 13:25:02 +08:00
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
async function sendSummaryMessage(reason: 'connected' | 'cancelled' | 'rejected' | 'busy') {
|
|
|
|
|
if (!isCaller.value || !currentReceiverUserId) return
|
|
|
|
|
let content = ''
|
|
|
|
|
const durationStr = formatTextDuration(call.duration)
|
|
|
|
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
|
|
|
|
const deviceText = isMobile ? '移动端' : '电脑端'
|
2025-12-03 13:25:02 +08:00
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
if (reason === 'connected') content = `通话结束,时长:${durationStr}`
|
|
|
|
|
else if (reason === 'cancelled') content = '已取消呼叫'
|
|
|
|
|
else if (reason === 'rejected') content = '对方拒绝接听'
|
|
|
|
|
else if (reason === 'busy') content = '对方忙'
|
2025-12-03 15:33:48 +08:00
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
content += ` [${deviceText}]`
|
2025-12-03 13:25:02 +08:00
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
const roomId = getRoomId(userId, currentReceiverUserId)
|
|
|
|
|
const clientId = wsManager.getClientId()
|
|
|
|
|
const payload = {
|
|
|
|
|
sender_client_id: clientId || '',
|
|
|
|
|
receiver_user_id: currentReceiverUserId,
|
|
|
|
|
room_id: roomId,
|
|
|
|
|
message_type: 0,
|
|
|
|
|
content: content,
|
|
|
|
|
duration: 0,
|
|
|
|
|
extra: JSON.stringify({ isSystem: true }),
|
2025-12-03 13:25:02 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
try {
|
|
|
|
|
await messageApi.sendMessage(payload)
|
|
|
|
|
const localMsg: any = {
|
|
|
|
|
...payload, id: Date.now(), sender_user_id: userId, created_at: new Date().toISOString(), isSelf: true, extra: { isSystem: true }
|
2025-12-03 13:25:02 +08:00
|
|
|
}
|
2025-12-03 15:58:57 +08:00
|
|
|
chatStore.addMessage(roomId, localMsg)
|
|
|
|
|
chatStore.updateContactLastMsg(currentReceiverUserId, content, Date.now())
|
|
|
|
|
} catch (e) { console.error(e) }
|
|
|
|
|
}
|
2025-12-03 13:25:02 +08:00
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
// --- WebRTC 基础 ---
|
|
|
|
|
async function initMedia(videoEnabled: boolean): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
if (localStream.value) localStream.value.getTracks().forEach(t => t.stop())
|
|
|
|
|
const constraints: MediaStreamConstraints = {
|
|
|
|
|
video: videoEnabled ? { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: 'user' } : false,
|
|
|
|
|
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
|
2025-12-03 15:33:48 +08:00
|
|
|
}
|
2025-12-03 15:58:57 +08:00
|
|
|
const stream = await navigator.mediaDevices.getUserMedia(constraints)
|
|
|
|
|
localStream.value = stream
|
|
|
|
|
} catch (error) { throw new Error('无法获取设备权限') }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createPC(): Promise<void> {
|
|
|
|
|
const servers = [{ urls: 'stun:stun.l.google.com:19302' }]
|
|
|
|
|
if (pc) pc.close()
|
|
|
|
|
pc = new RTCPeerConnection({ iceServers: servers })
|
|
|
|
|
if (localStream.value) localStream.value.getTracks().forEach((track) => pc!.addTrack(track, localStream.value!))
|
|
|
|
|
pc.ontrack = (e) => { if (e.streams && e.streams[0]) remoteStream.value = e.streams[0] }
|
|
|
|
|
pc.onicecandidate = (e) => { if (e.candidate && call.id) sendSignal('candidate', e.candidate) }
|
2025-12-03 15:33:48 +08:00
|
|
|
}
|
2025-12-03 13:25:02 +08:00
|
|
|
|
|
|
|
|
function sendSignal(status: CallStatus, data?: any, receiverUserId?: string) {
|
|
|
|
|
if (!call.id) return
|
2025-12-03 15:33:48 +08:00
|
|
|
const targetUserId = receiverUserId || currentReceiverUserId
|
|
|
|
|
if (!targetUserId) return
|
2025-12-03 13:25:02 +08:00
|
|
|
const payload = {
|
|
|
|
|
sender_client_id: wsManager.getClientId() || '',
|
2025-12-03 15:33:48 +08:00
|
|
|
receiver_user_id: targetUserId,
|
2025-12-03 13:25:02 +08:00
|
|
|
room_id: '',
|
2025-12-03 15:58:57 +08:00
|
|
|
message_type: 6,
|
2025-12-03 13:25:02 +08:00
|
|
|
content: JSON.stringify(data || {}),
|
|
|
|
|
call_id: call.id,
|
|
|
|
|
call_status: status,
|
|
|
|
|
extra: JSON.stringify({ type: call.type }),
|
|
|
|
|
}
|
2025-12-03 15:58:57 +08:00
|
|
|
messageApi.sendMessage(payload).catch(console.error)
|
|
|
|
|
}
|
2025-12-03 13:25:02 +08:00
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
// --- 摄像头开关同步信令 ---
|
|
|
|
|
function sendSyncState(type: 'cam-toggle', value: boolean) {
|
|
|
|
|
if (call.status !== 'connected') return
|
|
|
|
|
const payload = {
|
|
|
|
|
sender_client_id: wsManager.getClientId() || '',
|
|
|
|
|
receiver_user_id: currentReceiverUserId,
|
|
|
|
|
room_id: '',
|
|
|
|
|
message_type: 6, // 借用信令通道
|
|
|
|
|
content: JSON.stringify({ action: type, value }),
|
|
|
|
|
call_id: call.id,
|
|
|
|
|
call_status: 'sync_state' as any, // 自定义状态
|
|
|
|
|
extra: JSON.stringify({ type: call.type })
|
|
|
|
|
}
|
2025-12-03 13:25:02 +08:00
|
|
|
messageApi.sendMessage(payload).catch(console.error)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
// --- 计时器 ---
|
2025-12-03 15:33:48 +08:00
|
|
|
function startCallTimer() {
|
|
|
|
|
stopCallTimer()
|
|
|
|
|
call.startTime = Date.now()
|
|
|
|
|
call.duration = 0
|
|
|
|
|
durationTimer = window.setInterval(() => {
|
|
|
|
|
if (call.startTime) call.duration = Math.floor((Date.now() - call.startTime) / 1000)
|
|
|
|
|
}, 1000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stopCallTimer() {
|
2025-12-03 15:58:57 +08:00
|
|
|
if (durationTimer) { clearInterval(durationTimer); durationTimer = null }
|
2025-12-03 15:33:48 +08:00
|
|
|
call.startTime = null
|
|
|
|
|
call.duration = 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDuration(seconds: number): string {
|
|
|
|
|
const m = Math.floor(seconds / 60).toString().padStart(2, '0')
|
|
|
|
|
const s = (seconds % 60).toString().padStart(2, '0')
|
2025-12-03 15:58:57 +08:00
|
|
|
const h = Math.floor(seconds / 3600)
|
|
|
|
|
return h > 0 ? `${h.toString().padStart(2, '0')}:${m}:${s}` : `${m}:${s}`
|
2025-12-03 15:33:48 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
// --- 操作 ---
|
2025-12-03 13:25:02 +08:00
|
|
|
async function startCall(type: 'audio' | 'video', receiverUserId: string) {
|
2025-12-03 15:58:57 +08:00
|
|
|
isCaller.value = true
|
2025-12-03 13:25:02 +08:00
|
|
|
currentReceiverUserId = receiverUserId
|
|
|
|
|
call.type = type
|
|
|
|
|
call.id = Date.now().toString()
|
|
|
|
|
call.active = true
|
|
|
|
|
call.minimized = false
|
|
|
|
|
call.status = 'outgoing'
|
2025-12-03 15:33:48 +08:00
|
|
|
call.statusText = '正在呼叫...'
|
|
|
|
|
call.duration = 0
|
2025-12-03 15:58:57 +08:00
|
|
|
call.camOff = false
|
|
|
|
|
call.remoteCamOff = false
|
|
|
|
|
remoteStream.value = null
|
2025-12-03 13:25:02 +08:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await initMedia(type === 'video')
|
|
|
|
|
await createPC()
|
2025-12-03 15:58:57 +08:00
|
|
|
playRingtone('dialing') // 播放拨号音
|
2025-12-03 13:25:02 +08:00
|
|
|
sendSignal('invite', undefined, receiverUserId)
|
|
|
|
|
} catch (error: any) {
|
2025-12-03 15:33:48 +08:00
|
|
|
toastStore.error(error.message || '无法启动通话')
|
2025-12-03 15:58:57 +08:00
|
|
|
closeCall()
|
2025-12-03 13:25:02 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function acceptCall(senderUserId?: string) {
|
2025-12-03 15:58:57 +08:00
|
|
|
isCaller.value = false
|
|
|
|
|
stopRingtone() // 停止来电铃声
|
2025-12-03 15:33:48 +08:00
|
|
|
if (senderUserId) currentReceiverUserId = senderUserId
|
2025-12-03 13:25:02 +08:00
|
|
|
call.status = 'connected'
|
2025-12-03 15:33:48 +08:00
|
|
|
call.statusText = '正在连接...'
|
2025-12-03 15:58:57 +08:00
|
|
|
call.camOff = false
|
|
|
|
|
call.remoteCamOff = false
|
2025-12-03 13:25:02 +08:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await initMedia(call.type === 'video')
|
|
|
|
|
await createPC()
|
2025-12-03 15:33:48 +08:00
|
|
|
sendSignal('accepted', undefined, currentReceiverUserId)
|
2025-12-03 13:25:02 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
endCall()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function endCall() {
|
2025-12-03 15:58:57 +08:00
|
|
|
stopRingtone()
|
|
|
|
|
if (isCaller.value) {
|
|
|
|
|
if (call.status === 'connected') sendSummaryMessage('connected')
|
|
|
|
|
else if (call.status === 'outgoing') sendSummaryMessage('cancelled')
|
|
|
|
|
}
|
2025-12-03 13:25:02 +08:00
|
|
|
sendSignal('hangup')
|
|
|
|
|
closeCall()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeCall() {
|
2025-12-03 15:58:57 +08:00
|
|
|
stopRingtone()
|
2025-12-03 13:25:02 +08:00
|
|
|
call.active = false
|
|
|
|
|
call.status = 'idle'
|
|
|
|
|
call.statusText = ''
|
|
|
|
|
call.id = null
|
2025-12-03 15:33:48 +08:00
|
|
|
stopCallTimer()
|
2025-12-03 15:58:57 +08:00
|
|
|
if (pc) { pc.close(); pc = null }
|
|
|
|
|
if (localStream.value) { localStream.value.getTracks().forEach(t => t.stop()); localStream.value = null }
|
2025-12-03 15:33:48 +08:00
|
|
|
remoteStream.value = null
|
2025-12-03 13:25:02 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleSignaling(message: ChatMessage) {
|
2025-12-03 15:58:57 +08:00
|
|
|
const signal = message.call_status as any
|
2025-12-03 15:33:48 +08:00
|
|
|
const content = message.content ? JSON.parse(message.content) : {}
|
|
|
|
|
const extra = message.extra ? (typeof message.extra === 'string' ? JSON.parse(message.extra) : message.extra) : {}
|
2025-12-03 13:25:02 +08:00
|
|
|
|
2025-12-03 15:33:48 +08:00
|
|
|
if (extra.type) call.type = extra.type
|
2025-12-03 13:25:02 +08:00
|
|
|
|
2025-12-03 15:58:57 +08:00
|
|
|
// 处理自定义状态同步信令
|
|
|
|
|
if (signal === 'sync_state') {
|
|
|
|
|
if (content.action === 'cam-toggle') {
|
|
|
|
|
call.remoteCamOff = content.value
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 13:25:02 +08:00
|
|
|
if (signal === 'invite') {
|
2025-12-03 15:58:57 +08:00
|
|
|
isCaller.value = false
|
2025-12-03 13:25:02 +08:00
|
|
|
currentReceiverUserId = message.sender_user_id
|
2025-12-03 15:33:48 +08:00
|
|
|
call.id = message.call_id
|
2025-12-03 13:25:02 +08:00
|
|
|
call.active = true
|
|
|
|
|
call.minimized = false
|
|
|
|
|
call.status = 'incoming'
|
2025-12-03 15:33:48 +08:00
|
|
|
call.statusText = `邀请你进行${call.type === 'video' ? '视频' : '语音'}通话`
|
2025-12-03 15:58:57 +08:00
|
|
|
call.remoteCamOff = false
|
|
|
|
|
|
|
|
|
|
playRingtone('incoming') // 播放来电铃声
|
|
|
|
|
sendSystemNotification('新来电', `收到来自 ${message.sender_user_id} 的${call.type === 'video' ? '视频' : '语音'}通话邀请`) // 触发系统通知
|
|
|
|
|
|
2025-12-03 15:33:48 +08:00
|
|
|
if (onIncomingCall) onIncomingCall(message.sender_user_id)
|
|
|
|
|
|
2025-12-03 13:25:02 +08:00
|
|
|
} else if (signal === 'accepted') {
|
2025-12-03 15:58:57 +08:00
|
|
|
stopRingtone() // 对方接听,停止拨号音
|
2025-12-03 13:25:02 +08:00
|
|
|
call.status = 'connected'
|
|
|
|
|
call.statusText = '通话中'
|
2025-12-03 15:33:48 +08:00
|
|
|
startCallTimer()
|
2025-12-03 13:25:02 +08:00
|
|
|
const offer = await pc!.createOffer()
|
|
|
|
|
await pc!.setLocalDescription(offer)
|
2025-12-03 15:33:48 +08:00
|
|
|
sendSignal('offer', offer)
|
|
|
|
|
|
2025-12-03 13:25:02 +08:00
|
|
|
} else if (signal === 'offer') {
|
2025-12-03 15:58:57 +08:00
|
|
|
stopRingtone() // 建立连接,停止可能存在的铃声
|
2025-12-03 13:25:02 +08:00
|
|
|
if (!pc) {
|
|
|
|
|
await initMedia(call.type === 'video')
|
|
|
|
|
await createPC()
|
|
|
|
|
}
|
2025-12-03 15:33:48 +08:00
|
|
|
await pc!.setRemoteDescription(content)
|
|
|
|
|
processPendingCandidates()
|
2025-12-03 13:25:02 +08:00
|
|
|
const answer = await pc!.createAnswer()
|
|
|
|
|
await pc!.setLocalDescription(answer)
|
|
|
|
|
sendSignal('answer', answer, message.sender_user_id)
|
|
|
|
|
call.status = 'connected'
|
|
|
|
|
call.statusText = '通话中'
|
2025-12-03 15:33:48 +08:00
|
|
|
startCallTimer()
|
|
|
|
|
|
2025-12-03 13:25:02 +08:00
|
|
|
} else if (signal === 'answer') {
|
2025-12-03 15:33:48 +08:00
|
|
|
await pc!.setRemoteDescription(content)
|
|
|
|
|
processPendingCandidates()
|
|
|
|
|
|
2025-12-03 13:25:02 +08:00
|
|
|
} else if (signal === 'candidate') {
|
2025-12-03 15:58:57 +08:00
|
|
|
if (pc && pc.remoteDescription) await pc.addIceCandidate(content)
|
|
|
|
|
else pendingCandidates.push(content)
|
2025-12-03 15:33:48 +08:00
|
|
|
|
2025-12-03 13:25:02 +08:00
|
|
|
} else if (['hangup', 'ended', 'answered_elsewhere'].includes(signal)) {
|
2025-12-03 15:58:57 +08:00
|
|
|
if (isCaller.value) {
|
|
|
|
|
if (call.status === 'outgoing') sendSummaryMessage('rejected')
|
|
|
|
|
else if (call.status === 'connected') sendSummaryMessage('connected')
|
|
|
|
|
}
|
2025-12-03 13:25:02 +08:00
|
|
|
closeCall()
|
2025-12-03 15:33:48 +08:00
|
|
|
if (signal === 'answered_elsewhere') toastStore.info('已在其他设备接听')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function processPendingCandidates() {
|
|
|
|
|
while (pendingCandidates.length > 0) {
|
2025-12-03 15:58:57 +08:00
|
|
|
const c = pendingCandidates.shift()
|
|
|
|
|
if (c && pc) await pc.addIceCandidate(c)
|
2025-12-03 13:25:02 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleMute() {
|
|
|
|
|
call.muted = !call.muted
|
2025-12-03 15:58:57 +08:00
|
|
|
if (localStream.value) localStream.value.getAudioTracks().forEach(t => t.enabled = !call.muted)
|
2025-12-03 13:25:02 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleCamera() {
|
|
|
|
|
call.camOff = !call.camOff
|
2025-12-03 15:58:57 +08:00
|
|
|
// 同步状态给对方
|
|
|
|
|
sendSyncState('cam-toggle', call.camOff)
|
|
|
|
|
if (localStream.value) localStream.value.getVideoTracks().forEach(t => t.enabled = !call.camOff)
|
2025-12-03 13:25:02 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
2025-12-03 15:58:57 +08:00
|
|
|
call, localStream, remoteStream, startCall, acceptCall, endCall, handleSignaling, toggleMute, toggleCamera, formatDuration, sendSystemNotification
|
2025-12-03 13:25:02 +08:00
|
|
|
}
|
|
|
|
|
}
|