WebRTC 1对1视频聊天系统详解

89 阅读7分钟

在这篇文章中,我们将深入分析一个基于WebRTC技术实现的1对1视频聊天系统。该系统不仅支持实时视频通话,还集成了文本消息传输功能,是一个完整的P2P通信解决方案。 这里我的测试信令服务器和STUN服务器都是本地使用node启动的服务,想要服务器代码的小伙伴可以私聊。

技术架构概述

该系统基于以下核心技术构建:

  • HTML5/CSS3:构建用户界面
  • JavaScript:实现业务逻辑
  • WebRTC:实现点对点音视频通信
  • WebSocket:实现信令服务器通信
  • STUN/TURN:实现NAT穿透

系统功能模块

1. 用户界面设计 系统采用响应式设计,支持在桌面和移动设备上使用。主要界面包含以下区域:

<div class="video-wrapper">
    <video id="local-video" autoplay muted playsinline></video>
    <div class="video-label">本地视频</div>
</div>
<div class="video-wrapper">
    <video id="remote-video" autoplay playsinline></video>
    <div class="video-label">远程视频</div>
</div>

视频区域采用Flex布局,支持自适应屏幕大小。通过playsinline属性确保在移动设备上正常播放。

2. 连接管理模块 连接管理是WebRTC应用的核心,系统实现了完整的连接建立流程:

2.1 信令服务器连接

function initSignaling() {
    try {
        // 使用公共测试信令服务器
        signalingSocket = new WebSocket('ws://localhost:3000');

        signalingSocket.onopen = () => {
            signalingStatus.innerHTML = '<i class="fas fa-server"></i> 信令服务器状态: 已连接';
            addDebugMessage('信令服务器连接成功');
            updateStep(1);

            // 注册客户端
            signalingSocket.send(JSON.stringify({
                type: 'register',
                id: localId
            }));
        };

        signalingSocket.onmessage = (event) => {
            let message;
            if (event.data instanceof Blob) {
                // 处理Blob数据
                event.data.text().then(text => {
                    message = JSON.parse(text);
                    addDebugMessage(`收到信令消息: ${message && message.type}`);
                    handleSignalingMessage(message);
                });
                addDebugMessage('收到二进制格式的信令消息');
            } else {
                message = JSON.parse(event.data);
                addDebugMessage(`收到信令消息: ${message && message.type}`);
                handleSignalingMessage(message);
            }
        };

        signalingSocket.onerror = (error) => {
            addDebugMessage(`信令服务器错误: ${error.message}`);
            signalingStatus.innerHTML = '<i class="fas fa-server"></i> 信令服务器状态: 连接错误';
        };

        signalingSocket.onclose = () => {
            addDebugMessage('信令服务器连接已关闭');
            signalingStatus.innerHTML = '<i class="fas fa-server"></i> 信令服务器状态: 已断开';
        };
    } catch (error) {
        addDebugMessage(`信令连接失败: ${error.message}`);
        signalingStatus.innerHTML = '<i class="fas fa-server"></i> 信令服务器状态: 连接失败';
    }
}

信令服务器用于交换连接信息,包括SDP描述和ICE候选。

2.2 WebRTC连接初始化

// 初始化WebRTC连接
async function initConnection() {
    try {
        addDebugMessage('正在初始化WebRTC连接...');
        // 创建配置 - 使用公共STUN服务器
        const config = {
            iceServers: [
                // 国内可尝试的 (测试用,不稳定!)
                { urls: "stun:localhost:3478" },
                { urls: "stun:stun.voipstunt.com:3478" },
                { urls: "stun:stun.ekiga.net:3478" },
                { urls: "stun:stun.voxgratia.org:3478" },
                { urls: "stun:stun.internetcalls.com:3478" },
                { urls: "stun:stun.voip.aebc.com:3478" },
                { urls: "stun:stun.internetcalls.com:3478" },

                { urls: "stun:stun.miwifi.com:3478" },
                { urls: "stun:stun.qq.com:3478" },
                // 全球知名的 (非国内,延迟可能高)
                { urls: "stun:stun.l.google.com:19302" },
                { urls: "stun:stun1.l.google.com:19302" },
                { urls: "stun:stun2.l.google.com:19302" },
                {
                    urls: "turn:freeturn.net:80",
                    username: "free",
                    credential: "free"
                },
                {
                    urls: "turn:freeturn.tel:80",
                    username: "free",
                    credential: "free"
                },
            ]
        };

        // 创建对等连接
        peerConnection = new RTCPeerConnection(config);

        // 添加本地媒体流到连接
        // if (localStream) {
        //     localStream.getTracks().forEach(track => {
        //         peerConnection.addTrack(track, localStream);
        //     });
        // }
        // 添加本地媒体流到连接(避免重复添加)
        if (localStream) {
            localStream.getTracks().forEach(track => {
                // 检查是否已经存在该轨道的发送器
                const existingSender = peerConnection.getSenders().find(sender => 
                    sender.track && sender.track.kind === track.kind);

                // 如果不存在相同类型的发送器,则添加新轨道
                if (!existingSender) {
                    peerConnection.addTrack(track, localStream);
                    addDebugMessage(`添加媒体轨道: ${track.kind}`);
                }
            });
        }

        // 设置远程流接收
        peerConnection.ontrack = (event) => {
            addDebugMessage('收到远程媒体流');
            if (!remoteStream) {
                remoteStream = new MediaStream();
                remoteVideo.srcObject = remoteStream;
            }
            remoteStream.addTrack(event.track);
        };

        // 设置ICE候选处理
        peerConnection.onicecandidate = (event) => {
            if (event.candidate) {
                signalingSocket.send(JSON.stringify({
                    type: 'candidate',
                    from: localId,
                    to: remoteId,
                    candidate: event.candidate
                }));
                addDebugMessage('已发送ICE候选');
            } else {
                addDebugMessage('ICE候选收集完成');
            }
        };

        // 设置ICE连接状态变化
        peerConnection.oniceconnectionstatechange = () => {
            addDebugMessage(`ICE连接状态: ${peerConnection.iceConnectionState}`);
            if (peerConnection.iceConnectionState === 'connected') {
                updateStep(4);
            }
        };

        // 如果是发起方,创建数据通道
        if (!dataChannel) {
            dataChannel = peerConnection.createDataChannel("messaging", {
                ordered: true
            });
            setupDataChannelEvents();
        }

        // 设置远程数据通道事件
        peerConnection.ondatachannel = (event) => {
            addDebugMessage('收到远程数据通道');
            dataChannel = event.channel;
            setupDataChannelEvents();
        };

        // 创建包含媒体信息的OFFER
        const offer = await peerConnection.createOffer({
            offerToReceiveAudio: true,
            offerToReceiveVideo: true
        });

        await peerConnection.setLocalDescription(offer);
        addDebugMessage('已创建OFFER');

        // 发送OFFER
        signalingSocket.send(JSON.stringify({
            type: 'offer',
            from: localId,
            to: remoteId,
            sdp: offer.sdp
        }));

        addDebugMessage('已发送OFFER');
        updateStep(2);

        // 更新状态
        connectionStatus.textContent = "连接中...";
        connectionStatus.style.color = "#ffcc00";

    } catch (error) {
        addDebugMessage(`初始化连接失败: ${error.message}`);
        connectionStatus.textContent = "连接失败";
        connectionStatus.style.color = "#ff6b6b";
    }
}


3. 音视频流处理

系统支持音视频采集和传输:

3.1 获取本地媒体流

async function startLocalVideo() {
    try {
        const constraints = {
            video: {
                width: { ideal: 1280 },
                height: { ideal: 720 }
            },
            audio: true
        };

        localStream = await navigator.mediaDevices.getUserMedia(constraints);
        localVideo.srcObject = localStream;
        addDebugMessage('本地视频流已启动');

        startVideoBtn.disabled = true;
        stopVideoBtn.disabled = false;

        // 如果已经建立了连接,需要重新协商
        if (peerConnection && peerConnection.connectionState === 'connected') {
            signalingSocket.send(JSON.stringify({
                type: 'mediaStatus',
                from: localId,
                to: remoteId,
                status: 'started'
            }));

            await renegotiateConnection();
        }
    } catch (error) {
        addDebugMessage(`获取本地媒体流失败: ${error.message}`);
        addMessage('系统', `无法访问摄像头或麦克风: ${error.message}`, 'remote');
    }
}

3.2 媒体流重新协商

当媒体状态发生变化时,系统支持动态重新协商:

async function renegotiateConnection() {
    try {
        if (!peerConnection || !localStream) return;

        // 检查并添加本地媒体流到连接(避免重复添加)
        let tracksAdded = false;
        localStream.getTracks().forEach(track => {
            // 检查是否已经存在该轨道的发送器
            const existingSender = peerConnection.getSenders().find(sender => 
                sender.track && sender.track.kind === track.kind);

            // 如果不存在相同类型的发送器,则添加新轨道
            if (!existingSender) {
                peerConnection.addTrack(track, localStream);
                addDebugMessage(`添加媒体轨道: ${track.kind}`);
                tracksAdded = true;
            }
        });

        // 如果添加了新轨道,则触发重新协商
        if (tracksAdded) {
            // 创建新的offer
            const offer = await peerConnection.createOffer({
                offerToReceiveAudio: true,
                offerToReceiveVideo: true
            });

            await peerConnection.setLocalDescription(offer);

            // 发送媒体协商offer
            signalingSocket.send(JSON.stringify({
                type: 'mediaOffer',
                from: localId,
                to: remoteId,
                sdp: offer.sdp
            }));

            addDebugMessage('已发送媒体协商OFFER');
        }
    } catch (error) {
        addDebugMessage(`重新协商失败: ${error.message}`);
    }
}

4. 数据通道通信

除了音视频通信,系统还支持通过RTCDataChannel进行文本消息传输:

// 如果是发起方,创建数据通道
if (!dataChannel) {
    dataChannel = peerConnection.createDataChannel("messaging", {
        ordered: true
    });
    setupDataChannelEvents();
}

// 设置远程数据通道事件
peerConnection.ondatachannel = (event) => {
    addDebugMessage('收到远程数据通道');
    dataChannel = event.channel;
    setupDataChannelEvents();
};
function setupDataChannelEvents() {
    if (!dataChannel) return;

    // ICE状态跟踪
    peerConnection.oniceconnectionstatechange = () => {
        const state = peerConnection.iceConnectionState;
        addDebugMessage(`ICE连接状态变化: ${state}`);

        switch(state) {
            case 'checking':
                addDebugMessage('正在进行ICE连接检查...');
                break;
            case 'connected':
                addDebugMessage('ICE连接已建立');
                updateStep(4);
                // 确保连接状态更新
                connectionStatus.textContent = "已连接";
                connectionStatus.style.color = "#00ff80";
                connectBtn.disabled = true;
                disconnectBtn.disabled = false;
                messageInput.disabled = false;
                sendBtn.disabled = false;
                connectionTypeSpan.textContent = "P2P直连";
                break;
            case 'completed':
                addDebugMessage('ICE连接完成');
                break;
            case 'disconnected':
                addDebugMessage('ICE连接断开,尝试重新连接...');
                // 不立即重连,等待一段时间看是否能自动恢复
                setTimeout(() => {
                    if (peerConnection && peerConnection.iceConnectionState === 'disconnected') {
                        addDebugMessage('ICE连接仍未恢复,尝试重启');
                        // 尝试收集新的候选
                        peerConnection.restartIce();
                    }
                }, 3000);
                break;
            case 'failed':
                addDebugMessage('ICE连接失败,尝试重启...');
                peerConnection.restartIce();
                break;
            case 'closed':
                addDebugMessage('ICE连接已关闭');
                break;
        }
    };

    dataChannel.onopen = () => {
        addDebugMessage('数据通道已打开');
        connectionStatus.textContent = "已连接";
        connectionStatus.style.color = "#00ff80";
        connectBtn.disabled = true;
        disconnectBtn.disabled = false;
        messageInput.disabled = false;
        sendBtn.disabled = false;
        connectionTypeSpan.textContent = "P2P直连";

        // 添加欢迎消息
        addMessage('系统', 'WebRTC点对点连接已建立!现在可以发送消息了', 'remote');
    };

    dataChannel.onclose = () => {
        setTimeout(() => {
            addDebugMessage('尝试重新连接...');
            initConnection();
        }, 2000);
    };

    dataChannel.onerror = (error) => {
        addDebugMessage(`数据通道错误: ${error.message}`);
    };

    dataChannel.onmessage = (event) => {
        addDebugMessage(`收到消息: ${event.data}`);
        messagesReceived++;
        messagesReceivedSpan.textContent = messagesReceived;
        addMessage('远程端', event.data, 'remote');
    };
}

5. ICE连接状态管理

系统实现了完整的ICE连接状态跟踪:

// ICE状态跟踪
peerConnection.oniceconnectionstatechange = () => {
    const state = peerConnection.iceConnectionState;
    addDebugMessage(`ICE连接状态变化: ${state}`);

    switch(state) {
        case 'checking':
            addDebugMessage('正在进行ICE连接检查...');
            break;
        case 'connected':
            addDebugMessage('ICE连接已建立');
            updateStep(4);
            // 确保连接状态更新
            connectionStatus.textContent = "已连接";
            connectionStatus.style.color = "#00ff80";
            connectBtn.disabled = true;
            disconnectBtn.disabled = false;
            messageInput.disabled = false;
            sendBtn.disabled = false;
            connectionTypeSpan.textContent = "P2P直连";
            break;
        case 'completed':
            addDebugMessage('ICE连接完成');
            break;
        case 'disconnected':
            addDebugMessage('ICE连接断开,尝试重新连接...');
            // 不立即重连,等待一段时间看是否能自动恢复
            setTimeout(() => {
                if (peerConnection && peerConnection.iceConnectionState === 'disconnected') {
                    addDebugMessage('ICE连接仍未恢复,尝试重启');
                    // 尝试收集新的候选
                    peerConnection.restartIce();
                }
            }, 3000);
            break;
        case 'failed':
            addDebugMessage('ICE连接失败,尝试重启...');
            peerConnection.restartIce();
            break;
        case 'closed':
            addDebugMessage('ICE连接已关闭');
            break;
    }
};

技术亮点

1. 完整的连接状态跟踪

系统通过可视化步骤指示器展示连接建立过程:

<div class="connection-diagram">
    <div class="connection-steps">
        <div class="step" id="step1">
            <div class="step-number">1</div>
            <div class="step-label">信令连接</div>
        </div>
        <div class="step" id="step2">
            <div class="step-number">2</div>
            <div class="step-label">交换SDP</div>
        </div>
        <div class="step" id="step3">
            <div class="step-number">3</div>
            <div class="step-label">ICE穿透</div>
        </div>
        <div class="step" id="step4">
            <div class="step-number">4</div>
            <div class="step-label">连接建立</div>
        </div>
    </div>
</div>

2. 动态媒体协商

系统支持在连接建立后动态添加或移除媒体轨道,实现灵活的媒体控制。

3. 完善的错误处理和调试机制

通过详细的调试信息输出和错误处理机制,帮助开发者快速定位和解决问题。

完整代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebRTC消息发送系统</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #1a2980, #26d0ce);
            color: #fff;
            min-height: 100vh;
            padding: 20px;
            overflow-x: hidden;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
        
        @media (max-width: 768px) {
            .container {
                grid-template-columns: 1fr;
            }
        }
        
        header {
            text-align: center;
            margin-bottom: 30px;
            padding: 20px;
            background: rgba(0, 0, 0, 0.3);
            border-radius: 15px;
            grid-column: 1 / -1;
        }
        
        h1 {
            font-size: 2.8rem;
            margin-bottom: 10px;
            text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
        }
        
        .subtitle {
            font-size: 1.2rem;
            opacity: 0.9;
            max-width: 800px;
            margin: 0 auto;
        }
        
        .card {
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            border-radius: 15px;
            padding: 25px;
            margin-bottom: 25px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
            border: 1px solid rgba(255, 255, 255, 0.18);
            height: fit-content;
        }
        
        .card-title {
            font-size: 1.5rem;
            margin-bottom: 20px;
            display: flex;
            align-items: center;
            color: #00f2fe;
        }
        
        .card-title i {
            margin-right: 10px;
            font-size: 1.8rem;
        }
        
        .connection-info {
            display: flex;
            flex-direction: column;
            gap: 15px;
            margin-bottom: 20px;
        }
        
        .info-item {
            background: rgba(0, 0, 0, 0.2);
            padding: 12px;
            border-radius: 8px;
            display: flex;
            align-items: center;
        }
        
        .info-item i {
            font-size: 1.2rem;
            margin-right: 10px;
            width: 30px;
            text-align: center;
        }
        
        .controls {
            display: flex;
            flex-direction: column;
            gap: 15px;
            margin-bottom: 20px;
        }
        
        input, select {
            width: 100%;
            padding: 12px 15px;
            border-radius: 8px;
            border: none;
            background: rgba(0, 0, 0, 0.3);
            color: white;
            font-size: 1rem;
        }
        
        input::placeholder {
            color: rgba(255, 255, 255, 0.6);
        }
        
        button {
            background: linear-gradient(to right, #4facfe, #00f2fe);
            color: white;
            border: none;
            padding: 15px 20px;
            font-size: 1.1rem;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.3s ease;
            font-weight: 600;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
        }
        
        button:hover {
            transform: translateY(-3px);
            box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
        }
        
        button:active {
            transform: translateY(1px);
        }
        
        .btn-disconnect {
            background: linear-gradient(to right, #ff416c, #ff4b2b);
        }
        
        .btn-send {
            background: linear-gradient(to right, #00b09b, #96c93d);
            width: 100%;
            margin-top: 10px;
        }
        
        .chat-container {
            display: flex;
            flex-direction: column;
            height: 400px;
        }
        
        .chat-messages {
            flex: 1;
            overflow-y: auto;
            padding: 15px;
            background: rgba(0, 0, 0, 0.2);
            border-radius: 8px;
            margin-bottom: 15px;
            display: flex;
            flex-direction: column;
            gap: 10px;
        }
        
        .message {
            padding: 10px 15px;
            border-radius: 18px;
            max-width: 80%;
            word-wrap: break-word;
            animation: fadeIn 0.3s ease;
        }
        
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }
        
        .message.local {
            background: linear-gradient(135deg, #4facfe, #00f2fe);
            align-self: flex-end;
            border-bottom-right-radius: 5px;
        }
        
        .message.remote {
            background: rgba(255, 255, 255, 0.15);
            align-self: flex-start;
            border-bottom-left-radius: 5px;
        }
        
        .message-info {
            font-size: 0.8rem;
            opacity: 0.7;
            margin-top: 5px;
        }
        
        .message-input {
            display: flex;
            gap: 10px;
        }
        
        .message-input input {
            flex: 1;
        }
        
        .status {
            padding: 15px;
            border-radius: 8px;
            background: rgba(0, 0, 0, 0.3);
            text-align: center;
            font-weight: 500;
            min-height: 54px;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-top: 15px;
        }
        
        .status.connected {
            background: rgba(0, 255, 128, 0.2);
            color: #00ff80;
        }
        
        .status.disconnected {
            background: rgba(255, 0, 0, 0.2);
            color: #ff6b6b;
        }
        
        .instructions {
            line-height: 1.6;
            margin-top: 20px;
        }
        
        .instructions ul {
            padding-left: 25px;
            margin: 15px 0;
        }
        
        .instructions li {
            margin-bottom: 8px;
            font-size: 0.95rem;
        }
        
        .footer {
            text-align: center;
            margin-top: 30px;
            padding: 20px;
            font-size: 0.9rem;
            opacity: 0.8;
            grid-column: 1 / -1;
        }
        
        .code-block {
            background: rgba(0, 0, 0, 0.3);
            padding: 15px;
            border-radius: 8px;
            margin: 15px 0;
            font-family: monospace;
            font-size: 0.9rem;
            overflow-x: auto;
        }
        
        .peer-connection {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 20px;
            margin-bottom: 20px;
        }
        
        .peer {
            background: rgba(0, 0, 0, 0.2);
            padding: 20px;
            border-radius: 50%;
            width: 100px;
            height: 100px;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-direction: column;
            position: relative;
        }
        
        .peer:before {
            content: "";
            position: absolute;
            top: 50%;
            left: 50%;
            width: 120%;
            height: 120%;
            border: 2px dashed rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            transform: translate(-50%, -50%);
            animation: pulse 2s infinite;
        }
        
        @keyframes pulse {
            0% { opacity: 0.5; }
            50% { opacity: 0.2; }
            100% { opacity: 0.5; }
        }
        
        .peer i {
            font-size: 2.5rem;
            margin-bottom: 10px;
        }
        
        .peer-connection-line {
            flex: 1;
            height: 4px;
            background: linear-gradient(to right, #4facfe, #00f2fe);
            position: relative;
            overflow: hidden;
        }
        
        .peer-connection-line:after {
            content: "";
            position: absolute;
            top: 0;
            left: -100%;
            width: 100%;
            height: 100%;
            background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.5), transparent);
            animation: flow 1.5s infinite;
        }
        
        @keyframes flow {
            100% { left: 100%; }
        }
        
        .connection-stats {
            display: flex;
            justify-content: space-around;
            margin-top: 10px;
        }
        
        .stat-item {
            text-align: center;
        }
        
        .stat-value {
            font-weight: bold;
            font-size: 1.2rem;
            color: #00f2fe;
        }
        
        .stat-label {
            font-size: 0.8rem;
            opacity: 0.8;
        }
        
        .debug-console {
            background: rgba(0, 0, 0, 0.3);
            border-radius: 8px;
            padding: 15px;
            margin-top: 20px;
            max-height: 200px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 0.85rem;
        }
        
        .debug-title {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
        }
        
        .debug-message {
            padding: 5px 0;
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        }
        
        .debug-message:last-child {
            border-bottom: none;
        }
        
        .connection-diagram {
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: 20px 0;
        }
        
        .connection-steps {
            display: flex;
            justify-content: space-around;
            width: 100%;
            margin: 15px 0;
        }
        
        .step {
            display: flex;
            flex-direction: column;
            align-items: center;
            text-align: center;
            width: 80px;
        }
        
        .step-number {
            width: 30px;
            height: 30px;
            background: #4facfe;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 8px;
            font-weight: bold;
        }
        
        .step.active .step-number {
            background: #00f2fe;
            box-shadow: 0 0 10px rgba(0, 242, 254, 0.5);
        }
        
        .step-label {
            font-size: 0.8rem;
        }
        
        .troubleshooting {
            background: rgba(255, 100, 100, 0.15);
            border-left: 4px solid #ff6b6b;
            padding: 15px;
            border-radius: 0 8px 8px 0;
            margin-top: 20px;
        }
        
        .troubleshooting h3 {
            color: #ff6b6b;
            margin-bottom: 10px;
        }
        
        .qr-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            margin-top: 15px;
        }
        
        .qr-code {
            width: 120px;
            height: 120px;
            background: #fff;
            padding: 10px;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 10px;
        }
        
        .qr-code span {
            font-size: 0.7rem;
            text-align: center;
            color: #000;
        }
        .video-container {
            display: flex;
            justify-content: space-around;
            margin: 20px 0;
            grid-column: 1 / -1;
            gap: 20px;
        }

        @media (max-width: 768px) {
            .video-container {
                flex-direction: column;
            }
        }

        .video-wrapper {
            position: relative;
            flex: 1;
            max-width: 45%;
        }

        @media (max-width: 768px) {
            .video-wrapper {
                max-width: 100%;
            }
        }

        video {
            width: 100%;
            height: 200px;
            background: #000;
            border-radius: 10px;
            object-fit: cover;
        }

        .video-label {
            position: absolute;
            bottom: 10px;
            left: 10px;
            background: rgba(0, 0, 0, 0.5);
            color: white;
            padding: 5px 10px;
            border-radius: 5px;
            font-size: 0.8rem;
        }

    </style>
</head>
<body>
    <div class="video-container">
        <div class="video-wrapper">
            <video id="local-video" autoplay muted playsinline></video>
            <div class="video-label">本地视频</div>
        </div>
        <div class="video-wrapper">
            <video id="remote-video" autoplay playsinline></video>
            <div class="video-label">远程视频</div>
        </div>
    </div>
    <div class="container">
        <header>
            <h1><i class="fas fa-satellite-dish"></i> WebRTC消息发送系统</h1>
            <p class="subtitle">点对点实时通信实现 - 修复连接问题版本</p>
        </header>
        
        <div class="card">
            <h2 class="card-title"><i class="fas fa-plug"></i> 连接设置</h2>
            
            <div class="peer-connection">
                <div class="peer">
                    <i class="fas fa-user"></i>
                    <div>本机</div>
                </div>
                <div class="peer-connection-line"></div>
                <div class="peer">
                    <i class="fas fa-user-friends"></i>
                    <div>对等端</div>
                </div>
            </div>
            
            <div class="connection-diagram">
                <div class="connection-steps">
                    <div class="step" id="step1">
                        <div class="step-number">1</div>
                        <div class="step-label">信令连接</div>
                    </div>
                    <div class="step" id="step2">
                        <div class="step-number">2</div>
                        <div class="step-label">交换SDP</div>
                    </div>
                    <div class="step" id="step3">
                        <div class="step-number">3</div>
                        <div class="step-label">ICE穿透</div>
                    </div>
                    <div class="step" id="step4">
                        <div class="step-number">4</div>
                        <div class="step-label">连接建立</div>
                    </div>
                </div>
            </div>
            
            <div class="connection-info">
                <div class="info-item">
                    <i class="fas fa-signal"></i>
                    <div>连接状态: <span id="connection-status">未连接</span></div>
                </div>
                <div class="info-item">
                    <i class="fas fa-id-badge"></i>
                    <div>本地ID: <span id="local-id">生成中...</span></div>
                </div>
            </div>
            
            <div class="controls">
                <div class="info-item">
                    <i class="fas fa-user-friends"></i>
                    <input type="text" id="remote-id" placeholder="输入对等端ID">
                </div>
                
                <button id="connect-btn">
                    <i class="fas fa-link"></i> 连接到对等端
                </button>
                
                <button id="start-video-btn">
                    <i class="fas fa-video"></i> 开启视频
                </button>
                
                <button id="stop-video-btn" disabled>
                    <i class="fas fa-video-slash"></i> 关闭视频
                </button>
                
                <button id="disconnect-btn" class="btn-disconnect" disabled>
                    <i class="fas fa-unlink"></i> 断开连接
                </button>
            </div>
            
            <div class="qr-container">
                <div class="qr-code">
                    <span>扫描二维码<br>与同一设备<br>测试连接</span>
                </div>
                <div>在同一设备上打开两个标签页测试</div>
            </div>
            
            <div class="status" id="signaling-status">
                <i class="fas fa-server"></i> 信令服务器状态: 正在连接...
            </div>
            
            <div class="troubleshooting">
                <h3><i class="fas fa-tools"></i> 连接问题排查</h3>
                <ul>
                    <li>确保两个客户端都连接到同一个信令服务器</li>
                    <li>检查防火墙是否允许WebSocket连接</li>
                    <li>尝试使用公共STUN服务器:stun.l.google.com:19302</li>
                    <li>在同一设备打开两个标签页测试连接</li>
                </ul>
            </div>
        </div>
        
        <div class="card">
            <h2 class="card-title"><i class="fas fa-comments"></i> 消息发送</h2>
            
            <div class="chat-container">
                <div class="chat-messages" id="chat-messages">
                    <div class="message remote">
                        <div>欢迎使用WebRTC消息系统!连接对等端后即可开始聊天</div>
                        <div class="message-info">系统消息</div>
                    </div>
                    <div class="message remote">
                        <div>已修复连接问题,现在可以稳定建立P2P连接</div>
                        <div class="message-info">系统消息</div>
                    </div>
                </div>
                
                <div class="message-input">
                    <input type="text" id="message-input" placeholder="输入消息..." disabled>
                    <button id="send-btn" class="btn-send" disabled>
                        <i class="fas fa-paper-plane"></i> 发送
                    </button>
                </div>
            </div>
            
            <div class="connection-stats">
                <div class="stat-item">
                    <div class="stat-value" id="messages-sent">0</div>
                    <div class="stat-label">已发送</div>
                </div>
                <div class="stat-item">
                    <div class="stat-value" id="messages-received">0</div>
                    <div class="stat-label">已接收</div>
                </div>
                <div class="stat-item">
                    <div class="stat-value" id="connection-type">-</div>
                    <div class="stat-label">连接类型</div>
                </div>
            </div>
            
            <div class="debug-console">
                <div class="debug-title">
                    <div>连接调试信息</div>
                    <button id="clear-debug" style="padding: 5px 10px; font-size: 0.8rem;">清除</button>
                </div>
                <div id="debug-output"></div>
            </div>
        </div>
        
        <div class="card">
            <h2 class="card-title"><i class="fas fa-code"></i> WebRTC数据通道</h2>
            
            <div class="instructions">
                <p>WebRTC使用<strong>RTCPeerConnection</strong>建立点对点连接,通过<strong>RTCDataChannel</strong>发送消息:</p>
                
                <div class="code-block">
// 创建对等连接<br>
const peerConnection = new RTCPeerConnection({<br>
&nbsp;&nbsp;iceServers: [<br>
&nbsp;&nbsp;&nbsp;&nbsp;{ urls: "stun:stun.l.google.com:19302" },<br>
&nbsp;&nbsp;&nbsp;&nbsp;{ urls: "stun:stun1.l.google.com:19302" }<br>
&nbsp;&nbsp;]<br>
});<br><br>
// 创建数据通道<br>
const dataChannel = peerConnection.createDataChannel('messaging');<br><br>
// 发送消息<br>
function sendMessage(message) {<br>
&nbsp;&nbsp;if (dataChannel.readyState === 'open') {<br>
&nbsp;&nbsp;&nbsp;&nbsp;dataChannel.send(message);<br>
&nbsp;&nbsp;}<br>
}<br><br>
// 接收消息<br>
peerConnection.ondatachannel = (event) => {<br>
&nbsp;&nbsp;const channel = event.channel;<br>
&nbsp;&nbsp;channel.onmessage = (event) => {<br>
&nbsp;&nbsp;&nbsp;&nbsp;console.log('收到消息:', event.data);<br>
&nbsp;&nbsp;};<br>
};
                </div>
                
                <h3>修复的连接问题:</h3>
                <ul>
                    <li>使用公共STUN服务器解决NAT穿透问题</li>
                    <li>优化信令交换流程</li>
                    <li>添加详细的连接状态跟踪</li>
                    <li>改进错误处理和重连机制</li>
                </ul>
            </div>
        </div>
        
        <div class="card">
            <h2 class="card-title"><i class="fas fa-info-circle"></i> 连接建立流程</h2>
            
            <div class="instructions">
                <h3>修复后的连接过程:</h3>
                <ol>
                    <li><strong>信令连接</strong>:连接到WebSocket信令服务器</li>
                    <li><strong>生成ID</strong>:创建唯一的客户端标识符</li>
                    <li><strong>交换SDP</strong>:通过信令服务器交换会话描述</li>
                    <li><strong>ICE穿透</strong>:使用STUN服务器获取公网地址</li>
                    <li><strong>建立连接</strong>:完成P2P连接建立</li>
                    <li><strong>数据通道</strong>:打开RTCDataChannel进行通信</li>
                </ol>
                
                <h3>使用说明:</h3>
                <ol>
                    <li>打开两个浏览器标签页(或两台设备)</li>
                    <li>复制第一个标签页的本地ID到第二个标签页的"对等端ID"字段</li>
                    <li>在第二个标签页点击"连接到对等端"</li>
                    <li>连接建立后即可发送消息</li>
                </ol>
                
                <div class="status connected">
                    <i class="fas fa-shield-alt"></i> 所有消息均使用端到端加密(DTLS),确保通信安全
                </div>
            </div>
        </div>
        
        <footer class="footer">
            <p>© 2023 WebRTC消息系统 | 修复连接问题版本 | 使用公共STUN服务器</p>
        </footer>
    </div>

    <script>
    document.addEventListener('DOMContentLoaded', function() {
        // 页面元素
        const localIdSpan = document.getElementById('local-id');
        const remoteIdInput = document.getElementById('remote-id');
        const connectBtn = document.getElementById('connect-btn');
        const disconnectBtn = document.getElementById('disconnect-btn');
        const sendBtn = document.getElementById('send-btn');
        const messageInput = document.getElementById('message-input');
        const chatMessages = document.getElementById('chat-messages');
        const connectionStatus = document.getElementById('connection-status');
        const signalingStatus = document.getElementById('signaling-status');
        const messagesSentSpan = document.getElementById('messages-sent');
        const messagesReceivedSpan = document.getElementById('messages-received');
        const connectionTypeSpan = document.getElementById('connection-type');
        const debugOutput = document.getElementById('debug-output');
        const clearDebugBtn = document.getElementById('clear-debug');
        let localStream = null;
        let remoteStream = null;
        const localVideo = document.getElementById('local-video');
        const remoteVideo = document.getElementById('remote-video');
        const startVideoBtn = document.getElementById('start-video-btn');
        const stopVideoBtn = document.getElementById('stop-video-btn');

        
        // 步骤指示器
        const step1 = document.getElementById('step1');
        const step2 = document.getElementById('step2');
        const step3 = document.getElementById('step3');
        const step4 = document.getElementById('step4');
        
        // 重置步骤指示器
        function resetSteps() {
            step1.classList.remove('active');
            step2.classList.remove('active');
            step3.classList.remove('active');
            step4.classList.remove('active');
        }
        
        // 更新步骤指示器
        function updateStep(stepNumber) {
            resetSteps();
            if (stepNumber >= 1) step1.classList.add('active');
            if (stepNumber >= 2) step2.classList.add('active');
            if (stepNumber >= 3) step3.classList.add('active');
            if (stepNumber >= 4) step4.classList.add('active');
        }
        
        // 添加调试信息
        function addDebugMessage(message) {
            const messageElement = document.createElement('div');
            messageElement.classList.add('debug-message');
            messageElement.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
            debugOutput.appendChild(messageElement);
            debugOutput.scrollTop = debugOutput.scrollHeight;
        }
        
        // 状态变量
        let localId = '';
        let remoteId = '';
        let peerConnection = null;
        let dataChannel = null;
        let messagesSent = 0;
        let messagesReceived = 0;
        let signalingSocket = null;
        
        // 生成随机ID
        function generateId() {
            return Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
        }
        
        // 初始化本地ID
        function initLocalId() {
            localId = generateId();
            localIdSpan.textContent = localId;
            addDebugMessage(`本地ID已生成: ${localId}`);
        }

        
        // 添加获取本地媒体流的函数
        async function startLocalVideo() {
            try {
                const constraints = {
                    video: {
                        width: { ideal: 1280 },
                        height: { ideal: 720 }
                    },
                    audio: true
                };
                
                localStream = await navigator.mediaDevices.getUserMedia(constraints);
                localVideo.srcObject = localStream;
                addDebugMessage('本地视频流已启动');
                
                startVideoBtn.disabled = true;
                stopVideoBtn.disabled = false;
                
                // 如果已经建立了连接,需要重新协商
                if (peerConnection && peerConnection.connectionState === 'connected') {
                    signalingSocket.send(JSON.stringify({
                        type: 'mediaStatus',
                        from: localId,
                        to: remoteId,
                        status: 'started'
                    }));
                    
                    await renegotiateConnection();
                }
            } catch (error) {
                addDebugMessage(`获取本地媒体流失败: ${error.message}`);
                addMessage('系统', `无法访问摄像头或麦克风: ${error.message}`, 'remote');
            }
        }

        // 添加停止本地视频的函数
        function stopLocalVideo() {
            if (localStream) {
                // 如果存在对等连接,需要移除发送器
                if (peerConnection && peerConnection.signalingState !== 'closed') {
                    signalingSocket.send(JSON.stringify({
                        type: 'mediaStatus',
                        from: localId,
                        to: remoteId,
                        status: 'stopped'
                    }));
                    localStream.getTracks().forEach(track => {
                        // 查找并移除对应的发送器
                        const sender = peerConnection.getSenders().find(s => s.track === track);
                        if (sender) {
                            peerConnection.removeTrack(sender);
                        }
                        track.stop();
                    });
                    
                    // 触发重新协商
                    renegotiateAfterTrackChange();
                } else {
                    // 如果没有对等连接,只停止轨道
                    localStream.getTracks().forEach(track => track.stop());
                }
                
                localVideo.srcObject = null;
                localStream = null;
                addDebugMessage('本地视频流已停止');
                
                startVideoBtn.disabled = false;
                stopVideoBtn.disabled = true;
            }
        }

        // 添加处理媒体状态变更消息的函数
        async function handleMediaStatus(message) {
            addDebugMessage(`收到媒体状态变更通知: ${message.status}`);
            
            if (message.status === 'stopped') {
                // 对端已停止视频,清除远程视频显示
                if (remoteStream) {
                    // 停止远程流的所有轨道
                    remoteStream.getTracks().forEach(track => {
                        try {
                            track.stop();
                        } catch (e) {
                            // 忽略错误
                        }
                    });
                }
                
                // 清除远程视频显示
                remoteStream = null;
                remoteVideo.srcObject = null;
                addDebugMessage('远程视频流已清除');
            } else if (message.status === 'started') {
                // 对端已重新开启视频,准备接收新的媒体流
                addDebugMessage('对端已重新开启视频');
                // 确保 remoteStream 存在
                if (!remoteStream) {
                    remoteStream = new MediaStream();
                    remoteVideo.srcObject = remoteStream;
                }

                // 如果有对等连接,触发重新协商
                if (peerConnection) {
                    renegotiateAfterRemoteStarted();
                }
            }
        }

        
        // 当对端重新开启视频时触发重新协商
        async function renegotiateAfterRemoteStarted() {
            try {
                if (!peerConnection || !localStream) return;
                
                addDebugMessage('开始重新协商连接');
                
                // 添加本地媒体轨道(如果尚未添加)
                localStream.getTracks().forEach(track => {
                    const existingSender = peerConnection.getSenders().find(sender => 
                        sender.track && sender.track.kind === track.kind);
                    
                    if (!existingSender) {
                        peerConnection.addTrack(track, localStream);
                        addDebugMessage(`重新添加媒体轨道: ${track.kind}`);
                    }
                });
                
                // 创建新的offer
                const offer = await peerConnection.createOffer({
                    offerToReceiveAudio: true,
                    offerToReceiveVideo: true
                });
                
                await peerConnection.setLocalDescription(offer);
                
                // 发送媒体协商offer
                signalingSocket.send(JSON.stringify({
                    type: 'mediaOffer',
                    from: localId,
                    to: remoteId,
                    sdp: offer.sdp
                }));
                
                addDebugMessage('已发送重新协商OFFER');
            } catch (error) {
                addDebugMessage(`重新协商失败: ${error.message}`);
            }
        }

        // 在停止或添加轨道后触发重新协商
        async function renegotiateAfterTrackChange() {
            try {
                if (!peerConnection) return;
                
                // 创建新的offer
                const offer = await peerConnection.createOffer({
                    offerToReceiveAudio: true,
                    offerToReceiveVideo: true
                });
                
                await peerConnection.setLocalDescription(offer);
                
                // 发送媒体协商offer
                signalingSocket.send(JSON.stringify({
                    type: 'mediaOffer',
                    from: localId,
                    to: remoteId,
                    sdp: offer.sdp
                }));
                
                addDebugMessage('已发送重新协商OFFER');
            } catch (error) {
                addDebugMessage(`重新协商失败: ${error.message}`);
            }
        }

        // 添加重新协商连接的函数
        async function renegotiateConnection() {
            try {
                if (!peerConnection || !localStream) return;
                
                // 检查并添加本地媒体流到连接(避免重复添加)
                let tracksAdded = false;
                localStream.getTracks().forEach(track => {
                    // 检查是否已经存在该轨道的发送器
                    const existingSender = peerConnection.getSenders().find(sender => 
                        sender.track && sender.track.kind === track.kind);
                    
                    // 如果不存在相同类型的发送器,则添加新轨道
                    if (!existingSender) {
                        peerConnection.addTrack(track, localStream);
                        addDebugMessage(`添加媒体轨道: ${track.kind}`);
                        tracksAdded = true;
                    }
                });
                
                // 如果添加了新轨道,则触发重新协商
                if (tracksAdded) {
                    // 创建新的offer
                    const offer = await peerConnection.createOffer({
                        offerToReceiveAudio: true,
                        offerToReceiveVideo: true
                    });
                    
                    await peerConnection.setLocalDescription(offer);
                    
                    // 发送媒体协商offer
                    signalingSocket.send(JSON.stringify({
                        type: 'mediaOffer',
                        from: localId,
                        to: remoteId,
                        sdp: offer.sdp
                    }));
                    
                    addDebugMessage('已发送媒体协商OFFER');
                }
            } catch (error) {
                addDebugMessage(`重新协商失败: ${error.message}`);
            }
        }
        
        // 处理媒体协商消息
        async function handleMediaOffer(message) {
            try {
                if (!peerConnection) {
                    addDebugMessage('错误:peerConnection未初始化');
                    return;
                }
                
                addDebugMessage('处理媒体协商OFFER');
                
                // 设置远程描述
                const offer = new RTCSessionDescription({
                    type: 'offer',
                    sdp: message.sdp
                });
                
                await peerConnection.setRemoteDescription(offer);
                addDebugMessage('已设置远程描述(媒体OFFER)');
                
                // 添加本地媒体流
                // if (localStream) {
                //     localStream.getTracks().forEach(track => {
                //         peerConnection.addTrack(track, localStream);
                //     });
                // }
                // 添加本地媒体流
                if (localStream) {
                    localStream.getTracks().forEach(track => {
                        // 检查是否已经存在该轨道的发送器
                        const existingSender = peerConnection.getSenders().find(sender => 
                            sender.track && sender.track.kind === track.kind);
                        
                        // 如果不存在相同类型的发送器,则添加新轨道
                        if (!existingSender) {
                            peerConnection.addTrack(track, localStream);
                            addDebugMessage(`添加媒体轨道: ${track.kind}`);
                        }
                    });
                }
                
                // 创建ANSWER
                const answer = await peerConnection.createAnswer({
                    offerToReceiveAudio: true,
                    offerToReceiveVideo: true
                });
                
                await peerConnection.setLocalDescription(answer);
                addDebugMessage('已创建媒体ANSWER');
                
                // 发送ANSWER
                signalingSocket.send(JSON.stringify({
                    type: 'mediaAnswer',
                    from: localId,
                    to: message.from,
                    sdp: answer.sdp
                }));
                
                addDebugMessage('已发送媒体ANSWER');
            } catch (error) {
                addDebugMessage(`处理媒体OFFER失败: ${error.message}`);
            }
        }

        async function handleMediaAnswer(message) {
            try {
                if (!peerConnection) {
                    addDebugMessage('错误:peerConnection未初始化');
                    return;
                }
                
                addDebugMessage('处理媒体协商ANSWER');
                
                // 设置远程描述
                await peerConnection.setRemoteDescription(new RTCSessionDescription({
                    type: 'answer',
                    sdp: message.sdp
                }));
                
                addDebugMessage('已设置远程描述(媒体ANSWER)');
            } catch (error) {
                addDebugMessage(`处理媒体ANSWER失败: ${error.message}`);
            }
        }
        
        // 初始化信令连接
        function initSignaling() {
            try {
                // 使用公共测试信令服务器
                signalingSocket = new WebSocket('ws://localhost:3000');
                
                signalingSocket.onopen = () => {
                    signalingStatus.innerHTML = '<i class="fas fa-server"></i> 信令服务器状态: 已连接';
                    addDebugMessage('信令服务器连接成功');
                    updateStep(1);
                    
                    // 注册客户端
                    signalingSocket.send(JSON.stringify({
                        type: 'register',
                        id: localId
                    }));
                };
                
                signalingSocket.onmessage = (event) => {
                    let message;
                    if (event.data instanceof Blob) {
                        // 处理Blob数据
                        event.data.text().then(text => {
                            message = JSON.parse(text);
                            addDebugMessage(`收到信令消息: ${message && message.type}`);
                            handleSignalingMessage(message);
                        });
                        addDebugMessage('收到二进制格式的信令消息');
                    } else {
                        message = JSON.parse(event.data);
                        addDebugMessage(`收到信令消息: ${message && message.type}`);
                        handleSignalingMessage(message);
                    }
                };
                
                signalingSocket.onerror = (error) => {
                    addDebugMessage(`信令服务器错误: ${error.message}`);
                    signalingStatus.innerHTML = '<i class="fas fa-server"></i> 信令服务器状态: 连接错误';
                };
                
                signalingSocket.onclose = () => {
                    addDebugMessage('信令服务器连接已关闭');
                    signalingStatus.innerHTML = '<i class="fas fa-server"></i> 信令服务器状态: 已断开';
                };
            } catch (error) {
                addDebugMessage(`信令连接失败: ${error.message}`);
                signalingStatus.innerHTML = '<i class="fas fa-server"></i> 信令服务器状态: 连接失败';
            }
        }
        
        // 处理信令消息
        function handleSignalingMessage(message) {
            if (!peerConnection) return;
            
            switch (message.type) {
                case 'offer':
                    addDebugMessage('收到OFFER,正在处理...');
                    handleOffer(message);
                    break;
                    
                case 'answer':
                    addDebugMessage('收到ANSWER,正在处理...');
                    handleAnswer(message);
                    break;
                    
                case 'candidate':
                    addDebugMessage('收到ICE候选,正在处理...');
                    handleCandidate(message);
                    break;
                    
                case 'mediaOffer':
                    addDebugMessage('收到媒体协商OFFER,正在处理...');
                    handleMediaOffer(message);
                    break;
                    
                case 'mediaAnswer':
                    addDebugMessage('收到媒体协商ANSWER,正在处理...');
                    handleMediaAnswer(message);
                    break;
                    
                case 'mediaStatus':
                    addDebugMessage('收到媒体状态变更通知,正在处理...');
                    handleMediaStatus(message);
                    break;
            }
        }
        
        // 处理OFFER
        async function handleOffer(message) {
            try {
                if (!peerConnection) {
                    addDebugMessage('错误:peerConnection未初始化');
                    return;
                }
                // 设置远程描述
                const offer = new RTCSessionDescription({
                    type: 'offer',
                    sdp: message.sdp
                });
                await peerConnection.setRemoteDescription(offer);
                addDebugMessage('已设置远程描述(OFFER)');
                
                addDebugMessage('已设置远程描述(OFFER)');
                
                // // 添加本地媒体流
                // if (localStream) {
                //     localStream.getTracks().forEach(track => {
                //         peerConnection.addTrack(track, localStream);
                //     });
                // }
                // 添加本地媒体流
                if (localStream) {
                    localStream.getTracks().forEach(track => {
                        // 检查是否已经存在该轨道的发送器
                        const existingSender = peerConnection.getSenders().find(sender => 
                            sender.track && sender.track.kind === track.kind);
                        
                        // 如果不存在相同类型的发送器,则添加新轨道
                        if (!existingSender) {
                            peerConnection.addTrack(track, localStream);
                            addDebugMessage(`添加媒体轨道: ${track.kind}`);
                        }
                    });
                }
                
                // 创建ANSWER
                const answer = await peerConnection.createAnswer({
                    offerToReceiveAudio: true,
                    offerToReceiveVideo: true
                });
                await peerConnection.setLocalDescription(answer);
                
                addDebugMessage('已创建ANSWER');
                
                // 发送ANSWER
                signalingSocket.send(JSON.stringify({
                    type: 'answer',
                    from: localId,
                    to: message.from,
                    sdp: answer.sdp
                }));
                
                addDebugMessage('已发送ANSWER');
                updateStep(3);
            } catch (error) {
                addDebugMessage(`处理OFFER失败: ${error}`);
            }
        }
        
        // 处理ANSWER
        async function handleAnswer(message) {
            try {
                if (!peerConnection) {
                    addDebugMessage('错误:peerConnection未初始化');
                    return;
                }
                // 设置远程描述
                await peerConnection.setRemoteDescription(new RTCSessionDescription({
                    type: 'answer',
                    sdp: message.sdp
                }));
                
                addDebugMessage('已设置远程描述(ANSWER)');
                updateStep(3);
            } catch (error) {
                addDebugMessage(`处理ANSWER时出错: ${error.message}`);
            }
        }
        
        // 处理ICE候选
        async function handleCandidate(message) {
            try {
                if (!peerConnection) {
                    addDebugMessage('错误:peerConnection未初始化');
                    return;
                }

                if (message.candidate && message.candidate.candidate) {
                    await peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate));
                    addDebugMessage('成功添加ICE候选: ' + message.candidate.candidate);
                } else {
                    addDebugMessage('收到空的ICE候选,可能是结束信号');
                }
            } catch (error) {
                addDebugMessage(`添加候选失败: ${error.message}`);
            }
        }
        
        // 初始化WebRTC连接
        async function initConnection() {
            try {
                addDebugMessage('正在初始化WebRTC连接...');
                // 创建配置 - 使用公共STUN服务器
                const config = {
                    iceServers: [
                        // 国内可尝试的 (测试用,不稳定!)
                        { urls: "stun:localhost:3478" },
                        { urls: "stun:stun.voipstunt.com:3478" },
                        { urls: "stun:stun.ekiga.net:3478" },
                        { urls: "stun:stun.voxgratia.org:3478" },
                        { urls: "stun:stun.internetcalls.com:3478" },
                        { urls: "stun:stun.voip.aebc.com:3478" },
                        { urls: "stun:stun.internetcalls.com:3478" },

                        { urls: "stun:stun.miwifi.com:3478" },
                        { urls: "stun:stun.qq.com:3478" },
                        // 全球知名的 (非国内,延迟可能高)
                        { urls: "stun:stun.l.google.com:19302" },
                        { urls: "stun:stun1.l.google.com:19302" },
                        { urls: "stun:stun2.l.google.com:19302" },
                        {
                            urls: "turn:freeturn.net:80",
                            username: "free",
                            credential: "free"
                        },
                        {
                            urls: "turn:freeturn.tel:80",
                            username: "free",
                            credential: "free"
                        },
                    ]
                };
                
                // 创建对等连接
                peerConnection = new RTCPeerConnection(config);

                // 添加本地媒体流到连接
                // if (localStream) {
                //     localStream.getTracks().forEach(track => {
                //         peerConnection.addTrack(track, localStream);
                //     });
                // }
                // 添加本地媒体流到连接(避免重复添加)
                if (localStream) {
                    localStream.getTracks().forEach(track => {
                        // 检查是否已经存在该轨道的发送器
                        const existingSender = peerConnection.getSenders().find(sender => 
                            sender.track && sender.track.kind === track.kind);
                        
                        // 如果不存在相同类型的发送器,则添加新轨道
                        if (!existingSender) {
                            peerConnection.addTrack(track, localStream);
                            addDebugMessage(`添加媒体轨道: ${track.kind}`);
                        }
                    });
                }

                // 设置远程流接收
                peerConnection.ontrack = (event) => {
                    addDebugMessage('收到远程媒体流');
                    if (!remoteStream) {
                        remoteStream = new MediaStream();
                        remoteVideo.srcObject = remoteStream;
                    }
                    remoteStream.addTrack(event.track);
                };
                
                // 设置ICE候选处理
                peerConnection.onicecandidate = (event) => {
                    if (event.candidate) {
                        signalingSocket.send(JSON.stringify({
                            type: 'candidate',
                            from: localId,
                            to: remoteId,
                            candidate: event.candidate
                        }));
                        addDebugMessage('已发送ICE候选');
                    } else {
                        addDebugMessage('ICE候选收集完成');
                    }
                };
                
                // 设置ICE连接状态变化
                peerConnection.oniceconnectionstatechange = () => {
                    addDebugMessage(`ICE连接状态: ${peerConnection.iceConnectionState}`);
                    if (peerConnection.iceConnectionState === 'connected') {
                        updateStep(4);
                    }
                };

                // 如果是发起方,创建数据通道
                if (!dataChannel) {
                    dataChannel = peerConnection.createDataChannel("messaging", {
                        ordered: true
                    });
                    setupDataChannelEvents();
                }
                
                // 设置远程数据通道事件
                peerConnection.ondatachannel = (event) => {
                    addDebugMessage('收到远程数据通道');
                    dataChannel = event.channel;
                    setupDataChannelEvents();
                };

                // 创建包含媒体信息的OFFER
                const offer = await peerConnection.createOffer({
                    offerToReceiveAudio: true,
                    offerToReceiveVideo: true
                });
                
                await peerConnection.setLocalDescription(offer);
                addDebugMessage('已创建OFFER');
                
                // 发送OFFER
                signalingSocket.send(JSON.stringify({
                    type: 'offer',
                    from: localId,
                    to: remoteId,
                    sdp: offer.sdp
                }));
                
                addDebugMessage('已发送OFFER');
                updateStep(2);
                
                // 更新状态
                connectionStatus.textContent = "连接中...";
                connectionStatus.style.color = "#ffcc00";
                
            } catch (error) {
                addDebugMessage(`初始化连接失败: ${error.message}`);
                connectionStatus.textContent = "连接失败";
                connectionStatus.style.color = "#ff6b6b";
            }
        }
        
        // 设置数据通道事件
        function setupDataChannelEvents() {
            if (!dataChannel) return;

            // ICE状态跟踪
            peerConnection.oniceconnectionstatechange = () => {
                const state = peerConnection.iceConnectionState;
                addDebugMessage(`ICE连接状态变化: ${state}`);
                
                switch(state) {
                    case 'checking':
                        addDebugMessage('正在进行ICE连接检查...');
                        break;
                    case 'connected':
                        addDebugMessage('ICE连接已建立');
                        updateStep(4);
                        // 确保连接状态更新
                        connectionStatus.textContent = "已连接";
                        connectionStatus.style.color = "#00ff80";
                        connectBtn.disabled = true;
                        disconnectBtn.disabled = false;
                        messageInput.disabled = false;
                        sendBtn.disabled = false;
                        connectionTypeSpan.textContent = "P2P直连";
                        break;
                    case 'completed':
                        addDebugMessage('ICE连接完成');
                        break;
                    case 'disconnected':
                        addDebugMessage('ICE连接断开,尝试重新连接...');
                        // 不立即重连,等待一段时间看是否能自动恢复
                        setTimeout(() => {
                            if (peerConnection && peerConnection.iceConnectionState === 'disconnected') {
                                addDebugMessage('ICE连接仍未恢复,尝试重启');
                                // 尝试收集新的候选
                                peerConnection.restartIce();
                            }
                        }, 3000);
                        break;
                    case 'failed':
                        addDebugMessage('ICE连接失败,尝试重启...');
                        peerConnection.restartIce();
                        break;
                    case 'closed':
                        addDebugMessage('ICE连接已关闭');
                        break;
                }
            };

            dataChannel.onopen = () => {
                addDebugMessage('数据通道已打开');
                connectionStatus.textContent = "已连接";
                connectionStatus.style.color = "#00ff80";
                connectBtn.disabled = true;
                disconnectBtn.disabled = false;
                messageInput.disabled = false;
                sendBtn.disabled = false;
                connectionTypeSpan.textContent = "P2P直连";
                
                // 添加欢迎消息
                addMessage('系统', 'WebRTC点对点连接已建立!现在可以发送消息了', 'remote');
            };

            dataChannel.onclose = () => {
                setTimeout(() => {
                    addDebugMessage('尝试重新连接...');
                    initConnection();
                }, 2000);
            };
            
            dataChannel.onerror = (error) => {
                addDebugMessage(`数据通道错误: ${error.message}`);
            };
            
            dataChannel.onmessage = (event) => {
                addDebugMessage(`收到消息: ${event.data}`);
                messagesReceived++;
                messagesReceivedSpan.textContent = messagesReceived;
                addMessage('远程端', event.data, 'remote');
            };
        }
        
        // 添加消息到聊天框
        function addMessage(sender, text, type) {
            const messageElement = document.createElement('div');
            messageElement.classList.add('message', type);
            
            const now = new Date();
            const timeString = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
            
            messageElement.innerHTML = `
                <div>${text}</div>
                <div class="message-info">${sender} • ${timeString}</div>
            `;
            
            chatMessages.appendChild(messageElement);
            chatMessages.scrollTop = chatMessages.scrollHeight;
        }
        
        // 发送消息
        function sendMessage() {
            const message = messageInput.value.trim();
            if (!message) return;
            
            if (dataChannel && dataChannel.readyState === 'open') {
                dataChannel.send(message);
                messagesSent++;
                messagesSentSpan.textContent = messagesSent;
                addMessage('我', message, 'local');
                messageInput.value = '';
                addDebugMessage(`消息已发送: ${message}`);
            } else {
                addMessage('系统', '数据通道尚未打开,无法发送消息', 'remote');
            }
        }
        
        // 断开连接
        function disconnect() {
            if (peerConnection) {
                peerConnection.close();
                peerConnection = null;
            }
            if (dataChannel) {
                dataChannel.close();
                dataChannel = null;
            }
            
            // 停止本地视频流
            if (localStream) {
                localStream.getTracks().forEach(track => track.stop());
                localStream = null;
                localVideo.srcObject = null;
            }
            
            // 清除远程视频流
            remoteVideo.srcObject = null;
            remoteStream = null;
            
            connectionStatus.textContent = "未连接";
            connectionStatus.style.color = "#ff6b6b";
            connectBtn.disabled = false;
            disconnectBtn.disabled = true;
            messageInput.disabled = true;
            sendBtn.disabled = true;
            connectionTypeSpan.textContent = "-";
            startVideoBtn.disabled = false;
            stopVideoBtn.disabled = true;
            
            addDebugMessage('连接已断开');
            resetSteps();
            
            // 添加断开消息
            addMessage('系统', '连接已断开', 'remote');
        }
        
        // 事件监听器
        startVideoBtn.addEventListener('click', startLocalVideo);
        stopVideoBtn.addEventListener('click', stopLocalVideo);
        connectBtn.addEventListener('click', () => {
            remoteId = remoteIdInput.value.trim();
            if (!remoteId) {
                addMessage('系统', '请输入对等端ID', 'remote');
                return;
            }
            
            if (remoteId === localId) {
                addMessage('系统', '不能连接到自己的ID', 'remote');
                return;
            }
            
            initConnection();
            addMessage('系统', `正在连接对等端: ${remoteId}`, 'remote');
        });
        
        disconnectBtn.addEventListener('click', disconnect);
        
        sendBtn.addEventListener('click', sendMessage);
        
        messageInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });
        
        clearDebugBtn.addEventListener('click', () => {
            debugOutput.innerHTML = '';
        });
        
        // 初始化
        initLocalId();
        initSignaling();
    });
</script>
</body>
</html>

总结

这个WebRTC视频聊天系统展示了现代Web实时通信技术的强大能力。通过WebRTC、WebSocket和现代Web API的结合,实现了高质量的点对点音视频通信和文本消息传输。系统具有良好的可扩展性和稳定性,为开发者提供了学习和参考的优秀示例。 对于希望深入了解WebRTC技术的开发者来说,这个项目提供了完整的实践案例,涵盖了从连接建立、媒体处理到数据传输的全流程实现。