From 9fb4de853fc8aab2000f312ba2b8907e3dc9a61d Mon Sep 17 00:00:00 2001
From: 张含笑 <zhx18749296735@163.com>
Date: Thu, 29 Jan 2026 17:33:29 +0800
Subject: [PATCH] feat:调整
---
applications/drone-command/src/views/audioCall.vue | 608 +++++++++++++++++++++++++++---------------------------
1 files changed, 307 insertions(+), 301 deletions(-)
diff --git a/applications/drone-command/src/views/audioCall.vue b/applications/drone-command/src/views/audioCall.vue
index 55de074..9957564 100644
--- a/applications/drone-command/src/views/audioCall.vue
+++ b/applications/drone-command/src/views/audioCall.vue
@@ -1,439 +1,445 @@
<template>
<div style="padding:16px">
- <h2>1v1 语音通话 WebRTC Demo(对齐后端 ChatMessage,JS)</h2>
-
- <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
- <span>我的 userId:</span>
- <input v-model="uid" style="width:120px" />
-
- <span>对方 userId:</span>
- <input v-model="peerUid" style="width:120px" />
-
- <button @click="connectWS" :disabled="connected">连接WS</button>
-
- <button @click="requestCall" :disabled="!connected || state!=='idle'">呼叫</button>
-
- <button v-if="state==='ringing'" @click="acceptCall">接听</button>
- <button v-if="state==='ringing'" @click="rejectCall">拒绝</button>
-
- <button @click="hangup" :disabled="!connected || (state!=='calling' && state!=='in-call')">挂断</button>
-
- <strong>状态:{{ state }}</strong>
- <strong v-if="state==='in-call'">通话时长:{{ durationText }}</strong>
- </div>
-
- <div style="margin-top:8px;color:#666">
- <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
- </div>
-
- <pre style="margin-top:12px;max-height:280px;overflow:auto;">{{ logText }}</pre>
-
- <audio ref="remoteAudio" autoplay></audio>
+ <h2>1v1 语音通话 WebRTC Demo(对齐后端 ChatMessage,JS)</h2>
+
+ <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
+ <span>我的 userId:</span>
+ <input v-model="uid" style="width:120px" />
+
+ <span>对方 userId:</span>
+ <input v-model="peerUid" style="width:120px" />
+
+ <button @click="connectWS" :disabled="connected">连接WS</button>
+
+ <button @click="requestCall" :disabled="!connected || state!=='idle'">呼叫</button>
+
+ <button v-if="state==='ringing'" @click="acceptCall">接听</button>
+ <button v-if="state==='ringing'" @click="rejectCall">拒绝</button>
+
+ <button @click="hangup" :disabled="!connected || (state!=='calling' && state!=='in-call')">挂断</button>
+
+ <strong>状态:{{ state }}</strong>
+ <strong v-if="state==='in-call'">通话时长:{{ durationText }}</strong>
+ </div>
+
+ <div style="margin-top:8px;color:#666">
+ <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
+ </div>
+
+ <pre style="margin-top:12px;max-height:280px;overflow:auto;">{{ logText }}</pre>
+
+ <audio ref="remoteAudio" autoplay></audio>
</div>
- </template>
-
- <script setup>
- import { ref, computed, onBeforeUnmount } from 'vue'
-
- /** ✅ 你的 WS 地址前缀(后面拼 userId) */
- const WS_BASE = 'wss://域名/ws/chat?userId='
-
- const uid = ref('2')
- const peerUid = ref('3')
-
- const connected = ref(false)
- const state = ref('idle') // idle | calling | ringing | in-call
- const logText = ref('')
- const remoteAudio = ref(null)
-
- let ws = null
- let pc = null
- let localStream = null
- const pendingCandidates = []
-
- // 来电暂存
- let incomingFrom = null
- let acceptedByMe = false // 我是否已经点了接听
- let offeredByPeer = false // 是否已收到对方 offer(用于流程判断)
-
- // 计时
- let timer = null
- const seconds = ref(0)
- const durationText = computed(() => {
+</template>
+
+<script setup>
+import { ref, computed, onBeforeUnmount } from 'vue'
+
+/** ✅ 你的 WS 地址前缀(后面拼 userId) */
+const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId='
+
+const uid = ref('2')
+const peerUid = ref('3')
+
+const connected = ref(false)
+const state = ref('idle') // idle | calling | ringing | in-call
+const logText = ref('')
+const remoteAudio = ref(null)
+
+let ws = null
+let pc = null
+let localStream = null
+const pendingCandidates = []
+
+// 来电暂存
+let incomingFrom = null
+let acceptedByMe = false // 我是否已经点了接听
+let offeredByPeer = false // 是否已收到对方 offer(用于流程判断)
+
+// 计时
+let timer = null
+const seconds = ref(0)
+const durationText = computed(() => {
const m = String(Math.floor(seconds.value / 60)).padStart(2, '0')
const s = String(seconds.value % 60).padStart(2, '0')
return `${m}:${s}`
- })
- function startTimer() {
+})
+
+function startTimer() {
stopTimer()
seconds.value = 0
timer = setInterval(() => seconds.value++, 1000)
- }
- function stopTimer() {
+}
+
+function stopTimer() {
if (timer) clearInterval(timer)
timer = null
- }
-
- // 心跳
- let pingTimer = null
- function startPing() {
+}
+
+// 心跳
+let pingTimer = null
+
+function startPing() {
stopPing()
// 每 25 秒发一次 ping(你后端有 PING/PONG)
pingTimer = setInterval(() => {
- if (connected.value) send('ping', 'system', null)
+ if (connected.value) send('ping', 'system', null)
}, 25000)
- }
- function stopPing() {
+}
+
+function stopPing() {
if (pingTimer) clearInterval(pingTimer)
pingTimer = null
- }
-
- function log(...args) {
+}
+
+function log(...args) {
const line = args.map(v => (typeof v === 'string' ? v : JSON.stringify(v))).join(' ')
logText.value += `[${new Date().toLocaleTimeString()}] ${line}\n`
console.log(...args)
- }
-
- /**
- * ✅ 严格按后端 ChatMessage 字段发:
- * { type, from, to, payload, timestamp }
- * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
- */
- function send(type, to, payload) {
+}
+
+/**
+ * ✅ 严格按后端 ChatMessage 字段发:
+ * { type, from, to, payload, timestamp }
+ * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
+ */
+function send(type, to, payload) {
if (!type) return
if (!ws || ws.readyState !== WebSocket.OPEN) return
-
+
const msg = {
- type, // "call/accept/busy/offer/answer/candidate/hangup/ping"
- from: String(uid.value),
- to: String(to),
- payload: payload ?? null,
- timestamp: Date.now(),
+ type, // "call/accept/busy/offer/answer/candidate/hangup/ping"
+ from: String(uid.value),
+ to: String(to),
+ payload: payload ?? null,
+ timestamp: Date.now(),
}
ws.send(JSON.stringify(msg))
- }
-
- /* ---------------- WebRTC ---------------- */
-
- function createPC() {
+}
+
+/* ---------------- WebRTC ---------------- */
+
+function createPC() {
pc = new RTCPeerConnection({
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
})
-
+
pc.onicecandidate = (e) => {
- if (e.candidate) {
- send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate })
- }
+ if (e.candidate) {
+ send('candidate', peerUid.value, { candidate: e.candidate.toJSON ? e.candidate.toJSON() : e.candidate })
+ }
}
-
+
pc.ontrack = (e) => {
- log('✅ 收到远端音频流')
- const stream = e.streams[0]
- if (remoteAudio.value) {
- remoteAudio.value.srcObject = stream
- remoteAudio.value.play().catch(() => {})
- }
+ log('✅ 收到远端音频流')
+ const stream = e.streams[0]
+ if (remoteAudio.value) {
+ remoteAudio.value.srcObject = stream
+ remoteAudio.value.play().catch(() => {
+ })
+ }
}
-
+
pc.onconnectionstatechange = () => {
- log('pc.connectionState =', pc.connectionState)
+ log('pc.connectionState =', pc.connectionState)
}
- }
-
- async function getLocalAudio() {
+}
+
+async function getLocalAudio() {
if (localStream) return localStream
-
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
- log('❌ 当前环境不支持 getUserMedia:请使用 https 或 localhost 打开页面')
- throw new Error('getUserMedia not available')
+ log('❌ 当前环境不支持 getUserMedia:请使用 https 或 localhost 打开页面')
+ throw new Error('getUserMedia not available')
}
-
+
try {
- localStream = await navigator.mediaDevices.getUserMedia({ audio: true })
- log('✅ 已获取本地麦克风')
- return localStream
+ localStream = await navigator.mediaDevices.getUserMedia({ audio: true })
+ log('✅ 已获取本地麦克风')
+ return localStream
} catch (e) {
- log('❌ 获取麦克风失败:', String(e))
- throw e
+ log('❌ 获取麦克风失败:', String(e))
+ throw e
}
- }
-
- /** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */
- async function ensureLocalTracksAdded() {
+}
+
+/** ✅ 关键:只 addTrack 一次,避免 “sender already exists” */
+async function ensureLocalTracksAdded() {
if (!pc) createPC()
const stream = await getLocalAudio()
-
+
const existingTrackIds = new Set(
- pc.getSenders().map(s => (s.track ? s.track.id : null)).filter(Boolean)
+ pc.getSenders().map(s => (s.track ? s.track.id : null)).filter(Boolean)
)
-
+
stream.getTracks().forEach(track => {
- if (!existingTrackIds.has(track.id)) {
- pc.addTrack(track, stream)
- log('addTrack ok:', track.id)
- } else {
- // log('skip addTrack:', track.id)
- }
+ if (!existingTrackIds.has(track.id)) {
+ pc.addTrack(track, stream)
+ log('addTrack ok:', track.id)
+ } else {
+ // log('skip addTrack:', track.id)
+ }
})
-
+
return stream
- }
-
- async function flushCandidates() {
+}
+
+async function flushCandidates() {
if (!pc || !pc.remoteDescription) return
while (pendingCandidates.length) {
- const c = pendingCandidates.shift()
- try {
- await pc.addIceCandidate(new RTCIceCandidate(c))
- } catch (e) {
- log('addIceCandidate failed:', String(e))
- }
+ const c = pendingCandidates.shift()
+ try {
+ await pc.addIceCandidate(new RTCIceCandidate(c))
+ } catch (e) {
+ log('addIceCandidate failed:', String(e))
+ }
}
- }
-
- /* ---------------- 信令处理 ---------------- */
-
- async function onSignal(msg) {
+}
+
+/* ---------------- 信令处理 ---------------- */
+
+async function onSignal(msg) {
if (!msg) return
const t = (msg.type || '').toString()
-
+
// ✅ 忽略后端回显给自己的消息(你的后端会回显 sender)
if (msg.from && String(msg.from) === String(uid.value)) {
- return
+ return
}
-
+
// system 消息:login/pong/busy 仅记录
if (msg.from === 'system') {
- log('system msg:', msg)
- return
- }
-
- if (!t) return
-
- if (t === 'call') {
- // 收到来电:弹接听按钮
- incomingFrom = String(msg.from)
- peerUid.value = incomingFrom
- acceptedByMe = false
- offeredByPeer = false
-
- state.value = 'ringing'
- log('📞 收到来电 call,来自', incomingFrom)
- return
- }
-
- if (t === 'accept') {
- // 主叫收到 accept:开始 offer
- log('✅ 对方接听 accept,开始建立连接(offer)')
- await startOffer()
- return
- }
-
- if (t === 'busy') {
- log('❌ 对方忙线/拒绝')
- cleanup(false)
- return
- }
-
- if (t === 'offer') {
- // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
- offeredByPeer = true
- incomingFrom = String(msg.from)
- peerUid.value = incomingFrom
-
- if (!acceptedByMe) {
- state.value = 'ringing'
- log('📩 收到 offer(但未接听),已等待用户点击接听')
- // 暂存 offer 到一个变量
- lastOfferSdp = msg.payload?.sdp
+ log('system msg:', msg)
return
- }
-
- // 已接听:直接 answer
- await answerOffer(msg.payload?.sdp, incomingFrom)
- return
}
-
+
+ if (!t) return
+
+ if (t === 'call') {
+ // 收到来电:弹接听按钮
+ incomingFrom = String(msg.from)
+ peerUid.value = incomingFrom
+ acceptedByMe = false
+ offeredByPeer = false
+
+ state.value = 'ringing'
+ log('📞 收到来电 call,来自', incomingFrom)
+ return
+ }
+
+ if (t === 'accept') {
+ // 主叫收到 accept:开始 offer
+ log('✅ 对方接听 accept,开始建立连接(offer)')
+ await startOffer()
+ return
+ }
+
+ if (t === 'busy') {
+ log('❌ 对方忙线/拒绝')
+ cleanup(false)
+ return
+ }
+
+ if (t === 'offer') {
+ // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
+ offeredByPeer = true
+ incomingFrom = String(msg.from)
+ peerUid.value = incomingFrom
+
+ if (!acceptedByMe) {
+ state.value = 'ringing'
+ log('📩 收到 offer(但未接听),已等待用户点击接听')
+ // 暂存 offer 到一个变量
+ lastOfferSdp = msg.payload?.sdp
+ return
+ }
+
+ // 已接听:直接 answer
+ await answerOffer(msg.payload?.sdp, incomingFrom)
+ return
+ }
+
if (t === 'answer') {
- log('✅ 收到 answer,通话建立')
- if (!pc) createPC()
- await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
- await flushCandidates()
-
- state.value = 'in-call'
- startTimer()
- return
+ log('✅ 收到 answer,通话建立')
+ if (!pc) createPC()
+ await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
+ await flushCandidates()
+
+ state.value = 'in-call'
+ startTimer()
+ return
}
-
+
if (t === 'candidate') {
- const c = msg.payload?.candidate
- if (!c) return
- if (pc && pc.remoteDescription) {
- await pc.addIceCandidate(new RTCIceCandidate(c))
- } else {
- pendingCandidates.push(c)
- }
- return
+ const c = msg.payload?.candidate
+ if (!c) return
+ if (pc && pc.remoteDescription) {
+ await pc.addIceCandidate(new RTCIceCandidate(c))
+ } else {
+ pendingCandidates.push(c)
+ }
+ return
}
-
+
if (t === 'hangup') {
- log('📴 对方挂断')
- cleanup(false)
- return
+ log('📴 对方挂断')
+ cleanup(false)
+ return
}
-
+
if (t === 'pong') {
- // 心跳响应
- // log('pong')
- return
+ // 心跳响应
+ // log('pong')
+ return
}
- }
-
- // 暂存 offer(用于“先点接听再 answer”)
- let lastOfferSdp = null
-
- /* ---------------- 业务操作 ---------------- */
-
- function connectWS() {
+}
+
+// 暂存 offer(用于“先点接听再 answer”)
+let lastOfferSdp = null
+
+/* ---------------- 业务操作 ---------------- */
+
+function connectWS() {
if (ws) ws.close()
-
+
ws = new WebSocket(WS_BASE + encodeURIComponent(uid.value))
-
+
ws.onopen = () => {
- connected.value = true
- log('WS 已连接 userId=', uid.value)
- startPing()
+ connected.value = true
+ log('WS 已连接 userId=', uid.value)
+ startPing()
}
-
+
ws.onclose = () => {
- connected.value = false
- log('WS 已关闭')
- stopPing()
+ connected.value = false
+ log('WS 已关闭')
+ stopPing()
}
-
+
ws.onerror = (e) => log('WS error', e)
-
+
ws.onmessage = (e) => {
- const msg = JSON.parse(e.data)
- log('收到信令', msg)
- onSignal(msg)
+ const msg = JSON.parse(e.data)
+ log('收到信令', msg)
+ onSignal(msg)
}
- }
-
- function requestCall() {
+}
+
+function requestCall() {
state.value = 'calling'
incomingFrom = null
acceptedByMe = false
offeredByPeer = false
lastOfferSdp = null
-
+
send('call', peerUid.value, null)
log('➡️ 发起呼叫 call 给', peerUid.value)
- }
-
- function acceptCall() {
+}
+
+function acceptCall() {
if (!incomingFrom) return
acceptedByMe = true
-
+
// 先告诉对方我接听了(后端会把双方置忙)
send('accept', incomingFrom, null)
log('✅ 已点击接听,发送 accept 给', incomingFrom)
-
+
state.value = 'calling'
-
+
// 如果 offer 已经提前到达(我们暂存了),立刻 answer
if (lastOfferSdp) {
- answerOffer(lastOfferSdp, incomingFrom)
- lastOfferSdp = null
+ answerOffer(lastOfferSdp, incomingFrom)
+ lastOfferSdp = null
}
- }
-
- function rejectCall() {
+}
+
+function rejectCall() {
if (!incomingFrom) return
-
+
// 你后端没有 reject,用 busy 表示拒绝/不可接听
send('busy', incomingFrom, null)
log('❌ 已拒绝来电,发送 busy 给', incomingFrom)
-
+
incomingFrom = null
acceptedByMe = false
offeredByPeer = false
lastOfferSdp = null
state.value = 'idle'
- }
-
- async function startOffer() {
+}
+
+async function startOffer() {
if (!pc) createPC()
-
+
// ✅ 不会重复 addTrack
await ensureLocalTracksAdded()
-
+
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
-
+
send('offer', peerUid.value, { sdp: pc.localDescription })
log('➡️ 已发送 offer 给', peerUid.value)
- }
-
- async function answerOffer(offerSdp, from) {
+}
+
+async function answerOffer(offerSdp, from) {
if (!offerSdp) {
- log('❌ offer sdp 为空,无法接听')
- return
+ log('❌ offer sdp 为空,无法接听')
+ return
}
if (!pc) createPC()
-
+
await pc.setRemoteDescription(new RTCSessionDescription(offerSdp))
await flushCandidates()
-
+
await ensureLocalTracksAdded()
-
+
const answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
-
+
send('answer', from, { sdp: pc.localDescription })
log('⬅️ 已发送 answer 给', from)
-
+
state.value = 'in-call'
startTimer()
- }
-
- function hangup() {
+}
+
+function hangup() {
cleanup(true)
- }
-
- /* ---------------- 清理 ---------------- */
-
- function cleanup(sendToPeer) {
+}
+
+/* ---------------- 清理 ---------------- */
+
+function cleanup(sendToPeer) {
try {
- if (sendToPeer) send('hangup', peerUid.value, null)
- } catch {}
-
+ if (sendToPeer) send('hangup', peerUid.value, null)
+ } catch {
+ }
+
stopTimer()
-
+
if (pc) {
- pc.getSenders().forEach(s => s.track && s.track.stop())
- pc.close()
+ pc.getSenders().forEach(s => s.track && s.track.stop())
+ pc.close()
}
if (localStream) {
- localStream.getTracks().forEach(t => t.stop())
+ localStream.getTracks().forEach(t => t.stop())
}
-
+
pc = null
localStream = null
pendingCandidates.length = 0
-
+
incomingFrom = null
acceptedByMe = false
offeredByPeer = false
lastOfferSdp = null
-
+
state.value = 'idle'
- }
-
- onBeforeUnmount(() => {
+}
+
+onBeforeUnmount(() => {
cleanup(false)
stopPing()
if (ws) ws.close()
- })
- </script>
+})
+</script>
\ No newline at end of file
--
Gitblit v1.9.3