十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从“远程电话”到WebRTC:一文讲透实时音视频通话原理与实现

从“远程电话”到WebRTC:一文讲透实时音视频通话原理与实现 在做技术分享的时候我很少遇到一个标题能像“小孩用话费打个远程电话就以为自己老爱国了”这样让我愣一下的。初看是个段子再看是个技术话题细想其实是很多开发者对通信技术认知边界的一个缩影——电话能打通背后的链路并不简单而我们现在习以为常的“免费通话”更不是天上掉下来的。这篇博客我想换个聊法从这段网络热梗切入讲清楚远程通话到底是怎么实现的传统电话和互联网通话的差异在哪以及开发者如果想从零搭建一个 WebRTC 通话 Demo需要掌握哪些关键环节。过程中我会给出可直接复制的代码、运行步骤和排错清单。不管你是刚接触音视频开发还是在做客服系统、在线教育、远程问诊这篇文章都值得收藏。1. 这个标题背后藏着一个技术认知断层先把这个梗拆开看。有人调侃“小孩用话费打个远程电话就以为自己老爱国了”这个段子的笑点在于孩子觉得自己通过一通电话完成了一件很“宏大”的事情但在成年人眼里这只是日常通信。如果只看表面这确实是个单纯的搞笑笑点但如果从技术角度去拆解“远程电话”四个字背后藏着一套极其复杂的通信体系而大多数普通人——包括不少刚转行做开发的同学——对它的认知是断层的。这里真正值得展开的是为什么传统电话要收话费而微信语音、钉钉通话、Zoom 会议这些互联网通话却能“免费”同样是“远程通话”一个是电信运营商提供的电路交换服务走的是公网电话网络按分钟计费另一个是构建在 IP 网络之上的音视频传输走的是宽带或移动数据运营成本主要由流量费承载软件厂商通过会员、企业授权、广告等方式变现。两者之间的区别不是“免费”和“收费”这么简单而是整个网络架构、服务模型、质量保障体系都不同。对开发者来说理解这个断层很重要。因为很多人在做音视频功能时会下意识地用“打电话”的思维去套 WebRTC比如关心“通话质量好不好”却说不清影响质量的因素关心“为什么连不上”却不知道信令服务器和媒体服务器是两回事。用传统电话的认知去理解现代实时通信是很多 demo 做不出来、生产环境经常出问题的根源。这篇文章要解决的就是这件事帮你把远程通话这条链路完整地捋一遍然后用一个 WebRTC 一对一通话的完整示例走通环境搭建、代码实现、运行验证和问题排查四个环节。2. 从“打电话”到“网络通话”通信技术演进的关键节点2.1 PSTN 电话为什么它“贵”传统电话的网络基础是 PSTNPublic Switched Telephone Network公共交换电话网络也就是我们常说的固定电话网络和移动通信网络的核心部分。它的特点是为语音通话专门设计采用电路交换Circuit Switching方式通话开始前网络在通话双方之间建立一条独占的物理通路这条通路在整个通话期间被占用其他人无法使用。这个设计带来的优点是质量稳定、时延低、没有抖动——因为通路是独占的。缺点也非常明显即使双方都没说话这条通道的资源也在被占用所以它天然适合按时间计费的模式。这也解释了为什么传统电话要“花话费”你购买的不是“信息量”而是“通道占用时长”。2.2 VoIP把语音变成数据包VoIPVoice over Internet Protocol互联网语音传输协议改变了思路。它不建立独占电路而是把语音信号数字化、压缩、打包然后通过 IP 网络尽力传输。接收端收到数据包后再解包、解码、还原成语音。因为 IP 网络是共享的VoIP 的带宽利用率远高于 PSTN成本也大幅下降。代表性产品包括 Skype、企业 IP 话机、运营商内部承载网等。VoIP 的关键技术包括编码器Codec、RTP 协议实时传输协议和 QoS服务质量保证。质量上VoIP 很难达到传统电话的绝对稳定但经过充分优化的专网 VoIP 已经非常接近。这里要澄清一个容易混淆的问题现代手机上的“VoLTE”通话并不是传统的电路交换。它是运营商把语音封装成数据包在 LTE/5G 网络内以 IP 方式传输由运营商网络内部的 QoS 机制保证质量。所以用户感知上还是“打电话”而且质量更好这是电信网络自身演进的结果和互联网 App 的 VoIP 不是一回事。2.3 WebRTC浏览器原生实时通信如果说 VoIP 是“用 IP 网络打电话”那 WebRTCWeb Real-Time Communication就是“让浏览器和移动 App 之间直接进行实时通信”。它的一大特点是不需要安装任何插件或额外客户端只要浏览器支持 WebRTC 标准就能完成音视频通话、数据共享。它的核心能力包括音视频采集、编码、网络传输、回声消除、噪音抑制等。WebRTC 的关键在于它把复杂的音视频引擎做成了浏览器内置能力开发者只需要处理信令、连接管理和业务逻辑。但这不等于说开发门槛极低。真正容易出问题的恰好是连接建立、NAT 穿透、媒体协商这些“藏在底层”的部分。下表对比三种通信方式的差异维度PSTN 传统电话VoIPWebRTC网络基础电路交换电话网IP 网络IP 网络计费方式按分钟计费通常按流量/套餐软件层免费流量自付音质保证独占通路质量稳定依赖带宽和 QoS依赖网络有自适应机制接入方式电话终端软终端/App/话机浏览器/App开发门槛不开放给普通开发者依赖 SIP/H.323 等协议浏览器原生 API适合 Web 工程师从应用场景看WebRTC 是目前对开发者最友好、上手最快、生态最开放的方案。如果你现在要新做一套音视频通话功能WebRTC 基本是第一选择。3. WebRTC 核心原理信令、SDP 与 NAT 穿透3.1 信令通话的“握手协议”很多人第一次写 WebRTC 时以为只要两端都拿到对方的媒体流通信就建立了。实际上WebRTC 本身不定义信令协议信令的传输方式由开发者自己决定。这是新手最容易理解偏差的地方。信令Signaling要完成的任务包括协商媒体格式双方支持哪些编码器、分辨率、帧率。交换网络信息双方的 IP 地址、端口。控制会话状态发起通话、振铃、接听、挂断。在 WebRTC 中双方通过交换 SDPSession Description Protocol会话描述协议来协商媒体格式。SDP 是一段包含音视频编码、传输地址、安全信息等内容的文本。发起方创建 Offer接收方回复 Answer这就是一次标准的媒体协商。关键点信令服务器可以用 WebSocket、HTTP 轮询、甚至邮件来传只要能传递 SDP 和 ICE 候选信息即可。在实际项目中比较常见的是用 WebSocket 做实时信令通道因为信令本身强调低延迟。3.2 ICE 与 NAT 穿透为什么局域网能通公网连不上真实网络环境中通信双方通常处在 NAT网络地址转换设备之后没有公网 IP。要让两端直接建立媒体连接必须做 NAT 穿透。WebRTC 使用 ICEInteractive Connectivity Establishment交互式连接建立框架来完成这件事。ICE 的流程大致是本地收集所有可能的传输候选地址包括局域网地址、NAT 映射后的公网地址、由 TURN 服务器分配的中继地址。通过信令通道把候选地址发给对端。双方尝试用各自的候选地址组合进行连通性检测找到可用的路径。选定优先级最高的可用路径开始媒体传输。这里涉及两个基础设施STUN 服务器帮助客户端发现自己在 NAT 之后的公网地址和端口。TURN 服务器在直接连接失败时以中继方式转发媒体数据。新手最容易踩的坑是在本地开发时一切正常部署到公网后两边连不上。原因多半是缺少可用的 TURN 服务器或者 STUN/TURN 配置错误。为什么 因为本地开发时两端可能在同一局域网ICE 直接找到了内网候选地址压根没走公网而真正跨网络通信时如果没有 TURN 转发就只能在直连失败后等待超时。场景是否需要在公网部署 TURN本地 localhost 调试否同一局域网两台设备互连否跨公网两个不同网络是一方在企业防火墙后强烈建议3.3 媒体传输RTP 与 SRTP媒体协商完成后WebRTC 通过 SRTPSecure Real-time Transport Protocol传输音视频数据。SRTP 在 RTP 基础上增加了加密能力保证媒体内容不被窃听或篡改。这是 WebRTC 安全性的重要组成部分也是它区别于传统 RTMP 直播方案的地方。作为应用层开发者你通常不需要直接处理 RTP 包但理解“媒体流是端到端加密的”这一点有助于回答产品、法务、客户关于通话隐私的提问。4. 环境准备与基础配置下面进入实操部分。我们将用最简单的 Node.js 浏览器实现一个一对一 WebRTC 视频通话 Demo。示例包含一个静态信令服务器和两个网页客户端核心代码量不大但覆盖了信令交换、媒体协商、NAT 穿透三个关键环节。4.1 软件环境本文 Demo 的运行环境相对轻量组件说明Node.js需要支持 ES Modules 或 CommonJS版本建议使用 LTS 版本具体以实际安装为准浏览器Chrome、Edge、Firefox 最新版均可需支持 WebRTC API操作系统Windows / macOS / Linux 均可网络方面本地 Demo 建议使用两台设备模拟“两端”可以是一台电脑的两个浏览器标签页也可以是同一局域网内的电脑和手机。如果两台设备不在同一网络需要部署 TURN 服务器我们会在第 8 节给出生产环境建议。4.2 初始化项目先创建项目目录并初始化 npm 工程mkdir webrtc-call-demo cd webrtc-call-demo npm init -y安装两个依赖npm install express wsexpress提供静态文件服务和简化的 HTTP 接口。ws提供 WebSocket 服务端实现用于信令通道。安装完成后目录结构如下webrtc-call-demo/ ├── node_modules/ ├── package.json ├── server.js └── public/ ├── index.html └── client.js5. 完整示例WebRTC 一对一通话代码实现5.1 信令服务器实现文件路径server.jsconst path require(path); const express require(express); const WebSocket require(ws); const app express(); const PORT 3000; // 提供静态页面 app.use(express.static(path.join(__dirname, public))); const server app.listen(PORT, () { console.log(WebRTC demo server running at http://localhost:${PORT}); }); // 创建 WebSocket 信令服务器 const wss new WebSocket.Server({ server }); // 用一个 Map 保存所有连接key 为会话 id const clients new Map(); function broadcast(senderId, message) { const data JSON.stringify({ senderId, ...message }); clients.forEach((client, id) { if (id ! senderId client.readyState WebSocket.OPEN) { client.send(data); } }); } wss.on(connection, (socket) { // 每个连接进入时分配一个自增 id const id client-${Date.now()}-${Math.random().toString(36).slice(2, 8)}; clients.set(id, socket); socket.send(JSON.stringify({ type: registered, id })); socket.on(message, (raw) { const message JSON.parse(raw.toString()); if (message.type offer || message.type answer || message.type candidate) { // 把信令转发给其他所有客户端 broadcast(id, message); } }); socket.on(close, () { clients.delete(id); }); });这段代码的核心逻辑是当某个客户端发送offer、answer、candidate类型消息时服务器把它转发给除发送方之外的所有客户端。在实际一对一场景中你可以用房间 ID 精确转发在这个 Demo 里我们用广播模拟双方消息互通。注意生产环境不能这样直接广播必须实现房间管理和目标定向转发否则会造成消息串线。5.2 页面结构文件路径public/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleWebRTC 一对一视频通话 Demo/title style body { font-family: Arial, sans-serif; max-width: 960px; margin: 40px auto; padding: 0 20px; } video { width: 320px; height: 240px; border: 1px solid #ccc; background: #f5f5f5; margin: 8px; } .row { display: flex; flex-wrap: wrap; margin: 16px 0; } button { margin-right: 8px; padding: 8px 16px; cursor: pointer; } /style /head body h1WebRTC 一对一视频通话 Demo/h1 div p本端 IDspan idlocalId初始化中.../span/p /div div classrow div p本地画面/p video idlocalVideo autoplay muted playsinline/video /div div p远端画面/p video idremoteVideo autoplay playsinline/video /div /div div classrow button idstartBtn开始采集/button button idcallBtn发起通话/button button idhangupBtn挂断/button /div div h3信令日志/h3 pre idlog等待操作.../pre /div script src/client.js/script /body /html5.3 客户端逻辑文件路径public/client.jslet localStream null; let peerConnection null; let localId null; const ws new WebSocket(ws://${location.host}); const logEl document.getElementById(log); const localVideo document.getElementById(localVideo); const remoteVideo document.getElementById(remoteVideo); function log(message) { logEl.innerHTML ${new Date().toLocaleTimeString()} - ${message}\n logEl.innerHTML; } // 创建 RTCPeerConnection // 在公网场景下需要把 stun/turn 配置补充到 iceServers 中 const rtcConfig { iceServers: [ { urls: stun:stun.l.google.com:19302 } ] }; ws.onmessage async (event) { const msg JSON.parse(event.data); switch (msg.type) { case registered: localId msg.id; document.getElementById(localId).textContent localId; log(已连接信令服务器分配 ID localId); break; case offer: log(收到远端 Offer正在处理...); await handleOffer(msg); break; case answer: log(收到远端 Answer); await peerConnection.setRemoteDescription(new RTCSessionDescription(msg)); break; case candidate: log(收到远端 ICE 候选); try { await peerConnection.addIceCandidate(new RTCIceCandidate(msg)); } catch (e) { console.error(添加 ICE 候选失败, e); } break; } }; async function handleOffer(msg) { if (!peerConnection) { await createPeerConnection(); } await peerConnection.setRemoteDescription(new RTCSessionDescription(msg)); const answer await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); ws.send(JSON.stringify(answer)); } async function createPeerConnection() { peerConnection new RTCPeerConnection(rtcConfig); // 把本地轨道添加到连接 if (localStream) { localStream.getTracks().forEach((track) { peerConnection.addTrack(track, localStream); }); } // 监听 ICE 候选发送给远端 peerConnection.onicecandidate (event) { if (event.candidate) { ws.send(JSON.stringify({ type: candidate, candidate: event.candidate })); } }; // 监听远端媒体流 peerConnection.ontrack (event) { remoteVideo.srcObject event.streams[0]; log(已接收到远端媒体流); }; } document.getElementById(startBtn).onclick async () { try { localStream await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject localStream; log(本地音视频采集成功); await createPeerConnection(); } catch (e) { log(采集失败 e.message); console.error(e); } }; document.getElementById(callBtn).onclick async () { if (!localStream) { log(请先点击开始采集); return; } if (!peerConnection) { await createPeerConnection(); } const offer await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); ws.send(JSON.stringify(offer)); log(已发送 Offer); }; document.getElementById(hangupBtn).onclick () { if (peerConnection) { peerConnection.close(); peerConnection null; } remoteVideo.srcObject null; log(通话已挂断); };5.4 代码逻辑说明这段代码浓缩了 WebRTC 工作流的几个核心动作采集navigator.mediaDevices.getUserMedia获取本地音视频流。创建连接new RTCPeerConnection(rtcConfig)创建连接对象并把本地轨道添加进去。发起通话调用createOffer()生成 SDP 媒体协商信息setLocalDescription后会触发onicecandidate收集 ICE 候选。转发信令所有offer、answer、candidate都通过 WebSocket 服务器转发给对端。接收通话对端收到 Offer 后创建 Answer双方完成媒体协商。显示远端画面通过ontrack事件拿到远端流并绑定到 video 元素。这里真正值得注意的坑有三个两个浏览器标签页同时打开时如果都用同一个本地摄像头第二个标签页可能采集失败因为摄像头已被占用。测试时可以用一台电脑和一台手机或者先用一个标签页采集再开第二个标签页只接收。muted属性只加在本地视频上远端视频不能加否则听不见声音。信令服务器的 IP 和端口变化后客户端里的ws://${location.host}会自动适应当前页面地址这是把静态文件和信令服务放在同一个端口下的好处。6. 运行结果与效果验证6.1 启动服务在项目根目录执行node server.js看到输出WebRTC demo server running at http://localhost:3000即表示服务启动成功。6.2 测试流程建议按以下流程验证在电脑 A 打开http://localhost:3000点击“开始采集”确认能看到本地画面。在电脑 B或同一局域网内的手机打开http://电脑A的局域网IP:3000点击“开始采集”确认能看到本地画面。点击电脑 A 的“发起通话”电脑 B 应自动接听并显示远端画面。观察双方页面下方的信令日志应能看到已发送 Offer、收到远端 Offer、已接收到远端媒体流等记录。6.3 判断成功的标准满足以下三个条件即表示通话建立成功双方都显示本地画面。远端视频窗口中出现了对端画面。语音可以正常听到远端视频元素未设置 muted。如果远端画面一直空白优先查看浏览器控制台报错信息。在 Chrome 中按 F12选择 Console 面板重点看有没有关于 ICE 失败、跨域访问、摄像头权限的错误。6.4 失败时的第一排查路径不要一上来就查代码。按下面顺序排查能节省大量时间信令服务器是否启动成功。浏览器是否弹出摄像头和麦克风权限提示是否已授权。是否用了 HTTPS 或 localhost。非本机地址访问摄像头必须 HTTPS这是浏览器安全策略。如果你在手机上用 IP 地址访问但因为没配 HTTPSgetUserMedia会直接失败。查看信令日志确认 Offer/Answer 是否正常交换。查看控制台是否有 ICE 失败相关提示。7. 常见问题与排查思路以下是我在实际调试 WebRTC Demo 时遇到过的典型问题整理成表方便你直接对照问题现象可能原因排查方式解决方案点击“开始采集”后页面无反应非 HTTPS 环境下调用 getUserMedia 被拦截打开浏览器控制台查看报错信息使用 localhost 访问或配置 HTTPS 证书本地画面正常远端画面空白媒体协商未完成或媒体流未绑定查看信令日志是否收到 Answer确认对端ontrack回调触发并绑定远端流手机能打开页面但无法采集页面通过 IP 访问但无 HTTPS看浏览器是否提示不安全用 localhost 或配置 HTTPS两个标签页都能采集但相互看不到画面第二个标签页共用摄像头失败或没有相互发起 Offer查看控制台日志重启浏览器确保只有一端调用摄像头或在两个不同设备上测试跨公网无法连接ICE 直连失败缺少 TURN 服务器控制台查看 ICE 候选类型部署 coturn 或使用云厂商 TURN 服务通话过程中声音有回声本地采集端未启用回声消除或扬声器外放查看音频设备设置使用耳机测试WebRTC 默认开启回声消除但外放环境仍可能有回声偶发卡顿、花屏网络抖动或带宽不足查看网络质量统计启用 WebRTC 带宽自适应或降低分辨率/码率提醒本地 Demo 能跑通只代表你理解了 WebRTC 的基础流程。真正要上生产环境需要处理的事项还有一箩筐下一节展开讲。8. 最佳实践与工程建议8.1 信令通道必须做身份认证与房间管理示例代码里的广播逻辑只适合演示。真实的业务系统里信令服务器至少要具备用户身份认证Token、Session。房间Room管理一个房间对应一次通话或一场会议。消息定向转发只把消息发给目标用户而不是广播给所有人。消息防重放对 SDP 和候选消息做时序控制。否则任何用户都可以伪造 Offer 干扰其他用户通话或者窃听信令内容。信令服务器是通话的指挥中枢安全要求不低于业务服务器。8.2 TURN 服务器不是可选是必选很多教程在讲 STUN 时会强调“STUN 用于 NAT 穿透”给新手的错觉是部署一个 STUN 就够了。实际上在跨运营商、企业防火墙、对称 NAT 环境下STUN 经常无法完成穿透。没有 TURN就意味着部分用户永远无法通话。生产环境建议至少部署两台 TURN 服务器分布在不同的运营商/可用区。TURN 服务开启身份验证防止被恶意利用。监控 TURN 的带宽使用和在线用户数及时扩容。使用 coturn 作为开源实现是社区最常用的方案。8.3 采集权限与隐私边界调用getUserMedia前应该向用户解释为什么需要摄像头和麦克风权限而不是直接弹浏览器授权框。在网页端HTTPS 是基本要求在移动端 App 上需要声明权限用途。另外不要以为把视频流传到远端就没有隐私风险。如果业务涉及敏感场景比如远程问诊、在线面试要在产品设计中明确录制、存储、转写规则避免数据滥用。8.4 通话质量的可观测性从第一天开始接入 WebRTC 统计信息而不是等项目上线后再补。RTCPeerConnection.getStats()能拿到大量信息包括往返时延RTT。丢包率。传输比特率。ICE 连接状态。编解码器协商结果。建议把这些指标上报到监控系统建立基线数据。没有监控的音视频系统出了问题基本只能靠用户截图反馈排查效率极低。8.5 移动端兼容与降级策略WebRTC 在移动浏览器上的表现受设备性能和网络环境影响更大。实际项目中建议根据设备能力和网络状态动态调整分辨率、帧率、码率。提供“仅语音通话”模式在弱网环境下降级。通话过程中如果发生 ICE 断开要有自动重连机制而不是直接挂断。测试设备覆盖中低端 Android 机型很多问题只在弱机上复现。9. 总结与后续学习方向回到开头那个网络热梗。孩子打一通电话看到的是“打通了”这个结果开发者应该看到的是这通电话从采集、编码、信令协商、网络穿透、传输解码到播放的整条链路。从“能打通”到“知道它为什么能打通”正是普通用户和开发者看待通信技术的分界线。这篇文章帮你做到的几件事理清了 PSTN、VoIP、WebRTC 三种远程通话技术的本质区别。理解 WebRTC 中信令、SDP、ICE、NAT 穿透这些核心概念。用 Node.js 浏览器搭建了一个可运行的一对一视频通话 Demo。掌握了排查通话连接失败的排查顺序。明确了从小 Demo 走向生产环境必须补齐的基础设施和安全设计。如果你现在正打算做音视频功能我的建议很明确先把本文的 Demo 跑通把信令流程画清楚然后去研究 coturn 的部署和getStats()的指标采集。这两个点搞透你已经超越了大多数停留在“调接口”层面的开发者。再往后可以顺着这几个方向深入WebRTC 的 Simulcast 和 SVC 机制理解多分辨率编码和带宽自适应。SFUSelective Forwarding Unit架构这是多人视频会议的主流方案代表开源项目是 mediasoup、Janus 和 LiveKit。WebCodecs 和 WebTransport 等新一代 Web 媒体 API它们在低延迟直播和游戏场景有更细粒度的控制能力。音视频开发这条路很长但 WebRTC 是一个足够友好的起点。先跑通 Demo再研究架构你会慢慢发现远程通话的“魔法”背后是一条清晰、可掌控的技术链路。
返回列表