资讯动态

WebRTC视频通话实战:信令、媒体协商与NAT穿透全解析

发布时间:2026/9/8 7:40:29 来源:尧图企业网站定制
中秋节前后很多人会想起“回不了家”的故事。比如《熊出没》中的光头强只能在中秋节给爸妈打电话隔着屏幕听熟悉的声音。这个情节让人共情因为它对应着现实中大量异地工作者的真实状态父母在家子女在外逢年过节能做的只是拨一通电话或开一次视频。对开发者来说这样一次通话并不神秘。从软件工程角度看它本质上是一个实时音视频系统涉及音视频采集、编码、网络传输、媒体协商、渲染播放等环节。这篇文章不展开动画剧情而是以“异地亲情通话”为场景带读者从零搭建一个浏览器端的视频通话最小系统。技术栈使用 WebRTC Node.js Socket.IO。读者可以跟着操作在本地跑通双端通话然后逐步理解信令、媒体协商、NAT 穿透这些概念。学完后你会知道一次视频通话的完整链路也知道进入生产环境前还需要补哪些能力。1. 为什么“打电话”场景比普通消息系统更难做1.1 从一次远程通话看基本链路“打电话”和“发消息”的体验差距来自时延和数据模型。消息是一次性传输的小数据包发送者和接收者之间不需要保持连续通道视频通话则需要在一段时间内连续传输音视频帧并且对时延和抖动非常敏感。如果把一次视频通话拆开它的基本链路是麦克风或摄像头采集、音频或视频编码、打包为 RTP然后通过网络传输接收端解包、解码、渲染到扬声器和屏幕。麦克风/摄像头采集 - 音频/视频编码 - 打包为 RTP - 网络传输 - 接收端解包 - 解码 - 扬声器/屏幕播放在 WebRTC 中这个链路的大部分细节被封装在浏览器内部开发者主要接触的是getUserMedia、RTCPeerConnection和音视频标签。但这不代表可以忽略底层机制。浏览器只负责实现不会替开发者解决业务层的房间管理、成员加入、断线重连和鉴权问题。发送端和接收端必须先知道对方的网络地址和媒体参数才能开始通话。这就像两个人打电话之前先需要知道对方的号码和约定的通讯方式。WebRTC 用 SDP 描述媒体参数用 ICE 候选描述网络路径而这两类信息都需要经过信令通道传递。信令通道不搬运音视频数据它只负责“帮双方互相认识”。1.2 实时通话系统的核心模块一个完整的视频通话系统可以拆成六个模块模块作用浏览器 WebRTC 中的对应能力采集拾取摄像头和麦克风数据getUserMedia编码压缩音视频数据降低带宽占用VP8/VP9/H.264、Opus传输低时延传送媒体包RTP、SRTP、RTCP穿透解决 NAT 和防火墙限制STUN/TURN、ICE信令交换 SDP 和 ICE 候选WebSocket/Socket.IO 自行实现渲染播放远端音视频video/audio 标签每个模块都可能成为问题点。采集阶段用户可能拒绝授权或设备被占用。编码阶段浏览器虽然会自动选择编码器但不同硬件和浏览器组合可能产生兼容性问题。传输阶段网络丢包、抖动、带宽不足会导致卡顿。穿透阶段NAT 类型复杂时需要中继服务。信令阶段房间管理和事件时序需要自己实现。渲染阶段还要处理muted、playsinline和自动播放策略。新手最容易忽略的是信令。很多人以为 WebRTC 会自动完成连接其实浏览器只负责生成 SDP 和 ICE 候选真正把它们送到对方手里的是自己的代码。Demo 范围虽然小但信令消息的可靠性和顺序仍然会影响连接成功率。2. 先理解 WebRTC 的四个关键机制在动代码之前需要把握四个机制否则之后遇到的每个问题都会变成“玄学”。2.1 P2P 连接与 NAT 穿透WebRTC 的特点是尽量建立点对点连接媒体数据不经过中心服务器。这样做的好处是时延低、带宽成本低。但在真实网络中两台设备各自位于家庭路由或企业网关后可能没有公网 IP直接发给对端很难成功。STUN 服务器解决“出口地址是什么”的问题。设备向 STUN 服务器发一个请求服务器返回“从公网看到你的地址是某个 IP:端口”。这个地址会被收集为 ICE 候选。但仅知道出口地址还不够部分 NAT 不允许外部主动向内发起连接所以 WebRTC 还要通过 ICE 协议组合多种候选并尝试不同路径。ICE 候选类型主要有三种候选类型获取方式适用范围host本机网卡地址局域网或同网段设备srflxSTUN 反射地址常见 NAT 后设备可被对端访问relayTURN 中继地址P2P 全部失败时的兜底方案relay候选会消耗中继服务器的带宽和流量成本不能忽略。生产环境需要评估中继用量并按地域部署多个节点。2.2 SDP 媒体协商两端要正常通话必须对“用什么编解码器、分辨率、端口、传输方向”达成一致。这个描述放在 SDP 中。发起方生成 offer接收方根据 offer 和自己的情况生成 answer。简化后的 SDP 片段如下v0 o- 4611731404012334688 2 IN IP4 127.0.0.1 s- t0 0 maudio 49170 RTP/AVP 111 artpmap:111 opus/48000/2 mvideo 51372 RTP/AVP 96 artpmap:96 VP8/90000真实 SDP 会比这个复杂得多里面包含编码器优先级、网络地址、带宽限制、传输方向等大量字段。开发者不需要手工拼接 SDP浏览器通过createOffer和createAnswer自动生成。关键是要用setLocalDescription和setRemoteDescription正确管理本地和远端状态。2.3 ICE Candidate 与 STUN/TURNICE 是浏览器自动完成的“选路”过程。RTCPeerConnection创建后会开始收集候选每次发现新候选都会触发onicecandidate回调。开发者拿到候选后要通过信令通道发给对端。对端调用addIceCandidate后两端再尝试寻找可用路径。可以通过connectionState观察连接状态。正常状态会经历new - checking - connected - completed如果看到failed说明没有候选路径可用通常需要补充 TURN。排错时也可以打开chrome://webrtc-internals查看候选对和连接状态。2.4 信令服务为什么不属于 WebRTC 标准WebRTC 本身不定义信令协议因为信令的业务属性太强需要房间系统、鉴权、自动扩容不同产品差异很大。WebRTC 只规定了 offer/answer 如何描述不规定它们如何传输。所以信令部分要用 WebSocket、Socket.IO 或普通 HTTP 自己实现。本文选择 Socket.IO因为它内置房间、事件广播和断线检测写 Demo 很方便。但在生产环境信令服务需要考虑消息丢失、重复、顺序、多节点房间同步和权限控制等问题。信令消息可以按以下格式设计事件发送方向负载触发时机join-room客户端 - 服务端roomName打开画面并准备通话joined服务端 - 客户端peerCount加入房间成功peer-joined服务端 - 原房间客户端无有第二个人加入offer客户端 - 服务端 - 对端description发起方生成 offeranswer对端 - 服务端 - 发起方description接收方生成 answerice-candidate客户端 - 服务端 - 对端candidate本地发现新候选peer-disconnected服务端 - 房间内其他客户端无对端离开或断线这张表在开发信令时非常有用可以先定消息类型再写具体实现。3. 环境准备与依赖安装3.1 技术选型这里选择 Node.js Socket.IO WebRTC。浏览器原生支持 WebRTC不需要额外安装音视频 SDK。Node.js 生态写信令服务最简单一台服务器可以同时托管静态资源和 Socket.IO。Socket.IO 自带房间和断线检测适合快速搭出可用最小系统。这是一个学习成本低、验证速度快的组合。如果已经有业务后端可以把 Socket.IO 挂到现有 HTTP 服务上也可以独立部署信令服务。环境要求如下项目要求Node.js14 或更高版本建议使用 LTS 版本浏览器Chrome/Edge 最新版需支持 WebRTC硬件可用摄像头、麦克风网络本地测试可在 localhost跨网测试需要 STUN/TURN先检查本机 Node.js 和 npm 是否可用node -v npm -v如果命令不存在需要先安装 Node.js。建议使用 LTS 版本避免新版本带来未知兼容问题。3.2 项目目录结构创建项目目录并规划文件位置webrtc-family-call/ ├── package.json ├── server.js └── public/ ├── index.html └── main.jsserver.js是 Node.js 信令服务入口。public目录存放前端静态页面。index.html是页面骨架。main.js是前端 WebRTC 逻辑。这样划分后前端和后端在同一个项目里部署时一条命令启动即可。后续如果信令服务需要扩容可以把public静态资源挂到 Nginx 或 CDNserver.js单独作为 Socket.IO 服务。3.3 初始化项目与安装依赖在项目目录下执行mkdir webrtc-family-call cd webrtc-family-call npm init -y npm install express socket.ionpm init -y会生成一个默认package.json。接着安装express和socket.io。express用来托管前端静态页面socket.io用来建立信令通道。给package.json添加启动脚本方便以后用npm start启动scripts: { start: node server.js }安装完成后项目内会生成node_modules和package-lock.json。package-lock.json建议提交到版本库保证团队成员安装的依赖版本一致。4. 实现最小视频通话系统4.1 后端信令服务器代码最小信令服务只需要做四件事管理房间、转发 offer/answer、转发 ICE candidate、通知成员离开。由于 Demo 限定一个房间最多两个人所以不需要复杂的房间状态机。在项目根目录创建server.jsconst express require(express); const http require(http); const { Server } require(socket.io); const app express(); const server http.createServer(app); const io new Server(server); app.use(express.static(public)); io.on(connection, (socket) { socket.on(join-room, (roomName) { const room io.sockets.adapter.rooms.get(roomName); const size room ? room.size : 0; if (size 2) { socket.emit(room-full); return; } socket.join(roomName); if (size 0) { socket.emit(joined, { peerCount: 1 }); } else if (size 1) { socket.to(roomName).emit(peer-joined); socket.emit(joined, { peerCount: 2 }); } }); socket.on(offer, (data) { socket.to(data.room).emit(offer, data); }); socket.on(answer, (data) { socket.to(data.room).emit(answer, data); }); socket.on(ice-candidate, (data) { socket.to(data.room).emit(ice-candidate, data); }); socket.on(leave-room, (roomName) { socket.to(roomName).emit(peer-disconnected); socket.leave(roomName); }); socket.on(disconnecting, () { const rooms [...socket.rooms].filter((r) r ! socket.id); rooms.forEach((room) { socket.to(room).emit(peer-disconnected); }); }); }); const PORT process.env.PORT || 3000; server.listen(PORT, () { console.log(Server listening on http://localhost:${PORT}); });关键点用io.sockets.adapter.rooms.get(roomName)获取房间当前人数避免第三个用户加入后产生状态混乱。第一个用户收到joined且peerCount为 1此时只创建连接对象等待对端。第二个用户加入时服务器先向旧成员发送peer-joined再向新成员发送joined且peerCount为 2。客户端约定peerCount为 2 的成员发起 offer。socket.to(data.room)会把消息发给房间内除自己外的成员。Demo 最多两人语义刚好。使用disconnecting事件而不是disconnect因为在disconnect阶段 socket 已经离开所有房间拿不到房间信息。4.2 前端页面与基础 UI创建public/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / title异地亲情视频通话 Demo/title style body { font-family: Arial, sans-serif; margin: 20px; } video { width: 360px; height: 270px; border: 1px solid #ddd; background: #f5f5f5; } .controls { margin-bottom: 12px; } .controls input { padding: 6px; } .controls button { padding: 6px 12px; } /style /head body h1异地亲情视频通话 Demo/h1 div classcontrols input idroomName placeholder请输入房间号 / button idjoinBtn加入房间/button button idhangupBtn挂断/button /div div video idlocalVideo autoplay muted playsinline/video video idremoteVideo autoplay playsinline/video /div script src/socket.io/socket.io.js/script script src/main.js/script /body /html本地 video 标签加muted是为了避免自己的声音被扬声器播放后又传回麦克风形成回声。playsinline对 iOS Safari 很重要否则视频可能会全屏播放。4.3 前端 WebRTC 逻辑创建public/main.jsconst socket io(); let localStream null; let peerConnection null; let roomName ; let isInitiator false; const localVideo document.getElementById(localVideo); const remoteVideo document.getElementById(remoteVideo); const roomInput document.getElementById(roomName); const joinBtn document.getElementById(joinBtn); const hangupBtn document.getElementById(hangupBtn); const iceServers [ { urls: stun:stun.l.google.com:19302 } ]; joinBtn.addEventListener(click, async () { roomName roomInput.value.trim(); if (!roomName) return; localStream await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject localStream; socket.emit(join-room, roomName); }); hangupBtn.addEventListener(click, () { if (peerConnection) { peerConnection.close(); peerConnection null; } remoteVideo.srcObject null; socket.emit(leave-room, roomName); }); socket.on(joined, async ({ peerCount }) { await createPeerConnection(); if (peerCount 2) { isInitiator true; const offer await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); socket.emit(offer, { room: roomName, description: offer }); } }); socket.on(peer-joined, async () { await createPeerConnection(); }); socket.on(offer, async ({ description }) { await createPeerConnection(); await peerConnection.setRemoteDescription(description); const answer await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); socket.emit(answer, { room: roomName, description: answer }); }); socket.on(answer, async ({ description }) { await peerConnection.setRemoteDescription(description); }); socket.on(ice-candidate, async ({ candidate }) { if (peerConnection) { await peerConnection.addIceCandidate(candidate); } }); socket.on(room-full, () { alert(房间人数已满请输入其他房间号); }); socket.on(peer-disconnected, () { if (peerConnection) { peerConnection.close(); peerConnection null; } remoteVideo.srcObject null; }); async function createPeerConnection() { if (peerConnection) return; peerConnection new RTCPeerConnection({ iceServers }); if (localStream) { localStream.getTracks().forEach((track) { peerConnection.addTrack(track, localStream); }); } peerConnection.onicecandidate (event) { if (event.candidate) { socket.emit(ice-candidate, { room: roomName, candidate: event.candidate }); } }; peerConnection.ontrack (event) { if (event.streams event.streams[0]) { remoteVideo.srcObject event.streams[0]; } }; peerConnection.onconnectionstatechange () { console.log(ICE connection state:, peerConnection.connectionState); }; }这段代码的流程用户输入房间号并点击加入。浏览器请求摄像头和麦克风权限获得本地媒体流。客户端向服务器发送join-room。服务器返回joined。如果是第二个用户会收到peerCount: 2于是创建 offer 并发送给先加入者。先加入者收到offer后创建 answer 回复。双方通过ice-candidate交换候选地址。媒体流通过 RTCPeerConnection 建立远端画面出现在remoteVideo。这里一个容易踩的坑是createPeerConnection在peer-joined和joined中都可能被调用所以函数开头要做空判断避免重复创建。4.4 房间和信令消息设计在 Coding 之前先定义事件会让通信逻辑更清晰。上面的实现中房间号作为消息路由的维度同一个房间内的事件只转发给房间内其他成员。Socket.IO 的 room 机制只在服务器内存中。服务器重启后房间状态会丢失。多机部署时需要把房间信息同步到 Redis 或使用独立的状态服务。这个复杂度不是 Demo 需要的但上线前必须考虑。Demo 限定一个房间两个人是因为 mesh 模式下每增加一个成员端上的peerConnection数量会同步增加。三人通话时每个端点要维护两条连接六人以上时带宽和 CPU 压力会非常明显。多人通话应该考虑 SFU 架构而不是在客户端无限循环peerConnection。5. 运行验证与效果确认5.1 启动服务在项目根目录执行npm start启动成功后终端会出现Server listening on http://localhost:3000如果端口被占用可以用环境变量指定其他端口PORT3001 npm start5.2 双标签页模拟双人通话打开两个 Chrome 标签页访问http://localhost:3000。第一个标签页输入房间名family点击“加入房间”。浏览器会弹出摄像头和麦克风权限请求选择允许。页面出现本地预览画面。第二个标签页输入相同房间名family点击“加入房间”。两个标签页应能看到对方的画面。本地 video 标签加了muted所以本地预览不会产生回声。远端 video 没有muted当对端说话时能听到声音。这种方式虽然是同一台设备上的两个标签页但 WebRTC 仍然会走完整的信令、ICE 候选和媒体连接流程适合先确认代码逻辑是否正确。它不能验证跨公网但足以排查 90% 的代码问题。5.3 预期输出与日志打开 DevTools 控制台可以看到类似输出ICE connection state: new ICE connection state: checking ICE connection state: connected如果看到failed说明候选路径没有打通。优先检查 STUN 服务器是否能访问以及两个页面是否真的加入了同一个房间。还可以打开chrome://webrtc-internals查看RTCIceCandidatePair的状态、音视频包的发送和接收情况。这是排查 WebRTC 问题最直接的工具。5.4 局域网真机测试要改的地址本地标签页跑通后可以用手机和电脑测试。先查看电脑局域网 IPipconfigLinux/macOS 使用ifconfig或ip addr。假设电脑 IP 是192.168.1.10手机访问http://192.168.1.10:3000这里有一个限制除了localhost浏览器通常将局域网 IP 视为非安全上下文getUserMedia会被拒绝。所以要真机测试最好通过 HTTPS 访问。临时测试可以在电脑上使用本地代理或其他方案但生产环境必须配置 HTTPS不能绕过权限限制。注意非 localhost 环境下摄像头权限受浏览器安全上下文限制。真机测试建议直接部署 HTTPS或用 CDN/网关统一接证书。6. 常见问题与排查链路6.1 本地摄像头无法打开现象点击“加入房间”后没有出现本地视频控制台报NotAllowedError或NotFoundError。排查顺序检查浏览器地址栏左侧是否允许摄像头和麦克风权限。打开浏览器设置里的摄像头权限看当前站点是否被阻止。检查系统是否已有其他应用占用摄像头。使用 HTTPS 或localhost访问页面排除安全上下文问题。打开chrome://settings/content/camera检查网站权限。如果报NotFoundError说明系统里没有可用的摄像头设备。如果报NotAllowedError通常是用户拒绝了权限或浏览器策略阻止了页面。6.2 信令能通但视频黑屏现象两个页面都加入了房间信令日志没报错但远端画面一直黑屏。可能原因SDP 没有正确到达对端。对端没有添加本地媒体流轨道。ICE 连接未成功。ontrack未触发或remoteVideo.srcObject没有被赋值。浏览器自动播放策略阻止了带声音的视频。检查方式在控制台打印peerConnection.connectionState确认状态为connected。在ontrack回调里打印日志确认触发了。打开chrome://webrtc-internals观察RTCInboundRtpVideoStream是否持续收到包。用getStats读取收发字节数确认媒体数据是否流动。下面是简易诊断代码setInterval(async () { if (!peerConnection) return; const stats await peerConnection.getStats(); stats.forEach((report) { console.log(report.type, report.id, report.state || report.bytesSent || ); }); }, 3000);6.3 不同网络无法互联现象两台设备在同一个局域网可以通话但处于不同宽带或移动网络时黑屏。原因NAT 类型或防火墙不允许 P2P 数据包直接通过ICE 选不到可用路径但当前没有配置 TURN。处理方式部署一个可用的 TURN 服务并在前端iceServers中配置。如果使用云厂商申请专门的媒体服务按需计费。增加 TURN 后connectionState应能进入connected或completed。TURN 配置示例const iceServers [ { urls: stun:stun.example.com:3478 }, { urls: turn:turn.example.com:3478, username: 你的用户名, credential: 你的密码 } ];TURN 账号不能硬编码在前端。生产环境应通过接口下发短期凭据防止账号被盗用后产生流量费用。6.4 回声和音量问题现象本地扬声器播放远端声音后麦克风又把声音录回去对方听到自己的回声。处理建议本地 video 标签保持muted。使用耳机避免外放导致回声。打开浏览器的回声消除和自动增益设置。不要用AudioContext随意处理麦克风流容易破坏默认的echoCancellation。音量问题通常可以通过getUserMedia的音频约束和audio标签音量属性调整。生产环境建议提供音量自测页面让用户先确认输入输出设备正常。7. 生产环境部署前必须补充的能力7.1 认证与房间管理Demo 中任何人只要知道房间名就可以加入。生产环境必须增加用户登录和房间访问控制。常见做法是用户通过业务系统获得临时 token。信令服务校验 token并绑定用户 ID。加入房间前检查用户是否有该房间的访问权限。记录每一次加入、离开、通话时长的日志。信令服务不能直接暴露给公网建议放在网关后面只允许登录用户连接。否则可能被非法用户恶意填充房间干扰在线通话。7.2 TURN 集群与就近接入跨网通话能否稳定很大程度取决于 TURN 服务质量。单台 TURN 无法支撑大量媒体流量生产环境需要按地域部署多个 TURN 节点。通过 DNS 解析或云负载均衡把用户调度到最近节点。提前规划带宽预算因为中继流量会占用大量服务器带宽。监控中继用户数、吞吐量、UDP/TCP 连接数。当 TURN 节点接近瓶颈时要及时扩容或设置调度阈值避免媒体连接不稳定。7.3 录音、录制、转写与合规如果产品涉及通话录音、录制或语音转写必须在业务上明确告知用户并获得同意。录音文件要加密存储设置访问权限和保留过期时间。不能把 Demo 中的简单回调逻辑直接搬到生产环境也不能在没有安全措施的情况下保存音视频文件。合规不是一句口号而是要在产品里落实到隐私政策、操作记录、数据生命周期管理和权限审计。7.4 监控与日志实时通话的故障通常与网络强相关生产环境至少记录加入房间成功率。join-room、offer、answer、ice-candidate消息数量。ICE 连接失败率。通话时长、平均码率、分辨率。断线重连次数。日志建议输出为结构化 JSON接入 Elasticsearch、Loki 等系统再配置告警。只有通过指标监控才能提前发现网络质量恶化或信令服务异常。7.5 资源成本与人数上限Demo 是两人 mesh 通话媒体流点对点传输。如果增加到三个人每端需要同时维护两条peerConnection上行带宽会翻倍。人数到 6 人以上客户端 CPU、带宽和网络都会明显吃力。常见视频会议架构对比如下架构媒体转发方式适合人数客户端压力服务端成本mesh端点之间直接互连2-5高低SFU服务端选择性转发10-100中中MCU服务端合流后发送更多低高生产环境建议使用 SFU 架构。常见开源方案包括 mediasoup、LiveKit、Janus也可以使用商业云服务。选型时需要考虑容量、价格、部署难度和浏览器兼容性。8. 从 Demo 到产品的实践建议最佳实践8.1 不要一开始就自研信令协议自研信令很容易在房间状态一致性、消息去重、服务端重启恢复上出问题。如果只是个人学习可以按本文的方式写一遍。如果要上线建议先使用成熟 SDK 或开源组件等业务规模稳定后再考虑自研。自研之前先回答清楚是否需要多人会议、是否需要录制、是否需要服务端转码。8.2 媒体流与数据通道分开处理WebRTC 还有 DataChannel可以用来传文件、聊天消息、演示内容。不要把业务数据塞进音视频轨道。DataChannel 要单独建立并注意小包并发性能。聊天和信令消息尽量走自己的业务通道不要和 WebRTC 的媒体状态混在一起。8.3 移动端和弱网要单独做降级策略移动端摄像头、网络和 CPU 差异大需要做好码率自适应。弱网时可以先降低视频分辨率再关闭视频最后只保留音频。WebRTC 支持对发送参数进行动态调整可以通过getStats观察丢包率然后降编码码率。不同场景的建议策略场景建议策略弱网降低视频码率优先保证音频通话CPU 占用高降低分辨率或关闭视频移动端使用 H.264 编码兼容性更好大房间开启 Simulcast让服务端选择合适视频层8.4 可复用检查清单上线一个音视频功能前可以按这个清单逐项检查是否在 HTTPS 环境下提供页面。是否配置了 STUN 和 TURNTURN 是否具备高可用。是否在客户端监听connectionState并提示用户网络异常。是否处理浏览器权限拒绝和设备不存在。是否包含信令服务重连和房间状态恢复。是否对房间加入做了认证和人数限制。是否记录通话质量日志能否追踪一次失败通话。是否明确录制和隐私规则。是否做好回滚方案信令服务升级不能影响在途通话。这个清单可以直接放到团队发布检查流程里。每次改动音视频相关代码前至少执行一遍能减少很多线上问题。中秋节光头强给爸妈打一通电话看起来是一次简单的情感连接但背后是采集、编码、网络传输、媒体协商、信令交互一系列技术环节共同工作。本文从一个最小视频通话 Demo 入手把这条链路从黑盒变成了可以调试的代码。看完之后建议先在本机把两个标签页跑通再慢慢换成 HTTPS、增加 TURN、接入房间鉴权。后面每加一层能力都会更接近一个可上线的音视频产品。对新手而言最重要的不是背熟 API而是遇到黑屏或连接失败时能顺着 ICE、SDP、信令这条链路找到问题到底出在哪个环节。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价