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