想象一下,阿蓝和阿红想面对面聊天,但不想下载任何会议软件。他们只需要打开浏览器:阿蓝先把自己这扇“窗户”上的摄像头和麦克风打开(getUserMedia),然后托一个信使给对方递纸条,约定彼此的系统参数(信令交换)。纸条传完之后,两个人之间会努力搭出一条“跨楼专用走廊”(RTCPeerConnection),走廊一旦建成,画面和声音就在这条走廊里实时流动。 这条走廊并不是从头到尾都经过一个总机房,而是尽量点对点直达;只有在“门卫”不让走的时候,才借道小区之间的中转站。WebRTC 把这些复杂过程都藏进了浏览器,前端工程师只需要调用几个核心 API,就能在网页里实现视频会议、远程协作、AI 实时互动等能力。对 AI 从业者来说,它是把摄像头、麦克风变成“实时数据入口”的钥匙。
学懂 getUserMedia 与 RTCPeerConnection,等于同时掌握实时音视频的“取水阀”和“跨楼专线”,这是给任何 AI 应用加上实时交互能力的基础。
目录
可视化演示
展示getUserMedia媒体流获取、RTCPeerConnection的P2P连接与SDP交换、ICE/STUN/TURN NAT穿透协商及MediaStream+AI实时推理管线。
本节配有交互动效(进入视口后自动加载,约 3KB)。
媒体获取:getUserMedia采集音视频 通过Constraints请求摄像头和麦克风权限,返回MediaStream MediaStream包含视频轨(VID)和音频轨(AUD)两条独立轨道 stream = await getUserMedia({video, audio}) getUserMedia是WebRTC媒体采集的入口API
整体架构
阿蓝和阿红想开一场视频见面会,首先要弄清楚家里需要准备什么。阿蓝不需要自己发明电话线,只需要三样东西:一个能把摄像头和话筒“接进窗边”的水龙头(getUserMedia),一条能跨越两栋楼的专线(RTCPeerConnection),以及一位在楼与楼之间传话的信使(信令)。
WebRTC 就是浏览器内建的这套“实时通信工具箱”。它不需要安装插件,也不依赖某个大公司的中心服务器做媒体转发,而是尽量让两个浏览器直接建立点对点连接。整体上,WebRTC 的 API 分工很清晰:getUserMedia 负责媒体采集,RTCPeerConnection 负责连接与传输,信令则没有指定具体协议,通常用 WebSocket 传递。
实时通信的每一毫秒都有价值,整个环节可以看成一个接力赛:
理解了这五段接力,再看 WebRTC 的两个核心 API 就会很清楚:getUserMedia 负责第一段,RTCPeerConnection 负责中间三段。阿蓝的“窗户、纸条、专线”故事,就这么串起来了。
- 采集耗时摄像头把光信号变成数字帧需要时间。
- 编码耗时浏览器压缩原始视频,减少跨楼传送的体积。
- 网络、解码与渲染压缩后的数据穿过专线,对端解压并画到屏幕上。
媒体采集:getUserMedia
阿蓝要开始聊天,第一步不是打电话,而是先确认自己的摄像头和麦克风能工作。getUserMedia 就是这样一把“窗户钥匙”,它让浏览器打开设备,并把设备吐出的数据包装成一个叫 MediaStream 的“水流”。
调用它会触发浏览器的权限询问,只有在 HTTPS 或 localhost 页面下才能使用。拿到 MediaStream 后,你可以把它交给 <video> 元素做本地预览,也可以把里面的轨道交给 RTCPeerConnection。它不是一个 mp4 文件,而是一股不断更新的实时流,就像水龙头里持续流出的水,而不是一个装满水的水桶。
原始媒体流的体积非常大,这也是为什么 WebRTC 必须立刻进行编码压缩:
所以 1280×720 的未压缩一帧约 276 万字节。不压缩直接传,网络很快就会堵住;这就是为什么后续的编解码、码率控制如此重要。
- 宽度像素画面横向有多少个像素点。
- 高度像素画面纵向有多少个像素点。
- 颜色通道每个像素需要红、绿、蓝三个数值描述。
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });采集约束
阿蓝开好窗户后,还可以调整摄像头的角度、分辨率和声音细节。getUserMedia 允许你传入一组“约束条件”,相当于告诉设备:我想要多清晰的画面、用前置还是后置摄像头、要多高的帧率。
ideal 表示“尽量满足但不强制”,浏览器会选一个最接近的值;还有 exact 表示“必须满足”,不满足就报错。facingMode: 'user' 表示前置摄像头,'environment' 表示后置摄像头。拿到轨道后,还能用 track.enabled = false 实现“静音/关画”,用 track.stop() 彻底关闭设备。
约束条件决定的是采集源头,直接影响后续码率:
实际开发中,你不应该直接让用户设置 4K 超高清,而要结合网络带宽动态调整分辨率。阿蓝的窗户再大,如果连接走廊太窄,还是需要主动把“水量”调小。
- 画面面积宽高相乘得到每帧像素总数。
- 颜色与帧率每个像素 3 字节,再乘每秒帧数,得到未压缩码率。
- 压缩比编码器把没有变化的背景和冗余信息去掉,c 表示压缩后的比例。
const stream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 }, facingMode: 'user' }, audio: { echoCancellation: true }});
信令交换
阿蓝知道自己的窗户长什么样,阿红也知道自己的,但彼此还不知道对方的存在。这时需要一位信使:把阿蓝的“我会说哪些编解码器、我的 IP 和端口是什么”写在纸条上,传给阿红;阿红看完后写一张回条。
这张纸条在 WebRTC 里叫 SDP(Session Description Protocol),这条消息交换过程称为信令。信令服务器通常只是转发纸条,不负责传输真正的音视频。WebRTC 没有规定信令必须用哪种技术,常见选择是 WebSocket、Socket.IO 甚至 HTTP 轮询。关键是必须有一个“双方都能找到的确信中转点”。
整个交换遵循 Offer/Answer 模型:
阿蓝把纸条交给信使,阿红收到后写下 Answer,双方都拿到对方的 SDP,才算完成“约见”的第一步。没有信令,后面的一切都无从谈起。
- 媒体类型这张纸条是视频、音频还是数据通道。
- 编解码器我能用 VP8 还是 H.264,你用哪个。
- 地址与安全我的收件地址端口,以及用于加密的指纹。
P2P连接:RTCPeerConnection
纸条交换完成后,阿蓝需要找一个“总接口”来实际搭建跨楼专线,这个总接口就是 RTCPeerConnection。
创建连接后,先要调用 createOffer() 生成 Offer,再调用 setLocalDescription() 把本地描述设置好;把 Offer 通过信令发给对端后,对端调用 setRemoteDescription() 写入,并返回 Answer。双方都完成描述交换后,RTCPeerConnection 会收到对方发来的媒体流,触发 ontrack 事件。
RTCPeerConnection 不是一个瞬间完成的对象,而是一个有生命周期的状态机:
真实项目中,要监听 connectionState 的变化,在 failed 时重新创建连接或提示用户。阿蓝不会觉得“管道公司来了就等于走廊建好了”,她要看管道工人最终是否交货。
- 初始与协商中new 表示刚创建,connecting 表示正在寻找通路。
- 已连接与退化connected 表示正常通话,disconnected 表示短暂失去联系。
- 失败与关闭failed 表示协商失败,closed 表示连接被主动关闭。
const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]});stream.getTracks().forEach(track => pc.addTrack(track, stream));
ICE协商:穿透 NAT 的路由
阿蓝和阿红都住在小区里,对外只有一个小区门卫。阿蓝写在纸条上的地址可能是内网地址,阿红直接访问这个地址是找不到人的。ICE 就是那套“找到一条真正能走通的路”的机制。
ICE 会收集三类候选地址:host 是本机网卡地址;srflx 是向 STUN 服务器询问后得到的“公网反射地址”;relay 是 TURN 服务器提供的中继地址。收集完成后,两端通过信令交换候选,然后两两配对做连通性检查,挑一条延迟最低的路径。
如果双方都在对称型 NAT 后面,STUN 反射地址也常常无法互通,这时只能走 TURN 中继。ICE 候选配对数量可以这样估算:
实际开发中,通常使用“trickle ICE”边发现边发送候选,而不是等全部收集完再交换。阿蓝和阿红一边试试这个门,一边试试那个窗,最终找到一条双方都能走通的走廊。
- 本地候选数阿蓝这边可用的路径数量。
- 远端候选数阿红那边可用的路径数量。
- 笛卡尔积配对每条本地路径都尝试搭配每条远端路径。
媒体收发:轨道、编解码与统计
走廊建好后,真正的工作才刚刚开始:画面和声音要持续在这条专线上流动。RTCPeerConnection 里的媒体以轨道(track)为单位,addTrack() 加入本地轨道,ontrack 收到远端轨道。
如果想要临时关闭视频但不断开连接,可以把 track.enabled 设为 false;如果想把摄像头从前置换成后置,可以用 replaceTrack()。同一套连接里还可以打开 RTCDataChannel,用来传文本、文件或 AI 分析结果,相当于在走廊里加了一条独立的“数据小管道”。
WebRTC 默认使用 VP8、VP9、H.264 等视频编码器和 Opus 音频编码器,并会根据网络情况自动调整码率:
AI 应用还可以调用 pc.getStats() 拿到实时丢包、帧率、码率等指标,用来做网络质量监控或自动降清晰度。阿蓝的走廊已经建成,剩下的事情就是让水、电、数据在走廊里稳定地流动,并随时监控每段路况。
- 带宽估计浏览器通过丢包率、延迟等指标估算当前可用带宽。
- 最高码率采集约束中允许的上限。
- 取最小值取两者中更小的值作为编码目标,保证不超出链路能力。
AI 应用场景
AI 虚拟面试官
阶段:实时媒体采集与传输
解决的问题:传统视频面试需要安装客户端,且 AI 能力难以嵌入音视频链路。用 getUserMedia 在浏览器内直接采集考生画面,再通过 RTCPeerConnection 将画面实时传给远程考官;浏览器端 AI 同时分析眼神、语速和表情,并把结果通过 RTCDataChannel 发送给面试系统。
具体案例:一家在线教育公司要上线“AI 模拟面试”,候选人打开网页即可面试。前端用 getUserMedia 获取摄像头流,RTCPeerConnection 负责与考官端建连,AI 打分结果实时显示在考官面板上,全程无需下载插件。
实时 AI 字幕会议
阶段:媒体处理与数据通道
解决的问题:视频会议中语音识别服务需要拿到音频流,如果先上传到中心服务器再转发,延迟高且带宽成本大。用 getUserMedia 采集本地音频,经 RTCPeerConnection 点对点传给参与者;其中一端用 Web Audio 和 ASR 模型实时转写,字幕通过 RTCDataChannel 分发。
具体案例:企业内部会议工具增加“实时字幕”功能,发言人的声音在浏览器端被 AI 识别成文字,并通过同一条 P2P 连接的数据通道同步到所有参会者屏幕,延迟低于 1 秒。
AI 远程医疗会诊
阶段:音视频流与 AI 辅助诊断
解决的问题:远程医疗需要高清、低延迟的画面,同时 AI 还要对影像做实时分析。通过采集约束设置 1080p 分辨率,用 RTCPeerConnection 建立 P2P 通道;接收端用 getStats 监控网络质量,并在画面中叠加 AI 标注框。
具体案例:一家远程医疗平台让基层医生用浏览器连接专家端,AI 在专家端自动标记皮肤影像的疑似病灶,并通过 WebRTC 把标注后的画面实时回传,帮助专家快速判断。
Python 演示
# 信令交换模拟器: 演示 WebRTC 建立连接前 A/B 交换 SDP 和 ICE 候选class Message: def __init__(self, sender, receiver, payload): self.sender = sender self.receiver = receiver self.payload = payload class SignalingChannel: def __init__(self): self.messages = [] def send(self, sender, receiver, payload): self.messages.append(Message(sender, receiver, payload)) def relay_all(self): for msg in self.messages: print("{0} -> {1}: {2}".format(msg.sender, msg.receiver, msg.payload)) class PeerSim: def __init__(self, name): self.name = name self.local_sdp = "" self.remote_sdp = "" self.candidates = [] self.remote_candidates = [] def create_offer(self): self.local_sdp = self.name + " offer(video=vp8,audio=opus)" return self.local_sdp def create_answer(self, remote_sdp): self.remote_sdp = remote_sdp self.local_sdp = self.name + " answer(video=vp8,audio=opus)" return self.local_sdp def set_remote_description(self, remote_sdp): self.remote_sdp = remote_sdp def add_candidate(self, candidate): self.candidates.append(candidate) def receive_candidate(self, candidate): self.remote_candidates.append(candidate) def connected(self): return bool(self.local_sdp and self.remote_sdp and self.candidates and self.remote_candidates) def main(): channel = SignalingChannel() alice = PeerSim("alice") bob = PeerSim("bob") offer = alice.create_offer() channel.send(alice.name, bob.name, "SDP:" + offer) bob.set_remote_description(offer) answer = bob.create_answer(offer) channel.send(bob.name, alice.name, "SDP:" + answer) alice.set_remote_description(answer) alice.add_candidate("host:192.168.1.10") bob.add_candidate("srflx:203.0.113.5:3478") channel.send(alice.name, bob.name, "candidate:" + alice.candidates[-1]) channel.send(bob.name, alice.name, "candidate:" + bob.candidates[-1]) alice.receive_candidate(bob.candidates[-1]) bob.receive_candidate(alice.candidates[-1]) channel.relay_all() print("P2P-ready:", alice.connected(), bob.connected()) if __name__ == "__main__": main()
class SignalingChannel:模拟“信使”的中转站,所有消息都先经过它。channel.send(alice.name, bob.name, "SDP:" + offer)把阿蓝生成的 Offer 纸条发给阿红。bob.set_remote_description(offer)阿红写入阿蓝的 SDP,知道自己该用什么参数回应。answer = bob.create_answer(offer)阿红生成 Answer 纸条。alice.set_remote_description(answer)阿蓝收到 Answer 后,双方 SDP 协商完成。alice.add_candidate("host:192.168.1.10")阿蓝添加一条本地 ICE 候选地址。alice.receive_candidate(bob.candidates[-1])阿蓝收到阿红的候选地址,等待连通性检查。print("P2P-ready:", alice.connected(), bob.connected())双方都具备 SDP 和候选时,模拟连接可以进入就绪状态。
常见误区
误区WebRTC 是 P2P 的,所以建连后不需要任何服务器,连建连前也不需要。
正解P2P 指的是媒体流尽量点对点,不代表零服务器。建立连接前必须有一个信令服务器传递 SDP 和 ICE 候选;在对称型 NAT 下,还需要 TURN 服务器中继媒体。阿蓝和阿红的纸条总得有人递,特殊情况下走廊也得借别人的中转站。
为何常见:看到“P2P”就理解为“完全对等、无中心”,忽略了信令和 NAT 穿透这两个必须依赖服务器的环节。
误区调用 getUserMedia 成功后,会得到一个 mp4 文件,可以保存起来或直接给视频软件播放。
正解它返回的是实时 MediaStream,不是文件。MediaStream 是持续流动的数据源,要显示就把它赋给 <video> 元素的 srcObject;要录成文件必须再用 MediaRecorder 编码存储。它像水龙头里的活水,不像水桶里的死水。
为何常见:很多媒体 API 返回 Blob 或文件 URL,容易让人把实时流也想象成静态文件。
误区只要 `new RTCPeerConnection()` 并 addTrack,浏览器就能自动通话。
正解构造函数只是创建“总接口”,真正连通需要完成 SDP Offer/Answer 和 ICE 候选交换。A 发 Offer,B 回 Answer,双方 setRemoteDescription,再交换候选,最后状态才会变成 connected。相当于只约来了管道公司,还没递纸条确认见面地点。
为何常见:把“创建对象”和“建立连接”混为一谈,忽略了连接状态机。
误区两端一直连不上,一定是宽带不够或 Wi-Fi 信号太差。
正解连不上更可能是“地址不可达”而不是“带宽不足”。当双方都在对称型 NAT 后时,STUN 反射地址无法互通,即使带宽很好也会失败,只能靠 TURN 中继。阿蓝阿红如果都被门卫严格拦截,只有借小区中转货梯才能汇合。
为何常见:把网络质量与网络连通性混为一谈,前者看带宽和延迟,后者看地址能否互相访问。
误区既然是浏览器原生 API,视频应该裸传,不需要编码解码。
正解裸视频数据量太大,1280×720 一帧就约 2.7MB,每秒 30 帧会超过 800MB,根本无法实时传输。浏览器会自动协商 VP8/H.264 视频编码和 Opus 音频编码,并在弱网时降码率。专线里的水必须经过压缩罐才能高效运送。
为何常见:浏览器把编解码藏得太深,让人误以为“原生”就等于“没有编解码”。
面试题
Q1. getUserMedia 的主要作用是什么?
- A.获取摄像头和麦克风流← 正确答案
- B.建立浏览器间P2P连接
- C.编码压缩音视频数据
- D.传输信令消息给对端
答案:A。getUserMedia 只负责“打开设备取流”,相当于阿蓝把摄像头和话筒接到窗边;P2P 连接由 RTCPeerConnection 负责,编码和信令也不是它的职责。
Q2. WebRTC 中负责协商媒体参数的 SDP 消息由哪两个环节构成?
- A.Offer 与 Answer← 正确答案
- B.Play 与 Pause
- C.Pull 与 Push
- D.Host 与 Relay
答案:A。SDP 协商像递两张纸条:A 写 Offer,B 回 Answer,双方 setRemoteDescription 后才知道对方用什么编解码器、收在哪个地址。Play/Pause 是播放控制,Pull/Push 是其他协议习惯,Host/Relay 是 ICE 候选类型。
Q3. 让 RTCPeerConnection 发送本地音视频流的正确调用是?
- A.addTrack() 添加媒体轨道← 正确答案
- B.connect() 建立网络连接
- C.open() 打开媒体设备
- D.init() 初始化传输配置
答案:A。pc.addTrack(track, stream) 把本地轨道交给连接对象,之后才参与 SDP 协商和媒体发送;connect/open/init 都不是 RTCPeerConnection 的标准取流方式。
Q4. 对称型 NAT 下,WebRTC 最可靠的连通方案是?
- A.依赖 TURN 中继转发媒体← 正确答案
- B.增大 STUN 探测请求频率
- C.手动指定固定公网 IP
- D.关闭浏览器的安全策略
答案:A。对称型 NAT 的映射每次目标不同,普通 STUN 反射地址无法互通,只能通过 TURN 提供中继候选;加大探测频率和关闭安全策略都解决不了地址可达性问题。
Q5. ICE 候选地址的主要作用是?
- A.提供一条可尝试的连通路径← 正确答案
- B.保存聊天记录到云端
- C.管理摄像头的硬件驱动
- D.加密用户的登录会话口令
答案:A。ICE 候选就是阿蓝和阿红可能走通的一条条路径,包括本机地址、STUN 反射地址和 TURN 中继地址,目的是找到一条真正能连通的路。
Q6. 信令服务器在 WebRTC 中的正确角色是?
- A.建立阶段传递 SDP 与候选← 正确答案
- B.持续转发所有媒体数据
- C.代替浏览器完成视频编码
- D.存储并回放音视频流
答案:A。信令服务器只在建连阶段传递纸条;真正的音视频默认走 P2P 专线。只有在使用 TURN 中继时,服务器才转发媒体,但这和信令是两回事。
Q7. 要把本地摄像头画面送给对端,下面哪些做法正确?
- A.getUserMedia 拿到 MediaStream← 正确答案
- B.addTrack 把媒体轨道加入连接← 正确答案
- C.video.srcObject 先做本地预览← 正确答案
- D.MediaStream.createOffer 发起协商
答案:ABC。正确流程是先 getUserMedia 取流,再用 addTrack 加入 RTCPeerConnection,并可用 srcObject 本地预览;createOffer 是 RTCPeerConnection 的方法,MediaStream 上没有这个方法。
Q8. 关于 RTCPeerConnection 的正确说法有?
- A.既能传音视频也能传数据← 正确答案
- B.建连前需要交换 SDP 描述← 正确答案
- C.可以绕过浏览器安全策略
- D.连接后无需任何服务器
答案:AB。RTCPeerConnection 既能传音视频轨道,也能通过 RTCDataChannel 传数据;建连前必须交换 SDP 和 ICE 候选。它不能绕过安全策略,连接后也可能依赖 TURN 中继服务器。
Q9. WebRTC 中 ICE 候选包含哪些标准类型?
- A.host 本地内网地址候选← 正确答案
- B.srflx STUN 反射地址候选← 正确答案
- C.relay TURN 中继转发候选← 正确答案
- D.proxy 网关代理转发候选
答案:ABC。host、srflx、relay 是 WebRTC 三类标准 ICE 候选;proxy 不是标准类型。就像阿蓝的路径可以是自家门牌、门卫报出的对外门牌,或小区中转货梯。
Q10. 用户处于对称型 NAT 后,始终无法与对方建立 P2P,最可能原因是?
- A.信令交换内容不匹配
- B.浏览器版本太过陈旧
- C.缺少 TURN 中继服务← 正确答案
- D.对方摄像头权限被拒
答案:C。对称型 NAT 需要 TURN 中继这一“小区中转货梯”;信令不匹配、浏览器陈旧、摄像头权限被拒会引发各自问题,但不是对称 NAT 场景下的首因。