吉安感知网项目-前端
罗广辉
2026-01-13 9f1828431a15fc96d8bc5ee230429030bdbbc469
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
@@ -45,13 +45,14 @@
  
     <audio ref="remoteAudio" autoplay class="hidden-audio"></audio>
   </div>
  </template>
  <script setup>
import { ref, computed, onBeforeUnmount, onMounted } from 'vue'
  /** ✅ 你的 WS 地址前缀(后面拼 userId) */
  const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId='
</template>
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
/** ✅ 你的 WS 地址前缀(后面拼 userId) */
const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId='
  // 解析URL参数
  const parseUrlParams = () => {
@@ -73,38 +74,38 @@
  const uid = ref('3')
  const peerUid = ref('2')
  const connected = ref(false)
  const state = ref('idle') // idle | calling | ringing | in-call
  const logText = ref('')
  const remoteAudio = ref(null)
   /**
    * 根据状态返回对应颜色
    */
   const getStateColor = (state) => {
      const colors = {
         'idle': '#666',
         'calling': '#2196F3',
         'ringing': '#FF9800',
         'in-call': '#4CAF50'
      }
      return colors[state] || '#666'
const connected = ref(false)
const state = ref('idle') // idle | calling | ringing | in-call
const logText = ref('')
const remoteAudio = ref(null)
/**
 * 根据状态返回对应颜色
 */
const getStateColor = (state) => {
   const colors = {
      'idle': '#666',
      'calling': '#2196F3',
      'ringing': '#FF9800',
      'in-call': '#4CAF50'
   }
  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(() => {
   return colors[state] || '#666'
}
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}`
@@ -162,122 +163,123 @@
  
  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
      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
      // 收到来电:弹接听按钮
      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
      // 主叫收到 accept:开始 offer
      log('✅ 对方接听 accept,开始建立连接(offer)')
      await startOffer()
      return
   }
   if (t === 'busy') {
     log('❌ 对方忙线/拒绝')
     cleanup(false)
     return
      log('❌ 对方忙线/拒绝')
      cleanup(false)
      return
   }
  
   if (t === 'offer') {
@@ -473,12 +475,14 @@
   lastOfferSdp = null
  
   state.value = 'idle'
  }
  onMounted(() => {
    // 解析URL参数
    parseUrlParams()
  })
}
onMounted(() => {
   const receiveParameters = route.query.contact
   console.log('接收参数', receiveParameters)
   // 解析URL参数
   parseUrlParams()
})
  onBeforeUnmount(() => {
    cleanup(false)