
背景:我需要构建一个支持屏幕共享的 Web 端视频会议系统。
为什么又要造一个轮子?
这篇文章里,我想聊聊一个基于 HTTP 和 WebCodec 的实时会议架构。这里说的"Web",指的是完全跑在浏览器里的技术——不需要插件,不需要原生依赖。
一个实时会议系统,本质上就是音视频的实时传输。背后涉及的核心问题无非三个:
- 网络 —— 怎么把数据从一端送到另一端
- 编解码 —— 怎么把原始媒体数据压缩成可传输的格式
- 质量控制 —— 怎么在网络波动时保证体验
一个典型的多人会议,拓扑可能是这样的:
Alice
/ \
/ \
/ \
Bob ------- Carol
在 WebRTC 的世界里,这叫 P2P(点对点) 模式。理论很美好,但现实很骨感:
- 每个 Peer 需要通过 STUN 服务器发现自己的公网地址
- NAT 穿透失败时,还得靠 TURN 服务器中转流量
- ICE 信令还需要额外服务器交换 SDP 信息
- 大部分 WebRTC 实现不重用 UDP 端口,资源消耗很大
WebRTC vs SFU vs RTSP/RTMP
一个更可扩展的方案是 SFU(选择性转发单元) 模式:每个客户端把流推到中央服务器,服务器再复制转发给其他人:

这概念上和 RTSP/RTMP 有点像,但问题是——浏览器不支持 RTSP/RTMP,因为安全策略限制。
那怎么在 Web 上看 RTSP/RTMP 流呢?常见的做法是 转封装 成 HLS 或 MPEG-DASH,浏览器就能用 <video> 或 MSE 播放了。但这个思路代价很大:
- 多次内存拷贝 —— 从 RTP 解包成裸 H.264,再打包成 fMP4,每一层都增加 CPU 负担和延迟
- GOP 级缓冲 —— HLS/DASH 至少要攒够一个 GOP 才能开始播放,这就 1~2 秒延迟,对于无人机、远程驾驶这类场景是致命的
- 丢包敏感 —— 乱序或丢包会搞乱转封装过程,需要额外的完整性保障,这又是一个大坑
那现在 Web 端到底能干啥?
- WebRTC:功能完整但太重(多服务器、高端口占用、内部逻辑黑盒)
- HLS/DASH:稳定但延迟高,只适合"看"不适合"聊"
- WebCodecs(下面要说的):新出的浏览器 API,把编解码能力暴露给开发者,给了我们精细控制的可能性
看起来现在并没有一个"完美"的 Web 会议协议——要么继承重量级框架,要么在延迟和控制力之间做取舍。
所以 WebCodecs 来了:开发者友好的编解码 API
WebCodecs 的出现改变了很多事情。它直接把底层的视频/音频编码器、解码器接口暴露给 JavaScript,不再像 WebRTC 那样把所有东西包在黑盒里。
这意味着开发者可以:
- 从摄像头或屏幕采集原始帧,自行编码
- 逐帧解码接收到的流
- 自定义传输逻辑 —— 重传、优先级调度、自适应码率等
再配合 WritableStream 这类 Web 原生流式 API,我们基本上有了搭建自定义媒体管线的所有积木。
还缺什么?—— 传输协议
现在编解码能力有了,但还缺一个为 WebCodecs 量身定制的传输协议:
- 要处理流完整性、丢包恢复、乱序重组
- 要对 HTTP 友好(过防火墙、负载均衡、代理)
- 当 HTTP/3(QUIC)不可用时,要有 TCP 降级方案
HTTP/3 + QUIC 是理想选择 —— 多路复用、低延迟、原生流抽象。但目前不是所有基础设施都支持,所以得有降级方案。
我们的设计:HTTP + WebCodecs + 智能降级
我们的协议同时支持 HTTP/3(QUIC) 和 HTTP/2/1.1 with keep-alive 作为降级。遇到 TCP 的队头阻塞问题时,可以:
- 切换到低画质流
- 或者为不同媒体流开启多条 TCP 连接
整体架构大致如下:
SERVER
══════════════════════════════════════════════════════════════════════
+--------------------------------+
| Media Scheduler (MTL) |
|--------------------------------|
| Frame Window |
| Frame Database |
| GOP Dependency Graph |
| Path Scheduler |
| Quality Controller |
+--------------------------------+
│ │ │ │
HTTP #1 │ HTTP #2│ HTTP #3│ RTCP-like
───────────────────┼────────┼────────┼──────────────
│ │ │
▼ ▼ ▼
TCP1 TCP2 TCP3 TCP4
══════════════════════════════════════════════════════════════════════
NETWORK
══════════════════════════════════════════════════════════════════════
客户端同样维护多条 TCP 流,各自承载不同的 Feed。另外有一个控制通道(类似 RTCP)负责反馈和重传请求:
══════════════════════════════════════════════════════════════════════
NETWORK
══════════════════════════════════════════════════════════════════════
TCP1 TCP2 TCP3 TCP4
HTTP Conn1 HTTP Conn2 HTTP Conn3 Control Conn
│ │ │ ▲
└──────┬────────┴───────────────┘ │
▼ │
+----------------------------------------------+
| Frame Reassembler |
+----------------------------------------------+
│
▼
+-------------------------+
| Receiver Window |
|-------------------------|
|100 ✓ |
|101 ✓ |
|102 ✗ |
|103 ✓ |
|104 ✓ |
+-------------------------+
│
Missing Frame Detector
│
▼
RTCP-like Feedback
│
▲
Request Frame102 via other TCPs
│
▲
Drop blocking TCP, starting new ones
这套方案和 QUIC + WebRTC 有点像,但更轻量,且完全走 HTTP。在中等网络条件下表现不错,弱网环境可能会吃力——按需选用。
自定义帧格式:为 WebCodecs 量身打造
传统的 RTP 包是围绕 UDP MTU(~1500 字节)设计的,H.264 的分片打包方案也很复杂(参见 RFC 6184)。
对于 WebCodecs 管线,我们需要更简洁、更贴合浏览器 API 的格式。最关键的是 RFC 6381 定义的编解码器配置字符串,例如:
const encoderConfig = {
codec: "avc1.4d4028",
width: 800,
height: 600,
framerate: 30,
avc: { format: "annexb" }
};
这个字符串告诉浏览器用哪个编解码器、哪个 profile 和 level。我们的自定义协议定义了四种帧类型:
| 类型 | 名称 | 说明 |
|---|---|---|
vcnf |
视频配置 | 编解码器初始化数据(SPS/PPS 等) |
acnf |
音频配置 | 音频编解码器初始化数据 |
vfrm |
视频帧 | 编码后的 H.264/H.265/VP9 数据 |
afrm |
音频帧 | 编码后的 AAC/Opus 数据 |
每帧前面加一个简单的二进制头,多条 TCP 连接承载不同的 Feed。视频帧(vfrm)支持分片,大 IDR 帧可以拆成多个包发送。
详细的包结构如下:
vcnf — 视频配置
+------------------------------------------------+
| SEP(帧起始标记) |
+------------------------------------------------+
| BoxName = "vcnf" |
+------------------------------------------------+
| PayloadLen |
+------------------------------------------------+
| Width(uint16) |
+------------------------------------------------+
| Height(uint16) |
+------------------------------------------------+
| CodecLen(uint16) |
+------------------------------------------------+
| RFC6381 编解码器字符串 |
+------------------------------------------------+
| ExtraDataLen(uint32) |
+------------------------------------------------+
| AVC/HEVC 额外数据(SPS/PPS 等) |
+------------------------------------------------+
acnf — 音频配置
+------------------------------------------------+
| SEP |
+------------------------------------------------+
| BoxName = "acnf" |
+------------------------------------------------+
| PayloadLen |
+------------------------------------------------+
| RFC6381 编解码器字符串 |
+------------------------------------------------+
| 编解码器额外数据(如有) |
+------------------------------------------------+
afrm — 音频帧
+------------------------------------------------+
| SEP |
+------------------------------------------------+
| BoxName = "afrm" |
+------------------------------------------------+
| PayloadLen |
+------------------------------------------------+
| FEED(uint32)— Feed 标识 |
+------------------------------------------------+
| Sequence(uint32)— 序列号 |
+------------------------------------------------+
| Timestamp(uint64)— 媒体时间戳 |
+------------------------------------------------+
| FrameID(uint64)— 帧唯一 ID |
+------------------------------------------------+
| Duration(uint32)— 采样时长 |
+------------------------------------------------+
| Payload(编码后的音频数据) |
+------------------------------------------------+
vfrm — 视频帧(支持分片)
+------------------------------------------------+
| SEP |
+------------------------------------------------+
| BoxName = "vfrm" |
+------------------------------------------------+
| PayloadLen |
+------------------------------------------------+
| FEED(uint32) |
+------------------------------------------------+
| Sequence(uint32) |
+------------------------------------------------+
| Timestamp(uint64) |
+------------------------------------------------+
| FrameID(uint64) |
+------------------------------------------------+
| FragmentID(uint16)— 分片 ID |
+------------------------------------------------+
| FragmentCount(uint16)— 总分片数 |
+------------------------------------------------+
| FrameType(uint8) |
| 0 = IDR, 1 = P, 2 = B |
+------------------------------------------------+
| DependencyID(uint64)— 参考帧 ID |
+------------------------------------------------+
| Duration(uint32) |
+------------------------------------------------+
| PayloadLen(uint32) |
+------------------------------------------------+
| NAL 负载(编码后的视频数据) |
+------------------------------------------------+
这个格式轻量、易解析,和 WebCodecs 的 EncodedVideoChunk / EncodedAudioChunk API 配合得很好。
工程实现的一些想法
服务端打算用 Rust 配合 Tokio 来实现。Tokio 支持 SO_REUSEPORT,配合 epoll 能高效处理大量并发连接,比 WebRTC 那种每个 Peer 一个 UDP 端口的模式要节省资源。
客户端直接用浏览器原生的 VideoEncoder、VideoDecoder、AudioEncoder、AudioDecoder,喂数据走我们自定义的协议包格式就行。
总结
我们设计了一套实时会议系统,核心思路是:
- WebCodecs:在浏览器端实现灵活、低延迟的编解码
- HTTP/3(QUIC)+ TCP 降级:兼顾新协议的优势和老基础设施的兼容性
- 自定义帧格式:为浏览器 API 量身定制,不背 RTP 的历史包袱
- 多 TCP 连接:缓解队头阻塞,提升并行度
这套方案在弱网环境下不是最优解,但在 HTTP/3 或可靠 TCP 可用的场景下表现良好。关键是它给了开发者对媒体管线的完全控制权,又没有 WebRTC 那样的复杂度和黑盒限制。
如果你对这个方向感兴趣,欢迎来 Webtalk 仓库提 issue、PR,或者单纯聊聊你的想法。一起把 Web 原生实时通信这件事做得更好。
写代码不易,实时尤难 —— 共勉。
Ext Link: https://shiqifeng2000.github.io/2026-07-10-webtalk/
评论区
写评论还没有评论