You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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候选已交换,但对方视频无法显示,当前输出截图:
present output


问题排查与修复方案

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 20:07:02