| | |
| | | import { ref, computed, onBeforeUnmount } from 'vue' |
| | | |
| | | /** ✅ 你的 WS 地址前缀(后面拼 userId) */ |
| | | const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId=' |
| | | // const WS_BASE = 'ws://218.202.104.82:38201/ws/chat' |
| | | const WS_BASE = 'ws://220.177.172.27:8100/webrtc/ws/chat' |
| | | |
| | | const uid = ref('2') |
| | | const peerUid = ref('3') |
| | | const uid = ref('2021474568497131521') |
| | | const peerUid = ref('2021474815063486465') |
| | | |
| | | const connected = ref(false) |
| | | const state = ref('idle') // idle | calling | ringing | in-call |
| | |
| | | const s = String(seconds.value % 60).padStart(2, '0') |
| | | return `${m}:${s}` |
| | | }) |
| | | |
| | | function startTimer() { |
| | | stopTimer() |
| | | seconds.value = 0 |
| | | timer = setInterval(() => seconds.value++, 1000) |
| | | } |
| | | |
| | | function stopTimer() { |
| | | if (timer) clearInterval(timer) |
| | | timer = null |
| | |
| | | |
| | | // 心跳 |
| | | let pingTimer = null |
| | | |
| | | function startPing() { |
| | | stopPing() |
| | | // 每 25 秒发一次 ping(你后端有 PING/PONG) |
| | |
| | | if (connected.value) send('ping', 'system', null) |
| | | }, 25000) |
| | | } |
| | | |
| | | function stopPing() { |
| | | if (pingTimer) clearInterval(pingTimer) |
| | | pingTimer = null |
| | |
| | | |
| | | function createPC() { |
| | | pc = new RTCPeerConnection({ |
| | | iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], |
| | | iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, |
| | | { urls: 'stun:stun.l.google.com:19302' }, |
| | | { urls: 'stun:stun1.l.google.com:19302' }, |
| | | { urls: 'stun:stun.stunprotocol.org:3478' }, |
| | | // 添加多个公共TURN服务器以提高跨网络连接成功率 |
| | | { urls: ['turn:freeturn.net:80', 'turn:freeturn.net:443'], username: 'free', credential: 'free' }, |
| | | // 备用TURN服务器 |
| | | { urls: 'turn:turn.cloudlifter.io:443', username: 'webrtc', credential: 'webrtc' }], |
| | | }) |
| | | |
| | | pc.onicecandidate = (e) => { |
| | |
| | | } |
| | | } |
| | | |
| | | // 步骤7 接收音频流并且播放 |
| | | pc.ontrack = (e) => { |
| | | log('✅ 收到远端音频流') |
| | | const stream = e.streams[0] |
| | | if (remoteAudio.value) { |
| | | remoteAudio.value.srcObject = stream |
| | | remoteAudio.value.play().catch(() => { |
| | | }) |
| | | remoteAudio.value.play().catch(() => {}) |
| | | } |
| | | } |
| | | |
| | |
| | | pc.getSenders().map(s => (s.track ? s.track.id : null)).filter(Boolean) |
| | | ) |
| | | |
| | | // 步骤6 发送音频流 |
| | | stream.getTracks().forEach(track => { |
| | | if (!existingTrackIds.has(track.id)) { |
| | | pc.addTrack(track, stream) |
| | |
| | | return stream |
| | | } |
| | | |
| | | // 步骤5 |
| | | async function flushCandidates() { |
| | | if (!pc || !pc.remoteDescription) return |
| | | while (pendingCandidates.length) { |
| | |
| | | return |
| | | } |
| | | |
| | | // 步骤2 |
| | | if (t === 'offer') { |
| | | // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”) |
| | | offeredByPeer = true |
| | |
| | | return |
| | | } |
| | | |
| | | // 步骤3 |
| | | if (t === 'answer') { |
| | | log('✅ 收到 answer,通话建立') |
| | | if (!pc) createPC() |
| | | await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp)) |
| | | // 步骤4 |
| | | await flushCandidates() |
| | | |
| | | state.value = 'in-call' |
| | |
| | | function connectWS() { |
| | | if (ws) ws.close() |
| | | |
| | | ws = new WebSocket(WS_BASE + encodeURIComponent(uid.value)) |
| | | ws = new WebSocket(WS_BASE, "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoyMDIxNDc0NTY4NDk3MTMxNTIxLCJ1c2VyX2tleSI6Ijc5MDE3ZGEwLTZlZjItNGU0MS05MzdiLWFiZjM4NzRkNTYwMSIsInVzZXJuYW1lIjoieW9uZ2h1MSJ9.hEoBlIejSOyn44aA1FZrtlVHsrbmA84KX_5Qq2Mz7slO4jNlbJkFvzuomT7EqI9QigucNzULDjfiA9IHPfww8Q") |
| | | |
| | | ws.onopen = () => { |
| | | connected.value = true |
| | |
| | | // ✅ 不会重复 addTrack |
| | | await ensureLocalTracksAdded() |
| | | |
| | | // 步骤1 |
| | | const offer = await pc.createOffer() |
| | | await pc.setLocalDescription(offer) |
| | | |
| | |
| | | log('➡️ 已发送 offer 给', peerUid.value) |
| | | } |
| | | |
| | | // 步骤2 |
| | | async function answerOffer(offerSdp, from) { |
| | | if (!offerSdp) { |
| | | log('❌ offer sdp 为空,无法接听') |
| | |
| | | function cleanup(sendToPeer) { |
| | | try { |
| | | if (sendToPeer) send('hangup', peerUid.value, null) |
| | | } catch { |
| | | } |
| | | } catch {} |
| | | |
| | | stopTimer() |
| | | |
| | |
| | | if (ws) ws.close() |
| | | }) |
| | | </script> |
| | | |