| | |
| | | <template> |
| | | <div class="call-container" :class="{'incoming-call': state === 'ringing'} "> |
| | | <div class="content-wrapper"> |
| | | <!-- <div class="input-group"> |
| | | <div class="input-item"> |
| | | <span class="input-label">我的 userId:</span> |
| | | <input v-model="uid" class="input-field" /> |
| | | </div> |
| | | |
| | | <div class="input-item"> |
| | | <span class="input-label">对方 userId:</span> |
| | | <input v-model="peerUid" class="input-field" /> |
| | | </div> |
| | | </div>--> |
| | | |
| | | <div class="button-group" v-if="false"> |
| | | <button @click="connectWS" :disabled="connected" class="btn btn-connect">连接WS</button> |
| | | </div> |
| | |
| | | <div class="contactAvatar"> |
| | | <img :src="defaultAvatar" alt=""></img> |
| | | </div> |
| | | |
| | | <!-- 联系人名称--> |
| | | <div class="contactName"> |
| | | {{ contactName }} |
| | | </div> |
| | | |
| | | <div class="status-group"> |
| | | <strong v-if="state==='ringing'" class="status-text incoming-status">来电中...</strong> |
| | | <strong v-if="state==='calling'" class="status-text calling-status">呼叫中...</strong> |
| | |
| | | |
| | | |
| | | </div> |
| | | |
| | | <!-- <div class="tip-message"> |
| | | <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small> |
| | | </div>--> |
| | | |
| | | <!-- <div class="log-container"> |
| | | <div class="log-box"> |
| | | <div class="log-title">日志信息:</div> |
| | | <pre class="log-content">{{ logText }}</pre> |
| | | </div> |
| | | </div>--> |
| | | |
| | | <audio ref="remoteAudio" autoplay class="hidden-audio"></audio> |
| | | </div> |
| | | </template> |
| | |
| | | |
| | | state.value = 'ringing'; |
| | | log('📞 收到来电 call,来自', incomingFrom); |
| | | |
| | | // 开始来电提示(铃声和震动) |
| | | playRingtone(); |
| | | startVibration(); |
| | | |
| | | } |
| | | } catch (e) { |
| | | log('❌ 解析voiceparams参数失败:', String(e)); |
| | |
| | | } |
| | | |
| | | // 2. 同时保留原有的URL参数解析逻辑,作为备选 |
| | | const url = new URL(window.location.href); |
| | | const paramsStr = url.searchParams.get('params'); |
| | | if (paramsStr) { |
| | | try { |
| | | const params = JSON.parse(decodeURIComponent(paramsStr)); |
| | | if (params.peerUid) { |
| | | peerUid.value = String(params.peerUid); |
| | | log('🔗 从URL参数获取peerUid:', params.peerUid); |
| | | } |
| | | } catch (e) { |
| | | log('❌ 解析URL参数失败:', String(e)); |
| | | } |
| | | } |
| | | // const url = new URL(window.location.href); |
| | | // const paramsStr = url.searchParams.get('params'); |
| | | // if (paramsStr) { |
| | | // try { |
| | | // const params = JSON.parse(decodeURIComponent(paramsStr)); |
| | | // if (params.peerUid) { |
| | | // peerUid.value = String(params.peerUid); |
| | | // log('🔗 从URL参数获取peerUid:', params.peerUid); |
| | | // } |
| | | // } catch (e) { |
| | | // log('❌ 解析URL参数失败:', String(e)); |
| | | // } |
| | | // } |
| | | } |
| | | |
| | | // 从URL参数或本地存储获取用户ID |
| | | const getUserIdFromStorage = () => { |
| | | // 从url获取用户id |
| | | const contact = JSON.parse(decodeURIComponent(receiveParameters.value)).userId |
| | | console.log('从url获取用户id',contact) |
| | | // 从本地获取用户id |
| | | const userId = localStorage.getItem('userId') || sessionStorage.getItem('userId') |
| | | console.log('从本地存储获取userId:', userId) |
| | | if (userId) { |
| | | log('🔗 从本地存储获取userId:', userId) |
| | | return userId |
| | | } |
| | | return '3' // 默认值 |
| | |
| | | |
| | | const uid = ref('3') |
| | | const peerUid = ref('2') |
| | | // 联系人名称 |
| | | const contactName = ref('张三') |
| | | |
| | | const connected = ref(false) |
| | | const state = ref('idle') // idle | calling | ringing | in-call |
| | |
| | | const callEnded = ref(false) |
| | | const endMessage = ref('对方已挂断') |
| | | const endTimer = ref(null) |
| | | |
| | | /** |
| | | * 根据状态返回对应颜色 |
| | | */ |
| | | const getStateColor = (state) => { |
| | | const colors = { |
| | | 'idle': '#666', |
| | | 'calling': '#2196F3', |
| | | 'ringing': '#FF9800', |
| | | 'in-call': '#4CAF50' |
| | | } |
| | | return colors[state] || '#666' |
| | | } |
| | | |
| | | let ws = null |
| | | let pc = null |
| | |
| | | }, 25000) |
| | | } |
| | | |
| | | // 来电提示相关 |
| | | let ringtoneAudio = null |
| | | let vibrationTimer = null |
| | | |
| | | // 播放来电铃声 |
| | | function playRingtone() { |
| | | // 创建音频对象(使用默认的系统提示音) |
| | | ringtoneAudio = new Audio('data:audio/wav;base64,UklGRigAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQAAAAAAA=='); |
| | | ringtoneAudio.loop = true; |
| | | ringtoneAudio.volume = 0.5; |
| | | |
| | | try { |
| | | ringtoneAudio.play(); |
| | | log('🔊 开始播放来电铃声') |
| | | } catch (e) { |
| | | log('❌ 播放来电铃声失败:', String(e)) |
| | | } |
| | | } |
| | | |
| | | // 停止来电铃声 |
| | | function stopRingtone() { |
| | | if (ringtoneAudio) { |
| | | try { |
| | | ringtoneAudio.pause(); |
| | | ringtoneAudio.currentTime = 0; |
| | | ringtoneAudio = null; |
| | | log('🔇 停止播放来电铃声') |
| | | } catch (e) { |
| | | log('❌ 停止来电铃声失败:', String(e)) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 开始震动 |
| | | function startVibration() { |
| | | // 检查浏览器是否支持震动 API |
| | | if ('vibrate' in navigator) { |
| | | // 震动模式:震动1秒,暂停0.5秒,重复 |
| | | const vibrationPattern = [1000, 500]; |
| | | |
| | | try { |
| | | navigator.vibrate(vibrationPattern); |
| | | vibrationTimer = setInterval(() => { |
| | | navigator.vibrate(vibrationPattern); |
| | | }, 1500); // 每1.5秒重复一次震动模式 |
| | | log('📳 开始震动提示') |
| | | } catch (e) { |
| | | log('❌ 震动失败:', String(e)) |
| | | } |
| | | } else { |
| | | log('ℹ️ 当前浏览器不支持震动功能') |
| | | } |
| | | } |
| | | |
| | | // 停止震动 |
| | | function stopVibration() { |
| | | if (vibrationTimer) { |
| | | clearInterval(vibrationTimer); |
| | | vibrationTimer = null; |
| | | } |
| | | |
| | | if ('vibrate' in navigator) { |
| | | try { |
| | | navigator.vibrate(0); // 停止震动 |
| | | log('📳 停止震动提示') |
| | | } catch (e) { |
| | | log('❌ 停止震动失败:', String(e)) |
| | | } |
| | | } |
| | | } |
| | | function stopPing() { |
| | | if (pingTimer) clearInterval(pingTimer) |
| | | pingTimer = null |
| | |
| | | send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate }) |
| | | } |
| | | } |
| | | |
| | | // 步骤七:建立连接 |
| | | pc.ontrack = (e) => { |
| | | log('✅ 收到远端音频流') |
| | | const stream = e.streams[0] |
| | |
| | | log('pc.connectionState =', pc.connectionState) |
| | | } |
| | | } |
| | | |
| | | // 步骤六:连接音频 |
| | | async function getLocalAudio() { |
| | | if (localStream) return localStream |
| | | |
| | |
| | | throw e |
| | | } |
| | | } |
| | | |
| | | // 步骤5 确保本地音频流已添加 |
| | | /** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */ |
| | | async function ensureLocalTracksAdded() { |
| | | if (!pc) createPC() |
| | |
| | | |
| | | return stream |
| | | } |
| | | |
| | | // 步骤4 发送本地音频流 |
| | | async function flushCandidates() { |
| | | if (!pc || !pc.remoteDescription) return |
| | | while (pendingCandidates.length) { |
| | |
| | | offeredByPeer = false |
| | | |
| | | state.value = 'ringing' |
| | | log('📞 收到来电 call,来自', incomingFrom) |
| | | |
| | | // 开始来电提示(铃声和震动) |
| | | playRingtone() |
| | | startVibration() |
| | | |
| | | log('📞 收到来电 call,来自', incomingFrom) |
| | | return |
| | | } |
| | | |
| | |
| | | lastOfferSdp = msg.payload?.sdp |
| | | return |
| | | } |
| | | |
| | | // 步骤2 |
| | | // 已接听:直接 answer |
| | | await answerOffer(msg.payload?.sdp, incomingFrom) |
| | | return |
| | | } |
| | | |
| | | // 步骤3 接收 answer |
| | | if (t === 'answer') { |
| | | log('✅ 收到 answer,通话建立') |
| | | if (!pc) createPC() |
| | |
| | | onSignal(msg) |
| | | } |
| | | } |
| | | |
| | | // 发送呼叫信息 |
| | | function requestCall() { |
| | | state.value = 'calling' |
| | | incomingFrom = null |
| | |
| | | send('call', peerUid.value, null) |
| | | log('➡️ 发起呼叫 call 给', peerUid.value) |
| | | } |
| | | |
| | | // 接收信息 |
| | | function acceptCall() { |
| | | if (!incomingFrom) return |
| | | acceptedByMe = true |
| | | |
| | | // 停止来电提示 |
| | | stopRingtone() |
| | | stopVibration() |
| | | |
| | | // 先告诉对方我接听了(后端会把双方置忙) |
| | | send('accept', incomingFrom, null) |
| | |
| | | |
| | | function rejectCall() { |
| | | if (!incomingFrom) return |
| | | |
| | | // 停止来电提示 |
| | | stopRingtone() |
| | | stopVibration() |
| | | |
| | | // 你后端没有 reject,用 busy 表示拒绝/不可接听 |
| | | send('busy', incomingFrom, null) |
| | |
| | | |
| | | // ✅ 不会重复 addTrack |
| | | await ensureLocalTracksAdded() |
| | | |
| | | // 步骤1:创建 offer |
| | | const offer = await pc.createOffer() |
| | | await pc.setLocalDescription(offer) |
| | | |
| | | send('offer', peerUid.value, { sdp: pc.localDescription }) |
| | | log('➡️ 已发送 offer 给', peerUid.value) |
| | | } |
| | | |
| | | // 步骤2:等待 answer |
| | | async function answerOffer(offerSdp, from) { |
| | | if (!offerSdp) { |
| | | log('❌ offer sdp 为空,无法接听') |
| | |
| | | } catch {} |
| | | |
| | | stopTimer() |
| | | // 停止来电提示 |
| | | stopRingtone() |
| | | stopVibration() |
| | | |
| | | if (pc) { |
| | | pc.getSenders().forEach(s => s.track && s.track.stop()) |
| | |
| | | callEnded.value = true |
| | | endMessage.value = '对方已挂断' |
| | | |
| | | // 延迟10秒后跳转页面 |
| | | // 延迟5秒后跳转页面 |
| | | endTimer.value = setTimeout(() => { |
| | | log('⏰ 通话结束10秒后自动返回') |
| | | // 返回上一页(uni-app的语音通话列表页面) |
| | | if (window.uni) { |
| | | window.uni.navigateBack() |
| | |
| | | |
| | | onMounted(() => { |
| | | console.log('收拾收拾',route.query.voiceparams); |
| | | console.log('接收参数', receiveParameters.value) |
| | | console.log('接收参数', JSON.parse(decodeURIComponent(receiveParameters.value))) |
| | | |
| | | // 解析并使用contact参数 |
| | | if (receiveParameters.value) { |
| | |
| | | const contact = JSON.parse(decodeURIComponent(receiveParameters.value)) |
| | | if (contact.friendId) { |
| | | peerUid.value = contact.friendId |
| | | contactName.value = contact.friendNickName || '未知联系人' |
| | | log('🔗 从contact参数获取peerUid:', contact.friendId) |
| | | log('👤 联系人名称:', contact.friendNickName || '未知联系人') |
| | | log('👤 联系人名称:', contactName.value) |
| | | } |
| | | } catch (e) { |
| | | log('❌ 解析contact参数失败:', String(e)) |
| | |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | /* 联系人名称样式 */ |
| | | .contactName { |
| | | font-size: 18px; |
| | | font-weight: bold; |
| | | color: #333; |
| | | text-align: center; |
| | | margin-top: 10px; |
| | | margin-bottom: 40px; |
| | | |
| | | } |
| | | |
| | | /* 来电状态样式 */ |
| | | .status-text.incoming-status { |
| | | font-size: 24px; |
| | |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | margin-bottom: 40px; |
| | | border-radius: 50%; |
| | | background-color: #f0f0f0; |
| | | overflow: hidden; |