ICE候选交换后WebRTC Peer Track无法显示问题排查
问题描述
使用Agora RTM作为信令服务器开发P2P视频聊天应用,控制台显示ICE候选已成功交换,但对方的视频轨道无法正常显示,推测问题出在轨道处理环节。
代码实现
HTML代码
<html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>P2P Chat</title> <style> #voip{ display:grid; grid-template-columns: 1fr 1fr; gap:1em; } #p1,#p2{ background-color:black; width:100%; } </style> </head> <body> <div id="voip"> <video id="p1" autoplay muted playsinline></video> <video id="p2" autoplay controls muted playsinline></video> </div> <script src="agora-rtm-sdk-1.4.4.js"></script> <script src="server.js"></script> </body> </html>
server.js代码
'use strict'; try { let sid = String(Math.floor(Math.random() * 1e5)), ls, rs, pc, ws, room, pid, poff, m1, ice; alert(sid); const config = { offerToReceiveAudio: true, offerToReceiveVideo: true }, ng = navigator, id = x => document.getElementById(x), pp = id('p2'), i = [ 'Connection state changed:', 'Logged in...', 'Joined channel', 'Failed to join channel', 'Failed to login: ', 'Message received: ', 'Your Offer: ', 'Your Answer: ' ], io = (x, y, z = '') => console.log(i[parseInt(x)], y, 'Reason:', z); let init = async () => { console.log("Initializing user media..."); await ng.mediaDevices.getUserMedia({ audio: false, video: true }).then(async (s) => { console.log("User media initialized"); ls = s; id('p1').srcObject = s; await off(); }).catch(e => alert(`getUserMedia error: ${e.message}`)); }; let off = async () => { pc = new RTCPeerConnection({ iceServers: [ { urls: ['stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302'] } ] }); pc.addEventListener('connectionstatechange', e => { console.log('Connection state change:', pc.connectionState); if (pc.connectionState === 'connected') { console.log('Peer connected!'); } else if (pc.connectionState === 'failed') { console.error('Connection failed. Investigate the ICE candidates and network setup.'); } }); pc.onicecandidate = async (e) => { if (e.candidate) { console.log('New ICE candidate:', e.candidate); ice = e.candidate; } else { console.log('ICE candidate gathering complete'); } }; ls.getTracks().forEach(t => { console.log('Adding local track:', t); pc.addTrack(t, ls); }); pc.ontrack = tks => { console.log('Received remote track:', tks); if (!rs) { rs = new MediaStream(); pp.srcObject = rs; } tks.streams[0].getTracks().forEach(tk => { console.log('Adding track to remote stream:', tk); rs.addTrack(tk); }); }; poff = await pc.createOffer(config); console.log('Created offer:', poff); await pc.setLocalDescription(poff); ws = AgoraRTM.createInstance('84a1a61490d642ceb93c9c1ebd796dbf'); ws.on('ConnectionStateChanged', (s, r) => io(0, s, r)); await ws.login({ token: null, uid: sid }).then(async () => { io(1, id); room = ws.createChannel('main'); await room.join().then(() => { console.log('Joined channel'); room.on('MemberJoined', async (pi) => { pid = pi; console.log('Member joined, exchanging ICE candidates...'); await ws.sendMessageToPeer({ text: JSON.stringify({ 'type': 'offer', 'offer': poff, 'pid': sid }) }, pid) .then(async () => { console.log('Offer sent'); await ws.sendMessageToPeer({ text: JSON.stringify({ 'type': 'ice', 'ice': ice }) }, pid) .then(() => console.log('ICE Candidates exchanged')) .catch(e => console.error('ICE Candidates exchange failed!', e)); }).catch(e => console.error('Offer exchange failed!', e)); }); ws.on('MessageFromPeer', async (m, ii) => { m1 = JSON.parse(m.text); let mo = m1.offer || m1.answer || m1.ice; io(5, mo, ' LATEST '); if ('ice' === m1.type) { console.log('Received ICE candidate:', mo); await pc.addIceCandidate(new RTCIceCandidate(mo)).then(() => { console.log('ICE candidate successfully added.'); }).catch(error => { console.error('Error adding ICE candidate:', error); }); } if ('offer' === m1.type) { console.log('Received offer:', mo); await pc.setRemoteDescription(mo); let ans = await pc.createAnswer(config); io(7, ans, ii); await pc.setLocalDescription(ans); await ws.sendMessageToPeer({ text: JSON.stringify({ 'type': 'answer', 'answer': ans, 'pid': ii }) }, m1.pid); } else if ('answer' === m1.type) { console.log('Received answer:', mo); await pc.setRemoteDescription(new RTCSessionDescription(mo)); } }); }).catch(e => io(3, e)); }).catch(e => io(4, id)); }; init(); } catch (e0) { alert(e0); }
当前状态
控制台显示ICE候选已交换,但对方视频无法显示,当前输出截图:
问题排查与修复方案
1. ICE候选交换不完整
当前代码仅保存并发送了最后一个ICE候选,导致大部分关键候选未传递给对方,这是连接建立不完整的核心原因。
修复方式:
收集所有ICE候选,要么实时发送,要么等收集完成后批量发送:
// 替换原onicecandidate逻辑 let iceCandidates = []; pc.onicecandidate = async (e) => { if (e.candidate) { console.log('New ICE candidate:', e.candidate); iceCandidates.push(e.candidate); // 实时发送给已连接的对等方 if (pid) { await ws.sendMessageToPeer({ text: JSON.stringify({ 'type': 'ice', 'ice': e.candidate }) }, pid) .catch(e => console.error('ICE candidate send failed!', e)); } } else { console.log('ICE candidate gathering complete'); // 可选:收集完成后批量发送 if (pid) { await ws.sendMessageToPeer({ text: JSON.stringify({ 'type': 'ice-batch', 'iceList': iceCandidates }) }, pid) .catch(e => console.error('ICE batch send failed!', e)); } } };
同时在接收逻辑中添加批量处理:
// 在MessageFromPeer的ice判断后添加 if ('ice-batch' === m1.type) { console.log('Received ICE candidate batch:', m1.iceList); for (let candidate of m1.iceList) { await pc.addIceCandidate(new RTCIceCandidate(candidate)).then(() => { console.log('ICE candidate successfully added.'); }).catch(error => { console.error('Error adding ICE candidate:', error); }); } }
2. 轨道接收逻辑优化
原手动创建MediaStream并添加轨道的方式易出绑定问题,简化为直接使用接收到的流:
pc.ontrack = tks => { console.log('Received remote track:', tks); // 直接将接收到的流赋值给视频元素 pp.srcObject = tks.streams[0]; };
3. 信令发送时机修正
原代码发送offer后立即发送ICE候选,此时候选可能未收集完成。建议在ICE候选收集完成后再发送,或确保发送时已有可用候选。
4. 补充ICE连接状态监听
添加更精准的ICE连接状态日志:
pc.addEventListener('iceconnectionstatechange', () => { console.log('ICE connection state:', pc.iceConnectionState); if (pc.iceConnectionState === 'connected') { console.log('ICE连接已成功建立'); } });
测试验证
修复后重新运行,检查控制台:
- 确认所有ICE候选都已发送和接收;
- 查看
iceConnectionState是否变为connected; - 确认
ontrack事件触发并正确赋值流到视频元素。
内容的提问来源于stack exchange,提问作者HEMANT ABHIRAM SOMARAJU SCJC I
相关产品推荐
相关产品推荐

