🎯《WebRTC:让浏览器开口说话的黑科技!》🚀

64 阅读23分钟

副标题:从"这是啥玩意儿"到"我也能做视频聊天"的进化之路

🎨 作者:你的技术好伙伴 | 📅 2024年版 | ⏱️ 预计阅读时间:15分钟


📚 目录


第一章:WebRTC是个什么鬼? 🤔

1.1 先来个官方定义

WebRTC = Web (网页) + Real-Time Communication (实时通信)

简单翻译:网页实时通信技术

1.2 人话版解释

想象一下这个场景:

以前 😫:

  • 小明想和女朋友视频聊天
  • 下载QQ → 等待安装 → 注册账号 → 添加好友 → 等对方也下载安装...
  • 过了半小时,终于能聊了,黄花菜都凉了!

现在 😎:

  • 小明给女朋友发个网页链接
  • 点开链接 → 允许摄像头权限 → 开聊!
  • 前后不到10秒,爱情的火花瞬间点燃!💕

这就是WebRTC的魔力!不需要安装任何软件,打开浏览器就能视频通话!

1.3 一句话总结

WebRTC = 给浏览器装上了"千里眼" 👀 和"顺风耳" 👂 的超能力!


第二章:生活中的WebRTC比喻 🏠

为了让你真正理解WebRTC,我们用几个生活中的例子来比喻:

🎭 比喻1:对讲机模型

你(A房间)                                朋友(B房间)
   |                                          |
   |  "喂喂喂,听得到吗?"                      |
   |----------------------------------------->|
   |                                          |
   |                "听得到!很清楚!"          |
   |<-----------------------------------------|
   |                                          |

对应关系:

  • 对讲机 = WebRTC连接
  • 按下通话键 = 获取麦克风权限 (getUserMedia)
  • 调整频道 = 信令交换 (Signaling)
  • 声音传输 = 音频流传输

📮 比喻2:邮递员送信模型

场景:小明要给小红送一封信

第1步:写信 📝
   ↓
第2步:找邮递员(查地址)🚶
   ↓
第3步:邮递员查路线图 🗺️
   ↓
第4步:选择最快路径 🛤️
   ↓
第5步:送到!✅

对应到WebRTC:

  • 写信 = 准备要发送的音视频数据
  • 找邮递员 = 建立连接 (RTCPeerConnection)
  • 查路线图 = ICE协商(找网络路径)
  • 选择最快路径 = STUN/TURN服务器帮忙
  • 送到 = 数据成功传输

🏰 比喻3:城堡围墙模型(理解NAT穿透)

想象你住在一个有围墙的城堡里:

        城堡内部(内网)              城墙(路由器/NAT)         外面的世界(公网)
     
你的电脑 (192.168.1.5)    <-->    [城门守卫]    <-->    朋友的电脑 (公网IP)
                                   守卫会改地址!

问题: 你的朋友在外面,不知道你的内网地址(192.168.1.5),信送不进来!😱

解决方案:

  1. STUN服务器(像个告示牌)📋:告诉你"从外面看,你的地址是xxx"
  2. TURN服务器(像个邮局转发中心)📮:如果直接送不到,我来帮你转发!

第三章:WebRTC的工作原理深度剖析 🔬

3.1 完整工作流程图

┌─────────────────────────────────────────────────────────────────┐
│                    WebRTC 完整通信流程                            │
└─────────────────────────────────────────────────────────────────┘

   用户A                 信令服务器              STUN/TURN           用户B
     │                      │                      │                  │
     │  1. 打开网页          │                      │                  │
     │─────────────────────>│                      │                  │
     │                      │                      │                  │
     │  2. 获取摄像头/麦克风  │                      │                  │
     │  getUserMedia()      │                      │                  │
     │                      │                      │                  │
     │  3. 创建连接对象      │                      │                  │
     │  new RTCPeerConnection()                    │                  │
     │                      │                      │                  │
     │  4. 创建Offer         │                      │                  │
     │─────────────────────>│                      │                  │
     │                      │  5. 转发Offer        │                  │
     │                      │─────────────────────────────────────────>│
     │                      │                      │                  │
     │                      │  6. 创建Answer       │                  │
     │                      │<─────────────────────────────────────────│
     │  7. 接收Answer        │                      │                  │
     │<─────────────────────│                      │                  │
     │                      │                      │                  │
     │  8. 收集ICE候选       │                      │                  │
     │──────────────────────────────────────────────>│                  │
     │                      │        9. 返回公网地址 │                  │
     │<──────────────────────────────────────────────│                  │
     │                      │                      │                  │
     │  10. 交换ICE候选      │                      │                  │
     │<────────────────────>│<────────────────────────────────────────>│
     │                      │                      │                  │
     │  11. P2P连接建立!🎉  │                      │                  │
     │<═══════════════════════════════════════════════════════════════>│
     │                      │                      │                  │
     │         12. 开始传输音视频数据 💖💖💖                             │
     │<═══════════════════════════════════════════════════════════════>│
     │                      │                      │                  │

3.2 步骤详解(小白版)

🎬 第1步:获取媒体权限

就像你要打电话前,得先打开手机的麦克风权限一样。

// 浏览器会弹出提示:"是否允许访问摄像头和麦克风?"
navigator.mediaDevices.getUserMedia({ 
    video: true,  // 我要视频
    audio: true   // 我要声音
})
.then(stream => {
    console.log("✅ 成功获取摄像头和麦克风!");
    // 把自己的视频显示在页面上(自己能看到自己)
    document.getElementById('myVideo').srcObject = stream;
})
.catch(error => {
    console.log("❌ 获取失败:", error);
    alert("请允许使用摄像头和麦克风哦!");
});

🤝 第2步:建立连接(信令阶段)

这一步像是两个陌生人通过媒人(信令服务器)互相了解:

小明: "嘿,服务器,帮我告诉小红,我想和她视频聊天!" 服务器: "好的,小红在吗?小明想和你聊天~" 小红: "好啊!服务器帮我回复小明,我同意!"

// 创建连接对象(准备好通话设备)
const pc = new RTCPeerConnection({
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' }  // 谷歌提供的免费STUN服务器
    ]
});

// 小明创建"邀请"(Offer)
pc.createOffer()
  .then(offer => {
      pc.setLocalDescription(offer);
      // 通过信令服务器发送给小红
      signalingServer.send({ type: 'offer', offer: offer });
  });

// 小红收到邀请,创建"回复"(Answer)
pc.setRemoteDescription(offer);
pc.createAnswer()
  .then(answer => {
      pc.setLocalDescription(answer);
      // 发回给小明
      signalingServer.send({ type: 'answer', answer: answer });
  });

🗺️ 第3步:ICE协商(找路径)

这一步像是两个人在找见面地点:

小明: "我家在A小区,你从B路可以来" 小红: "我家在C小区,有3条路可以到你那" 系统: "好,我找到最快的路了!走这条!"

// 当发现新的网络路径时
pc.onicecandidate = (event) => {
    if (event.candidate) {
        // 发送给对方
        signalingServer.send({
            type: 'ice-candidate',
            candidate: event.candidate
        });
    }
};

// 收到对方的路径信息
pc.addIceCandidate(newCandidate);

🎉 第4步:连接成功!开始传输

// 监听对方的视频流
pc.ontrack = (event) => {
    console.log("🎊 收到对方的视频流!");
    document.getElementById('remoteVideo').srcObject = event.streams[0];
};

// 把自己的视频流添加到连接中
localStream.getTracks().forEach(track => {
    pc.addTrack(track, localStream);
});

第四章:核心技术组件详解 🛠️

4.1 三大核心API

🎥 1. getUserMedia - 设备管家

作用: 获取摄像头、麦克风的访问权限

就像: 你的贴身保镖,帮你管理摄像头和麦克风

// 基础用法
navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
})

// 高级用法(指定清晰度、帧率等)
navigator.mediaDevices.getUserMedia({
    video: {
        width: { min: 640, ideal: 1280, max: 1920 },
        height: { min: 480, ideal: 720, max: 1080 },
        frameRate: { ideal: 30 }
    },
    audio: {
        echoCancellation: true,  // 回声消除
        noiseSuppression: true,  // 噪音抑制
        autoGainControl: true    // 自动增益
    }
})

生活例子: 就像你买了个新手机,第一次打开相机APP,手机会问:"是否允许相机访问摄像头?" 📸


🌉 2. RTCPeerConnection - 连接大师

作用: 建立和管理点对点连接

就像: 婚介所的红娘,帮两个人牵线搭桥

// 创建连接
const config = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        {
            urls: 'turn:your-turn-server.com',
            username: 'user',
            credential: 'password'
        }
    ]
};

const peerConnection = new RTCPeerConnection(config);

// 监听连接状态
peerConnection.onconnectionstatechange = () => {
    console.log('连接状态:', peerConnection.connectionState);
    // 可能的状态:new, connecting, connected, disconnected, failed, closed
};

// 监听ICE连接状态
peerConnection.oniceconnectionstatechange = () => {
    console.log('ICE状态:', peerConnection.iceConnectionState);
};

状态转换图:

new(新建)
   ↓
connecting(连接中)⏳
   ↓
connected(已连接)✅ → 可以愉快聊天了!
   ↓
disconnected(断开)⚠️ → 网络不好?
   ↓
failed(失败)❌ → 完全连不上
   ↓
closed(关闭)🚪 → 挂断了

📨 3. RTCDataChannel - 快递员

作用: 传输非音视频的数据(文字、文件等)

就像: 快递小哥,能帮你送各种东西

// 创建数据通道
const dataChannel = peerConnection.createDataChannel('chat', {
    ordered: true  // 保证顺序
});

// 发送消息
dataChannel.send('你好呀!👋');

// 发送文件(二进制数据)
dataChannel.send(fileArrayBuffer);

// 接收消息
dataChannel.onmessage = (event) => {
    console.log('收到消息:', event.data);
};

// 监听通道状态
dataChannel.onopen = () => {
    console.log('数据通道已打开!✅');
};

dataChannel.onclose = () => {
    console.log('数据通道已关闭!');
};

应用场景:

  • 💬 聊天消息
  • 📁 文件传输
  • 🎮 游戏状态同步
  • 📊 实时数据共享

4.2 信令服务器(Signaling Server)

作用: 帮助两个客户端交换连接信息

重要: WebRTC本身不包含信令机制,需要自己实现!

🔍 为什么需要信令服务器?

想象两个人想视频聊天:

没有信令服务器: 😵

小明:我要和小红聊天!
小红:我要和小明聊天!
(但他们互相不知道对方的"地址",连不上!)

有信令服务器: 😊

小明 → 服务器:请帮我联系小红(附带我的连接信息)
服务器 → 小红:小明想和你聊天(转发连接信息)
小红 → 服务器:好的!(回复连接信息)
服务器 → 小明:小红同意了(转发连接信息)
(双方拿到对方信息,直接建立P2P连接!)

📡 信令服务器实现方式

方案1:WebSocket(最常用) ⭐⭐⭐⭐⭐

// 客户端代码
const signalingSocket = new WebSocket('ws://your-server.com:8080');

// 发送Offer
signalingSocket.send(JSON.stringify({
    type: 'offer',
    offer: offerDescription,
    targetUserId: 'user123'
}));

// 接收消息
signalingSocket.onmessage = (event) => {
    const message = JSON.parse(event.data);
    
    if (message.type === 'offer') {
        handleOffer(message.offer);
    } else if (message.type === 'answer') {
        handleAnswer(message.answer);
    } else if (message.type === 'ice-candidate') {
        handleIceCandidate(message.candidate);
    }
};

服务器端(Node.js + ws库):

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

const clients = new Map(); // 存储所有连接的客户端

wss.on('connection', (ws) => {
    const userId = generateUserId();
    clients.set(userId, ws);
    
    ws.on('message', (data) => {
        const message = JSON.parse(data);
        const targetClient = clients.get(message.targetUserId);
        
        if (targetClient) {
            // 转发消息给目标用户
            targetClient.send(data);
        }
    });
    
    ws.on('close', () => {
        clients.delete(userId);
    });
});

方案2:Socket.IO ⭐⭐⭐⭐

// 客户端
const socket = io('http://your-server.com');

socket.emit('offer', { offer, targetUserId });

socket.on('answer', (data) => {
    handleAnswer(data.answer);
});

方案3:HTTP轮询(不推荐,效率低)⭐⭐

方案4:Firebase Realtime Database(简单快速)⭐⭐⭐⭐


4.3 STUN 和 TURN 服务器

🔦 STUN服务器 - 镜子

全称: Session Traversal Utilities for NAT(NAT会话穿越工具)

作用: 告诉你"从外面看,你的地址是什么"

比喻:

你照镜子(STUN):
"哦!原来我在公网上的IP是 203.0.113.45!"

工作原理:

你的电脑(内网)        路由器(NAT)         STUN服务器
192.168.1.5              改地址            stun.google.com
     │                     │                     │
     │──"我是谁?"─────────>│                     │
     │                     │──"203.0.113.45"────>│
     │                     │<────────────────────│
     │<──"你是203.0.113.45"│                     │
     │                     │                     │

免费STUN服务器:

const config = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        { urls: 'stun:stun1.l.google.com:19302' },
        { urls: 'stun:stun2.l.google.com:19302' },
        { urls: 'stun:stun.stunprotocol.org' }
    ]
};

📮 TURN服务器 - 中继站

全称: Traversal Using Relays around NAT(使用中继穿越NAT)

作用: 当P2P连接失败时,充当中转站

比喻:

你想给朋友送快递,但两地不通邮:
TURN服务器 = 中间的快递中转站
你 → 中转站 → 朋友

什么时候需要TURN?

约5-10%的情况下,由于严格的防火墙或对称NAT,P2P连接会失败。这时就需要TURN:

用户A                TURN服务器              用户B
  │                      │                    │
  │─────音视频数据────────>│                    │
  │                      │─────转发数据────────>│
  │                      │<────转发数据─────────│
  │<─────转发数据─────────│                    │

⚠️ 注意:TURN服务器会消耗大量带宽!

配置TURN:

const config = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        {
            urls: 'turn:your-turn-server.com:3478',
            username: 'your-username',
            credential: 'your-password'
        }
    ]
};

搭建自己的TURN服务器(使用coturn):

  1. 安装coturn:
sudo apt-get install coturn
  1. 配置文件(/etc/turnserver.conf):
listening-port=3478
external-ip=你的公网IP
realm=你的域名
user=用户名:密码

4.4 SDP(会话描述协议)

全称: Session Description Protocol

作用: 描述连接的"配置清单"

比喻: 就像两个人约会前交换的个人信息:

小明的"个人简历"(SDP):
- 姓名:小明
- 支持的音频格式:opus, pcmu
- 支持的视频格式:VP8, H264
- 网络地址:候选1, 候选2, 候选3
- 加密方式:DTLS-SRTP

SDP示例:

v=0                                           # 版本
o=- 123456789 2 IN IP4 192.168.1.5           # 发起者信息
s=-                                           # 会话名称
t=0 0                                         # 时间
a=group:BUNDLE audio video                    # 音视频绑定
m=audio 9 UDP/TLS/RTP/SAVPF 111 103          # 音频媒体描述
c=IN IP4 192.168.1.5                         # 连接信息
a=rtpmap:111 opus/48000/2                    # 编解码器映射
a=candidate:1 1 UDP 2122260223 192.168.1.5 54321 typ host  # ICE候选
m=video 9 UDP/TLS/RTP/SAVPF 96 97            # 视频媒体描述
a=rtpmap:96 VP8/90000                        # VP8编解码器

你不需要手动写SDP! WebRTC会自动生成,你只需要交换它!


4.5 ICE(交互式连接建立)

全称: Interactive Connectivity Establishment

作用: 找到最佳的网络连接路径

比喻: 就像导航软件找最快路线:

导航软件(ICE):
"我找到了3条路线:
 路线1(P2P直连):5分钟 ⭐推荐
 路线2(通过STUN):8分钟
 路线3(通过TURN):15分钟"

ICE候选类型:

1. Host候选(最优) 🏠

  • 本地局域网地址
  • 例如:192.168.1.5
  • 速度最快,但只能在同一局域网使用

2. Server Reflexive候选(次优) 🌐

  • 通过STUN服务器获得的公网地址
  • 例如:203.0.113.45
  • P2P直连,速度快

3. Relay候选(保底) 📮

  • 通过TURN服务器中转
  • 速度较慢,但保证能连上

ICE工作流程:

1步:收集所有候选
   │
   ├─ 本地地址(192.168.1.5)
   ├─ 公网地址(通过STUN: 203.0.113.45)
   └─ 中继地址(通过TURN: relay.server.com)
   │
第2步:发送给对方
   │
第3步:对方也发送候选
   │
第4步:尝试所有可能的配对
   │
   尝试1192.168.1.5 ←→ 192.168.1.8    ❌ 失败
   尝试2203.0.113.45 ←→ 198.51.100.20 ✅ 成功!
   │
第5步:选择最佳路径

代码监听ICE过程:

pc.onicecandidate = (event) => {
    if (event.candidate) {
        console.log('发现新候选:', event.candidate.type);
        console.log('地址:', event.candidate.address);
        console.log('端口:', event.candidate.port);
        
        // 发送给对方
        sendToRemote({
            type: 'ice-candidate',
            candidate: event.candidate
        });
    } else {
        console.log('✅ ICE候选收集完成!');
    }
};

pc.onicegatheringstatechange = () => {
    console.log('ICE收集状态:', pc.iceGatheringState);
    // new → gathering → complete
};

第五章:动手实践:搭建你的第一个WebRTC应用 👨‍💻

5.1 项目目标

我们要做一个简单的1对1视频聊天应用! 🎥

功能清单:

  • ✅ 显示自己的视频
  • ✅ 显示对方的视频
  • ✅ 发送文字消息
  • ✅ 显示连接状态

5.2 项目结构

webrtc-video-chat/
│
├── index.html          # 前端页面
├── app.js              # 前端逻辑
├── style.css           # 样式
│
├── server/
│   ├── server.js       # 信令服务器(Node.js)
│   └── package.json    # 依赖配置

5.3 完整代码实现

📄 index.html

<!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="style.css">
</head>
<body>
    <div class="container">
        <h1>🎥 我的第一个WebRTC应用</h1>
        
        <!-- 连接状态 -->
        <div class="status" id="status">
            <span class="status-dot"></span>
            <span id="statusText">未连接</span>
        </div>
        
        <!-- 视频区域 -->
        <div class="video-container">
            <div class="video-wrapper">
                <video id="localVideo" autoplay muted playsinline></video>
                <p class="video-label">📹 你自己</p>
            </div>
            
            <div class="video-wrapper">
                <video id="remoteVideo" autoplay playsinline></video>
                <p class="video-label">👤 对方</p>
            </div>
        </div>
        
        <!-- 控制按钮 -->
        <div class="controls">
            <input type="text" id="roomId" placeholder="输入房间号(如:room123)">
            <button id="startBtn" onclick="startCall()">🚀 开始通话</button>
            <button id="hangupBtn" onclick="hangup()" disabled>📞 挂断</button>
        </div>
        
        <!-- 聊天区域 -->
        <div class="chat-container">
            <div class="messages" id="messages"></div>
            <div class="message-input">
                <input type="text" id="messageInput" placeholder="输入消息...">
                <button onclick="sendMessage()">发送 💬</button>
            </div>
        </div>
    </div>
    
    <script src="app.js"></script>
</body>
</html>

🎨 style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 20px;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    background: white;
    border-radius: 20px;
    padding: 30px;
    box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}

h1 {
    text-align: center;
    color: #333;
    margin-bottom: 20px;
}

/* 状态指示器 */
.status {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 10px;
    background: #f0f0f0;
    border-radius: 10px;
    margin-bottom: 20px;
}

.status-dot {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #ccc;
    animation: pulse 2s infinite;
}

.status.connected .status-dot {
    background: #4caf50;
}

.status.connecting .status-dot {
    background: #ff9800;
}

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}

/* 视频容器 */
.video-container {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    margin-bottom: 20px;
}

.video-wrapper {
    position: relative;
    background: #000;
    border-radius: 15px;
    overflow: hidden;
    aspect-ratio: 16 / 9;
}

video {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

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

/* 控制按钮 */
.controls {
    display: flex;
    gap: 10px;
    justify-content: center;
    margin-bottom: 20px;
}

input[type="text"] {
    flex: 1;
    max-width: 300px;
    padding: 12px 20px;
    border: 2px solid #ddd;
    border-radius: 25px;
    font-size: 16px;
    outline: none;
    transition: border-color 0.3s;
}

input[type="text"]:focus {
    border-color: #667eea;
}

button {
    padding: 12px 30px;
    border: none;
    border-radius: 25px;
    font-size: 16px;
    font-weight: bold;
    cursor: pointer;
    transition: all 0.3s;
}

#startBtn {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
}

#startBtn:hover {
    transform: translateY(-2px);
    box-shadow: 0 5px 20px rgba(102,126,234,0.4);
}

#hangupBtn {
    background: #f44336;
    color: white;
}

#hangupBtn:hover:not(:disabled) {
    transform: translateY(-2px);
    box-shadow: 0 5px 20px rgba(244,67,54,0.4);
}

button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* 聊天区域 */
.chat-container {
    border: 2px solid #ddd;
    border-radius: 15px;
    overflow: hidden;
}

.messages {
    height: 200px;
    overflow-y: auto;
    padding: 15px;
    background: #f9f9f9;
}

.message {
    margin-bottom: 10px;
    padding: 8px 15px;
    border-radius: 15px;
    max-width: 70%;
    word-wrap: break-word;
}

.message.local {
    background: #667eea;
    color: white;
    margin-left: auto;
    text-align: right;
}

.message.remote {
    background: #e0e0e0;
    color: #333;
}

.message-input {
    display: flex;
    gap: 10px;
    padding: 15px;
    background: white;
}

.message-input input {
    flex: 1;
    max-width: none;
}

/* 响应式设计 */
@media (max-width: 768px) {
    .video-container {
        grid-template-columns: 1fr;
    }
    
    .controls {
        flex-direction: column;
    }
    
    input[type="text"] {
        max-width: none;
    }
}

💻 app.js(前端逻辑)

// ==================== 全局变量 ====================

let localStream;              // 本地音视频流
let remoteStream;             // 远程音视频流
let peerConnection;           // RTCPeerConnection对象
let dataChannel;              // 数据通道
let socket;                   // WebSocket连接
let roomId;                   // 房间ID

// STUN/TURN服务器配置
const iceServers = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        { urls: 'stun:stun1.l.google.com:19302' },
    ]
};

// DOM元素
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const statusElement = document.getElementById('status');
const statusText = document.getElementById('statusText');
const startBtn = document.getElementById('startBtn');
const hangupBtn = document.getElementById('hangupBtn');
const messagesDiv = document.getElementById('messages');

// ==================== 主要函数 ====================

// 开始通话
async function startCall() {
    roomId = document.getElementById('roomId').value;
    
    if (!roomId) {
        alert('请输入房间号!');
        return;
    }
    
    try {
        updateStatus('connecting', '正在连接...');
        
        // 1. 连接信令服务器
        connectSignalingServer();
        
        // 2. 获取本地媒体流
        await getLocalStream();
        
        // 3. 创建RTCPeerConnection
        createPeerConnection();
        
        startBtn.disabled = true;
        hangupBtn.disabled = false;
        
        addMessage('系统', '✅ 已加入房间:' + roomId, 'system');
        
    } catch (error) {
        console.error('启动失败:', error);
        alert('启动失败:' + error.message);
        updateStatus('disconnected', '连接失败');
    }
}

// 连接信令服务器
function connectSignalingServer() {
    // 连接到本地服务器(开发环境)
    socket = new WebSocket('ws://localhost:8080');
    
    socket.onopen = () => {
        console.log('✅ 已连接到信令服务器');
        
        // 加入房间
        socket.send(JSON.stringify({
            type: 'join',
            room: roomId
        }));
    };
    
    socket.onmessage = async (event) => {
        const message = JSON.parse(event.data);
        console.log('收到信令:', message.type);
        
        switch (message.type) {
            case 'ready':
                // 房间里有其他人了,开始创建Offer
                createOffer();
                break;
                
            case 'offer':
                // 收到Offer,创建Answer
                await handleOffer(message.offer);
                break;
                
            case 'answer':
                // 收到Answer
                await handleAnswer(message.answer);
                break;
                
            case 'ice-candidate':
                // 收到ICE候选
                await handleIceCandidate(message.candidate);
                break;
                
            case 'user-left':
                handleUserLeft();
                break;
        }
    };
    
    socket.onerror = (error) => {
        console.error('❌ WebSocket错误:', error);
        updateStatus('disconnected', '连接错误');
    };
    
    socket.onclose = () => {
        console.log('🚪 WebSocket已关闭');
        updateStatus('disconnected', '已断开');
    };
}

// 获取本地媒体流
async function getLocalStream() {
    try {
        localStream = await navigator.mediaDevices.getUserMedia({
            video: {
                width: { ideal: 1280 },
                height: { ideal: 720 }
            },
            audio: {
                echoCancellation: true,
                noiseSuppression: true,
                autoGainControl: true
            }
        });
        
        localVideo.srcObject = localStream;
        console.log('✅ 已获取本地媒体流');
        
    } catch (error) {
        console.error('❌ 获取媒体流失败:', error);
        throw new Error('无法访问摄像头或麦克风');
    }
}

// 创建RTCPeerConnection
function createPeerConnection() {
    peerConnection = new RTCPeerConnection(iceServers);
    
    // 添加本地流到连接
    localStream.getTracks().forEach(track => {
        peerConnection.addTrack(track, localStream);
    });
    
    // 创建数据通道
    dataChannel = peerConnection.createDataChannel('chat');
    setupDataChannel();
    
    // 监听远程流
    peerConnection.ontrack = (event) => {
        console.log('🎊 收到远程流!');
        if (!remoteStream) {
            remoteStream = new MediaStream();
            remoteVideo.srcObject = remoteStream;
        }
        remoteStream.addTrack(event.track);
    };
    
    // 监听ICE候选
    peerConnection.onicecandidate = (event) => {
        if (event.candidate) {
            console.log('🗺️ 发现ICE候选:', event.candidate.type);
            socket.send(JSON.stringify({
                type: 'ice-candidate',
                candidate: event.candidate,
                room: roomId
            }));
        }
    };
    
    // 监听连接状态
    peerConnection.onconnectionstatechange = () => {
        console.log('连接状态:', peerConnection.connectionState);
        
        switch (peerConnection.connectionState) {
            case 'connected':
                updateStatus('connected', '已连接 ✅');
                addMessage('系统', '🎉 连接成功!', 'system');
                break;
            case 'disconnected':
                updateStatus('disconnected', '已断开');
                break;
            case 'failed':
                updateStatus('disconnected', '连接失败');
                alert('连接失败,请重试');
                break;
        }
    };
    
    // 监听ICE连接状态
    peerConnection.oniceconnectionstatechange = () => {
        console.log('ICE状态:', peerConnection.iceConnectionState);
    };
    
    // 监听数据通道(作为接收方)
    peerConnection.ondatachannel = (event) => {
        dataChannel = event.channel;
        setupDataChannel();
    };
}

// 设置数据通道
function setupDataChannel() {
    dataChannel.onopen = () => {
        console.log('✅ 数据通道已打开');
    };
    
    dataChannel.onmessage = (event) => {
        console.log('📩 收到消息:', event.data);
        addMessage('对方', event.data, 'remote');
    };
    
    dataChannel.onclose = () => {
        console.log('📪 数据通道已关闭');
    };
}

// 创建Offer
async function createOffer() {
    try {
        console.log('🎬 创建Offer...');
        const offer = await peerConnection.createOffer();
        await peerConnection.setLocalDescription(offer);
        
        socket.send(JSON.stringify({
            type: 'offer',
            offer: offer,
            room: roomId
        }));
        
        console.log('✅ Offer已发送');
    } catch (error) {
        console.error('❌ 创建Offer失败:', error);
    }
}

// 处理Offer
async function handleOffer(offer) {
    try {
        console.log('📨 收到Offer,创建Answer...');
        await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
        
        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(answer);
        
        socket.send(JSON.stringify({
            type: 'answer',
            answer: answer,
            room: roomId
        }));
        
        console.log('✅ Answer已发送');
    } catch (error) {
        console.error('❌ 处理Offer失败:', error);
    }
}

// 处理Answer
async function handleAnswer(answer) {
    try {
        console.log('📨 收到Answer');
        await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
        console.log('✅ Answer已处理');
    } catch (error) {
        console.error('❌ 处理Answer失败:', error);
    }
}

// 处理ICE候选
async function handleIceCandidate(candidate) {
    try {
        await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
        console.log('✅ ICE候选已添加');
    } catch (error) {
        console.error('❌ 添加ICE候选失败:', error);
    }
}

// 发送消息
function sendMessage() {
    const input = document.getElementById('messageInput');
    const message = input.value.trim();
    
    if (message && dataChannel && dataChannel.readyState === 'open') {
        dataChannel.send(message);
        addMessage('你', message, 'local');
        input.value = '';
    } else if (!dataChannel || dataChannel.readyState !== 'open') {
        alert('数据通道未就绪,请等待连接建立!');
    }
}

// 添加消息到聊天框
function addMessage(sender, message, type) {
    const messageDiv = document.createElement('div');
    messageDiv.className = `message ${type}`;
    messageDiv.innerHTML = `<strong>${sender}:</strong> ${message}`;
    messagesDiv.appendChild(messageDiv);
    messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

// 处理用户离开
function handleUserLeft() {
    addMessage('系统', '😢 对方已离开', 'system');
    hangup();
}

// 挂断
function hangup() {
    if (peerConnection) {
        peerConnection.close();
        peerConnection = null;
    }
    
    if (dataChannel) {
        dataChannel.close();
        dataChannel = null;
    }
    
    if (socket) {
        socket.close();
        socket = null;
    }
    
    if (localStream) {
        localStream.getTracks().forEach(track => track.stop());
        localStream = null;
    }
    
    localVideo.srcObject = null;
    remoteVideo.srcObject = null;
    
    startBtn.disabled = false;
    hangupBtn.disabled = true;
    
    updateStatus('disconnected', '未连接');
    addMessage('系统', '📞 已挂断', 'system');
}

// 更新状态
function updateStatus(state, text) {
    statusElement.className = `status ${state}`;
    statusText.textContent = text;
}

// 监听回车发送消息
document.getElementById('messageInput').addEventListener('keypress', (e) => {
    if (e.key === 'Enter') {
        sendMessage();
    }
});

// 页面卸载时清理
window.addEventListener('beforeunload', () => {
    hangup();
});

console.log('🎉 WebRTC应用已加载!');

🖥️ server/server.js(信令服务器)

const WebSocket = require('ws');

const PORT = 8080;
const wss = new WebSocket.Server({ port: PORT });

// 存储房间信息
const rooms = new Map();

console.log(`🚀 信令服务器已启动,监听端口:${PORT}`);

wss.on('connection', (ws) => {
    console.log('✅ 新客户端已连接');
    
    let currentRoom = null;
    let clientId = generateId();
    
    ws.on('message', (data) => {
        try {
            const message = JSON.parse(data);
            console.log(`📨 收到消息:${message.type} (房间: ${message.room})`);
            
            switch (message.type) {
                case 'join':
                    handleJoin(ws, message.room, clientId);
                    currentRoom = message.room;
                    break;
                    
                case 'offer':
                case 'answer':
                case 'ice-candidate':
                    // 转发给房间内的其他人
                    broadcastToRoom(message.room, message, ws);
                    break;
            }
        } catch (error) {
            console.error('❌ 处理消息错误:', error);
        }
    });
    
    ws.on('close', () => {
        console.log('🚪 客户端已断开');
        if (currentRoom) {
            handleLeave(currentRoom, ws);
        }
    });
    
    ws.on('error', (error) => {
        console.error('❌ WebSocket错误:', error);
    });
});

// 处理加入房间
function handleJoin(ws, roomId, clientId) {
    if (!rooms.has(roomId)) {
        rooms.set(roomId, []);
    }
    
    const room = rooms.get(roomId);
    
    // 如果房间里已经有人,通知新客户端
    if (room.length > 0) {
        ws.send(JSON.stringify({ type: 'ready' }));
    }
    
    room.push({ ws, clientId });
    console.log(`✅ 客户端 ${clientId} 加入房间 ${roomId},当前人数:${room.length}`);
}

// 处理离开房间
function handleLeave(roomId, ws) {
    const room = rooms.get(roomId);
    if (!room) return;
    
    const index = room.findIndex(client => client.ws === ws);
    if (index !== -1) {
        room.splice(index, 1);
        console.log(`🚪 客户端离开房间 ${roomId},剩余人数:${room.length}`);
        
        // 通知房间内其他人
        broadcastToRoom(roomId, { type: 'user-left' }, ws);
        
        // 如果房间空了,删除房间
        if (room.length === 0) {
            rooms.delete(roomId);
            console.log(`🗑️ 房间 ${roomId} 已删除`);
        }
    }
}

// 向房间内广播消息(排除发送者)
function broadcastToRoom(roomId, message, senderWs) {
    const room = rooms.get(roomId);
    if (!room) return;
    
    room.forEach(client => {
        if (client.ws !== senderWs && client.ws.readyState === WebSocket.OPEN) {
            client.ws.send(JSON.stringify(message));
        }
    });
}

// 生成随机ID
function generateId() {
    return Math.random().toString(36).substr(2, 9);
}

// 定期清理
setInterval(() => {
    console.log(`📊 当前房间数:${rooms.size}`);
    rooms.forEach((clients, roomId) => {
        console.log(`  - 房间 ${roomId}: ${clients.length} 人`);
    });
}, 30000); // 每30秒

📦 server/package.json

{
  "name": "webrtc-signaling-server",
  "version": "1.0.0",
  "description": "WebRTC信令服务器",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "ws": "^8.14.2"
  }
}

5.4 运行步骤 🏃‍♂️

第1步:安装依赖

# 进入server目录
cd server

# 安装依赖
npm install

第2步:启动信令服务器

npm start

看到以下输出表示成功:

🚀 信令服务器已启动,监听端口:8080

第3步:打开网页

  1. 用浏览器打开 index.html
  2. 输入房间号,例如:room123
  3. 点击"🚀 开始通话"
  4. 允许摄像头和麦克风权限

第4步:测试

  1. 在另一个浏览器窗口(或另一台电脑)打开同样的页面
  2. 输入相同的房间号:room123
  3. 点击"🚀 开始通话"
  4. 等待连接建立... 🎉 成功!

5.5 测试清单 ✅

  • 能看到自己的视频
  • 能看到对方的视频
  • 能听到对方的声音
  • 能发送文字消息
  • 能接收文字消息
  • 连接状态显示正确
  • 挂断功能正常

第六章:常见问题与解决方案 🔧

问题1:看不到自己的视频 😵

可能原因:

  1. 没有授权摄像头权限
  2. 摄像头被其他程序占用
  3. 浏览器不支持

解决方案:

// 检查浏览器支持
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
    alert('你的浏览器不支持WebRTC!请使用Chrome、Firefox或Edge。');
}

// 更详细的错误处理
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
    .catch(error => {
        console.error('错误详情:', error.name, error.message);
        
        switch (error.name) {
            case 'NotAllowedError':
                alert('请允许访问摄像头和麦克风!');
                break;
            case 'NotFoundError':
                alert('未找到摄像头或麦克风设备!');
                break;
            case 'NotReadableError':
                alert('设备被占用,请关闭其他使用摄像头的程序!');
                break;
            default:
                alert('获取媒体设备失败:' + error.message);
        }
    });

问题2:连接不上对方 😢

可能原因:

  1. 信令服务器没启动
  2. 防火墙阻挡
  3. 需要TURN服务器

诊断步骤:

// 1. 检查WebSocket连接
socket.onopen = () => {
    console.log('✅ 信令服务器连接成功');
};

socket.onerror = () => {
    console.log('❌ 信令服务器连接失败!');
    alert('无法连接到服务器,请检查服务器是否启动!');
};

// 2. 监听ICE连接状态
peerConnection.oniceconnectionstatechange = () => {
    const state = peerConnection.iceConnectionState;
    console.log('ICE连接状态:', state);
    
    if (state === 'failed') {
        console.log('❌ P2P连接失败,可能需要TURN服务器!');
        alert('连接失败!如果你们不在同一网络,可能需要配置TURN服务器。');
    }
};

// 3. 查看收集到的候选
peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
        const candidate = event.candidate;
        console.log(`候选类型:${candidate.type}`);
        console.log(`候选地址:${candidate.address}:${candidate.port}`);
        console.log(`候选协议:${candidate.protocol}`);
    }
};

解决方案:添加TURN服务器

const iceServers = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        {
            urls: 'turn:openrelay.metered.ca:80',
            username: 'openrelayproject',
            credential: 'openrelayproject'
        }
    ]
};

问题3:视频卡顿或模糊 📹

原因:

  • 网络带宽不足
  • CPU占用过高
  • 编码参数不合适

优化方案:

// 1. 降低视频分辨率
const stream = await navigator.mediaDevices.getUserMedia({
    video: {
        width: { ideal: 640 },      // 降低到640x480
        height: { ideal: 480 },
        frameRate: { ideal: 15 }     // 降低帧率到15fps
    },
    audio: true
});

// 2. 调整编码参数
const sender = peerConnection.getSenders().find(s => s.track.kind === 'video');
const parameters = sender.getParameters();

if (!parameters.encodings) {
    parameters.encodings = [{}];
}

// 限制最大比特率(1Mbps)
parameters.encodings[0].maxBitrate = 1000000;

await sender.setParameters(parameters);

// 3. 监控网络质量
peerConnection.getStats().then(stats => {
    stats.forEach(report => {
        if (report.type === 'inbound-rtp' && report.kind === 'video') {
            console.log('丢包率:', report.packetsLost);
            console.log('接收字节:', report.bytesReceived);
        }
    });
});

问题4:有视频没声音 🔇

检查清单:

// 1. 检查音频轨道是否添加
localStream.getAudioTracks().forEach(track => {
    console.log('本地音频轨道:', track.label, track.enabled);
    peerConnection.addTrack(track, localStream);
});

// 2. 检查远程音频
peerConnection.ontrack = (event) => {
    console.log('收到轨道:', event.track.kind, event.track.label);
    
    if (event.track.kind === 'audio') {
        console.log('音频轨道已接收!');
    }
};

// 3. 确保video元素不是静音
remoteVideo.muted = false;

// 4. 检查音量
remoteVideo.volume = 1.0; // 最大音量

问题5:数据通道无法发送消息 💬

诊断:

// 检查数据通道状态
console.log('数据通道状态:', dataChannel.readyState);
// 可能的状态:connecting, open, closing, closed

dataChannel.onopen = () => {
    console.log('✅ 数据通道已打开,可以发送消息了!');
};

dataChannel.onerror = (error) => {
    console.error('❌ 数据通道错误:', error);
};

// 发送前检查
function sendMessage(message) {
    if (!dataChannel) {
        console.log('❌ 数据通道未创建');
        return;
    }
    
    if (dataChannel.readyState !== 'open') {
        console.log('❌ 数据通道未就绪,当前状态:', dataChannel.readyState);
        return;
    }
    
    try {
        dataChannel.send(message);
        console.log('✅ 消息已发送');
    } catch (error) {
        console.error('❌ 发送失败:', error);
    }
}

问题6:浏览器兼容性问题 🌐

检测和适配:

// 检测浏览器
function detectBrowser() {
    const ua = navigator.userAgent;
    
    if (ua.includes('Chrome')) return 'Chrome';
    if (ua.includes('Firefox')) return 'Firefox';
    if (ua.includes('Safari')) return 'Safari';
    if (ua.includes('Edge')) return 'Edge';
    
    return 'Unknown';
}

console.log('当前浏览器:', detectBrowser());

// 使用adapter.js库来处理兼容性
// <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

// 检查必要的API支持
const checkSupport = () => {
    const support = {
        getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
        RTCPeerConnection: !!window.RTCPeerConnection,
        RTCDataChannel: !!(window.RTCPeerConnection && RTCPeerConnection.prototype.createDataChannel)
    };
    
    console.log('浏览器支持情况:', support);
    
    if (!support.getUserMedia || !support.RTCPeerConnection) {
        alert('你的浏览器不完全支持WebRTC,请升级浏览器或使用Chrome!');
        return false;
    }
    
    return true;
};

第七章:WebRTC的实际应用场景 🌍

7.1 视频会议(Zoom、腾讯会议)👥

特点:

  • 多人实时视频通话
  • 屏幕共享
  • 聊天功能

技术要点:

// 屏幕共享
async function shareScreen() {
    try {
        const screenStream = await navigator.mediaDevices.getDisplayMedia({
            video: { 
                cursor: 'always'  // 显示鼠标
            },
            audio: false
        });
        
        // 替换视频轨道
        const videoTrack = screenStream.getVideoTracks()[0];
        const sender = peerConnection.getSenders().find(s => s.track.kind === 'video');
        sender.replaceTrack(videoTrack);
        
        // 停止屏幕共享时恢复摄像头
        videoTrack.onended = () => {
            const cameraTrack = localStream.getVideoTracks()[0];
            sender.replaceTrack(cameraTrack);
        };
        
    } catch (error) {
        console.error('屏幕共享失败:', error);
    }
}

7.2 在线教育(网易云课堂、学而思)📚

特点:

  • 老师讲课,学生观看
  • 举手发言
  • 白板功能

实现思路:

// 白板功能(通过Canvas + DataChannel)
const canvas = document.getElementById('whiteboard');
const ctx = canvas.getContext('2d');

let drawing = false;
let lastX = 0;
let lastY = 0;

canvas.addEventListener('mousedown', (e) => {
    drawing = true;
    [lastX, lastY] = [e.offsetX, e.offsetY];
});

canvas.addEventListener('mousemove', (e) => {
    if (!drawing) return;
    
    // 绘制
    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(e.offsetX, e.offsetY);
    ctx.stroke();
    
    // 发送绘制数据
    dataChannel.send(JSON.stringify({
        type: 'draw',
        from: [lastX, lastY],
        to: [e.offsetX, e.offsetY]
    }));
    
    [lastX, lastY] = [e.offsetX, e.offsetY];
});

// 接收并绘制
dataChannel.onmessage = (event) => {
    const data = JSON.parse(event.data);
    
    if (data.type === 'draw') {
        ctx.beginPath();
        ctx.moveTo(data.from[0], data.from[1]);
        ctx.lineTo(data.to[0], data.to[1]);
        ctx.stroke();
    }
};

7.3 在线医疗(春雨医生)🏥

特点:

  • 医生与患者远程会诊
  • 病历传输
  • 高清视频

隐私保护:

// WebRTC内置端到端加密(DTLS-SRTP)
// 但需要注意信令服务器的安全

// 使用HTTPS
const signalingServer = new WebSocket('wss://secure-server.com');

// 验证身份
socket.send(JSON.stringify({
    type: 'auth',
    token: userAuthToken
}));

// 加密聊天内容
async function encryptMessage(message, key) {
    const encoder = new TextEncoder();
    const data = encoder.encode(message);
    
    const encrypted = await crypto.subtle.encrypt(
        { name: 'AES-GCM', iv: new Uint8Array(12) },
        key,
        data
    );
    
    return encrypted;
}

7.4 实时游戏(你画我猜)🎮

特点:

  • 低延迟数据传输
  • 状态同步
// 游戏状态同步
const gameState = {
    players: [],
    currentDrawer: null,
    word: '',
    score: {}
};

// 发送游戏状态
function syncGameState() {
    dataChannel.send(JSON.stringify({
        type: 'game-state',
        state: gameState
    }));
}

// 接收并更新
dataChannel.onmessage = (event) => {
    const data = JSON.parse(event.data);
    
    if (data.type === 'game-state') {
        Object.assign(gameState, data.state);
        updateUI();
    }
};

// 每秒同步一次
setInterval(syncGameState, 1000);

7.5 文件传输(P2P下载)📁

特点:

  • 点对点传输,不经过服务器
  • 速度快
// 发送文件
async function sendFile(file) {
    const chunkSize = 16384; // 16KB
    const reader = new FileReader();
    let offset = 0;
    
    // 先发送文件信息
    dataChannel.send(JSON.stringify({
        type: 'file-meta',
        name: file.name,
        size: file.size,
        type: file.type
    }));
    
    // 分块发送
    function readNextChunk() {
        const slice = file.slice(offset, offset + chunkSize);
        reader.readAsArrayBuffer(slice);
    }
    
    reader.onload = (e) => {
        dataChannel.send(e.target.result);
        offset += e.target.result.byteLength;
        
        // 更新进度
        const progress = (offset / file.size) * 100;
        console.log(`发送进度:${progress.toFixed(2)}%`);
        
        if (offset < file.size) {
            readNextChunk();
        } else {
            console.log('✅ 文件发送完成!');
            dataChannel.send(JSON.stringify({ type: 'file-end' }));
        }
    };
    
    readNextChunk();
}

// 接收文件
let receivedChunks = [];
let fileMetadata = null;

dataChannel.onmessage = (event) => {
    if (typeof event.data === 'string') {
        const data = JSON.parse(event.data);
        
        if (data.type === 'file-meta') {
            fileMetadata = data;
            receivedChunks = [];
            console.log(`准备接收文件:${data.name}`);
        } else if (data.type === 'file-end') {
            saveFile();
        }
    } else {
        // 二进制数据
        receivedChunks.push(event.data);
        
        const received = receivedChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
        const progress = (received / fileMetadata.size) * 100;
        console.log(`接收进度:${progress.toFixed(2)}%`);
    }
};

function saveFile() {
    const blob = new Blob(receivedChunks, { type: fileMetadata.type });
    const url = URL.createObjectURL(blob);
    
    const a = document.createElement('a');
    a.href = url;
    a.download = fileMetadata.name;
    a.click();
    
    URL.revokeObjectURL(url);
    console.log('✅ 文件已保存!');
}

7.6 监控系统 📹

特点:

  • 单向视频流
  • 多路监控
// 创建单向连接(只发送视频,不接收)
async function startBroadcast() {
    const stream = await navigator.mediaDevices.getUserMedia({
        video: {
            width: 1920,
            height: 1080,
            frameRate: 30
        },
        audio: true
    });
    
    const pc = new RTCPeerConnection(iceServers);
    
    // 只添加本地流,不监听远程流
    stream.getTracks().forEach(track => {
        pc.addTrack(track, stream);
    });
    
    // 设置为单向传输
    const transceivers = pc.getTransceivers();
    transceivers.forEach(transceiver => {
        transceiver.direction = 'sendonly';  // 只发送
    });
}

// 观看端
function startWatching() {
    const pc = new RTCPeerConnection(iceServers);
    
    // 设置为只接收
    pc.addTransceiver('video', { direction: 'recvonly' });
    pc.addTransceiver('audio', { direction: 'recvonly' });
    
    pc.ontrack = (event) => {
        video.srcObject = event.streams[0];
    };
}

🎁 附录:常用资源和工具

📚 学习资源

  1. 官方文档

  2. 教程网站

  3. 开源项目

🛠️ 开发工具

  1. 调试工具

    • chrome://webrtc-internals/ (Chrome内部工具,超好用!)
    • about:webrtc (Firefox)
  2. 测试工具

  3. 免费STUN/TURN服务器

    • Google STUN: stun:stun.l.google.com:19302
    • OpenRelay TURN: turn:openrelay.metered.ca:80

📊 性能监控

// 获取详细统计信息
async function getConnectionStats() {
    const stats = await peerConnection.getStats();
    const report = {};
    
    stats.forEach(stat => {
        if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
            report.video = {
                packetsReceived: stat.packetsReceived,
                packetsLost: stat.packetsLost,
                bytesReceived: stat.bytesReceived,
                framesDecoded: stat.framesDecoded,
                frameWidth: stat.frameWidth,
                frameHeight: stat.frameHeight
            };
        }
        
        if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
            report.connection = {
                localAddress: stat.localCandidateId,
                remoteAddress: stat.remoteCandidateId,
                currentRtt: stat.currentRoundTripTime,
                availableOutgoingBitrate: stat.availableOutgoingBitrate
            };
        }
    });
    
    return report;
}

// 定期监控
setInterval(async () => {
    const stats = await getConnectionStats();
    console.log('📊 连接统计:', stats);
}, 5000);

🎉 总结

恭喜你!🎊 你已经完成了WebRTC从入门到实践的完整学习旅程!

你学到了什么?

✅ WebRTC的基本概念和工作原理 ✅ 三大核心API的使用方法 ✅ 信令服务器的实现 ✅ STUN/TURN服务器的作用 ✅ ICE协商过程 ✅ 完整的实战项目 ✅ 常见问题的解决方案 ✅ 实际应用场景

下一步?

  1. 深入学习

    • 研究音视频编解码器(VP8、H264、Opus)
    • 学习SFU(Selective Forwarding Unit)架构
    • 了解MCU(Multipoint Control Unit)架构
  2. 扩展功能

    • 实现多人视频会议
    • 添加美颜滤镜
    • 录制和回放功能
    • AI背景虚化
  3. 性能优化

    • 自适应码率控制
    • 丢包恢复机制
    • 网络质量评估

最后的话

WebRTC是一项非常强大和有趣的技术,它正在改变我们的通信方式。希望这份文档能帮助你理解并掌握WebRTC,让你在实时通信的世界里自由翱翔!🚀

记住:最好的学习方式就是动手实践! 💪

现在就打开编辑器,开始你的WebRTC之旅吧!


📧 有问题?欢迎交流!

🌟 觉得有用?点个赞吧!

💖 祝你学习愉快!


版本: 1.0.0
最后更新: 2024年10月
作者: 你的技术好伙伴
许可: MIT License

🎈🎈🎈 END 🎈🎈🎈