当 Kurento 无法胜任我们的摄像头负载时,我们从源代码编译了 Chromium 的 WebRTC库,并在大约十天内构建了自己的精简版 RTSP 到 WebRTC 桥接器。

标准的 WebRTC 解决方案在演示中表现出色。十台摄像头,没问题。也许五十台也没问题。但当你需要将来自地理上分散位置的数百路实时摄像头视频流传输到基于浏览器的指挥中心时,“开发阶段能用”这句话很快就会变得毫无意义。
我们在 iSIM Platform 遇到了这一难题,当时我们正在构建一套本地部署的监控和指挥中心系统。摄像头分布在广阔的区域内,通过光纤网络连接到中央操作室,操作员需要在浏览器中直接以多摄像头网格的形式实时查看直播画面。无需插件,无需桌面应用。仅需一个浏览器。
核心问题在于:RTSP 是 IP 摄像头的标准协议。而浏览器无法直接处理 RTSP 协议。我们需要弥合这一鸿沟。
为什么选择 WebRTC?
该平台原本已有一个桌面版视频管理系统(VMS)客户端,这是一个基于 .NET 的厚客户端,操作员必须在每台工作站上进行安装、更新和维护。对于新客户的部署,我们希望转向基于浏览器的客户端。无需安装,无需插件,随时随地均可访问。现有客户将继续使用桌面客户端,但未来的所有部署都将采用浏览器原生方式。
最显而易见的捷径本是在浏览器中直接播放 RTSP 流。当时,VLC 浏览器插件在技术上确实能够处理 RTSP 流。但 VLC 的插件依赖于 NPAPI,这是一种源自网景时代的插件架构,而各大浏览器正积极淘汰该技术。Chrome 早在 2015 年 9 月的 Chrome 45 版本中就已完全移除了对 NPAPI 的支持。Firefox 随后在 2017 年 3 月的 Firefox 52 版本中也跟进。到了 2019 年,浏览器插件已成为过时的技术。即便当时还能正常工作,它们也仅限于桌面端。我们需要一种能在桌面和移动端运行、兼容 Chrome、Firefox 和 Edge 且无需安装的解决方案。插件是一条死胡同。
所以真正的问题是,浏览器应该原生使用哪种流媒体协议。我们考察了以下几个主要候选协议:
- HLS 是当时支持最广泛的选项。但标准 HLS 存在15 到 30 秒的延迟。该协议的工作原理是将视频分割成多个片段(通常每个片段 6 到 10 秒),播放器必须先下载完整的片段才能开始播放。对于需要操作员对实时事件做出反应并控制云台摄像机的监控指挥中心来说,半分钟的延迟就足以让视频流变得毫无用处。低延迟 HLS 也并非理想之选。苹果直到 2019 年 6 月的 WWDC 大会才正式发布 LL-HLS,即便如此,播放器也花了很长时间才跟上。
- MPEG-DASH 也存在类似的问题。同样是基于分段的传输方式,通常延迟在15 到 45 秒之间。HLS 和 DASH 的设计初衷都是为了向大量用户进行可扩展的视频传输,在这种情况下,几秒钟的延迟是可以接受的。但监控应用的情况则截然不同。
- WebRTC 的延迟仅为200 到 500 毫秒,专为实时通信而设计。它不会像传统方式那样将数秒的视频缓冲成片段,而是在数据包可用时立即发送,缓冲时间极短(仅为几十毫秒)。当操作员控制云台摄像机并需要即时视觉反馈,或对屏幕上的实时事件做出反应时,毫秒级的延迟至关重要。
WebRTC 是唯一现实的选择。我们的指挥中心客户端是用 Angular 构建的,操作员使用 Chrome、Firefox 或 Edge 浏览器,这些浏览器都对 WebRTC 有良好的支持。浏览器端很简单。难点在于服务器端:如何将 RTSP 摄像头流传输到 WebRTC,以及如何大规模地可靠地实现这一点。
首次尝试:Kurento
Kurento Media Server 似乎是合适的工具。它开源,基于 GStreamer 构建,支持 WebRTC,并且拥有活跃的社区。
Kurento 的架构围绕 MediaPipelines 和 MediaElements 展开。你需要创建一条管道,并将元素连接到该管道上。PlayerEndpoint 负责接收 RTSP 流,WebRtcEndpoint 负责输出 WebRTC 流,而媒体流则通过底层的 GStreamer 在它们之间传输。每个元素都拥有用于输入的 sink pads 和用于输出的 source pads。当两个相连的元素使用不兼容的编解码器时,Kurento 内部的 agnosticbin 模块会自动处理转码。该系统采用事件驱动模式,其抽象层使得原型开发变得十分便捷。
实现一个基础演示非常迅速。创建一个管道,添加一个包含摄像头 RTSP URL 的 PlayerEndpoint,创建一个 WebRtcEndpoint,将它们连接起来,调用 play() 方法,浏览器中就能看到实时视频了。一个 Node.js 信令服务器负责处理浏览器与 Kurento 之间的 WebSocket 通信,交换 SDP offer/answers 以及 ICE 候选人。kurento-trans-rtsp-to-webrtc 项目正是展示了这一模式,并作为我们初期实现的参考。
我们还使用了 Kurento 的Room功能来实现流分播,这对我们至关重要。在监控系统中,多个操作员通常需要同时查看同一台摄像机。如果没有分播功能,每个查看同一台摄像机的操作员都需要单独建立一个 RTSP 连接。IP 摄像机的资源有限:嵌入式处理器内存和 CPU 容量有限,网络接口也有限。大多数摄像机只能处理少量并发 RTSP 连接,否则就会出现丢流或无响应的情况。借助 Kurento 的 Room 功能,我们只需建立一个 RTSP 连接即可将生成的 WebRTC 流分发给多个客户端。
原型机运行正常。演示日也很顺利。
然后我们尝试扩大规模。
在实际负载下,当数十路视频流同时运行,且多个操作员同时查看不同的摄像头组时,系统崩溃了。每路视频流都创建了自己的 GStreamer 流水线,每个流水线都包含各自的元素,并通过 GStreamer 基于任务的线程模型生成线程。在开发过程中感觉很方便的 agnosticbin 自动转码功能,在大规模应用后却变成了 CPU 瓶颈。摄像头输出与浏览器预期之间的任何编解码器不匹配都会触发实时转码,这可能会导致整个 CPU 核心被占用。抖动缓冲区填充的速度超过了处理速度,导致帧丢失,并且这种现象会波及到所有会话。
房间功能也带来了一系列问题。通过房间路由的直播流会随机中断。操作员正在观看摄像头画面时,画面会突然卡住或变黑。有时,连接到已有观众的房间时,直播流根本无法启动。我们添加了重连逻辑,但这只是治标不治本,无法解决更深层次的不稳定性。连接到同一房间的操作员越多,情况就越糟糕。
Kurento 官方公布的基准测试结果显示了其性能上限:在 8 个虚拟 CPU、15GB 内存的实例上,最多只能支持大约 18 个用户同时进行 9 个 1:1 的并行会话。社区报告显示,即使只有 10 个并发流,CPU 使用率也会达到 100%。而我们需要处理数百个并发流。
我们当时几乎就要把 Kurento 推向生产环境了,但它的不稳定性对于客户部署来说风险太大。我们 fork 了源代码,进行了自定义构建。精简了功能,调整了缓冲区大小,并修改了 GStreamer 管道配置。这有所帮助,但还不够。问题不在于配置,而在于架构。Kurento 是一个通用媒体服务器:会议、录制、计算机视觉滤镜、混音、MCU/SFU 路由。我们只需要一个功能:接收 RTSP 流,输出 WebRTC 流。所有其他机制,例如 MediaElement 抽象、自动转码、事件系统开销等等,都是我们不需要的累赘。Kurento Room 项目本身现在在 GitHub 上已被标记为不再维护。
决定
某一刻,我豁然开朗。我们不需要媒体服务器,我们需要的是一个协议桥接器。
Kurento 拥有数百项功能,而我们只用其中一项。如果我们只自己开发这一项呢?一个功能极简的工具,专门将 RTSP 转换为 WebRTC,除此之外什么都不做。没有会议功能,没有录制功能,没有混音功能,仅仅是转换。
当时我负责该平台的架构设计。团队的其他五六位工程师正忙于系统的其他部分:指挥中心界面、摄像头管理、录制功能以及系统集成。我将这个 RTSP 到 WebRTC 的桥接器作为个人项目接手,并开始深入研究其实现所需的具体技术。无论我探索哪条路径,最终都指向同一个地方:Chromium的源代码。
为什么选择 Chromium?
2019 年,如果你想在浏览器之外(例如原生应用、服务器或代理)使用 WebRTC,可选方案寥寥无几。WebRTC 标准已在浏览器中实现,其参考实现以 libwebrtc 这一 C++ 库的形式存在于 Chromium 中。
当时并没有可以直接安装并链接使用的独立 WebRTC 库。Pion 作为一款纯 Go 语言实现的 WebRTC 库,虽于 2018 年问世,但当时尚属新生事物,在生产环境中尚未经过验证。GStreamer 的 webrtcbin 组件虽在同一时期发布,但仍不成熟。Janus Gateway 虽然存在,但它又是一个完整的媒体服务器,带有我们试图避免的所有开销。
实现原生 WebRTC 的唯一可靠途径,就是从 Chromium 源代码树中提取 libwebrtc 并自行编译。
搭建桥梁
让 libwebrtc 成功编译本身就是一项大工程。Chromium 的源代码库庞大无比。首先需要克隆 Google 的 depot_tools,然后使用 gclient sync 下载源代码,这会占用 40 多 GB 的磁盘空间。构建系统由 GN(用于生成构建文件)和 Ninja(用于执行构建)组成,这两者都是 Google 自家的工具。相关文档大多假设你正在构建整个 Chromium 项目,而非试图从中提取某个库。WebRTC 代码库中大约每七个提交就会引入一个构建故障,因此选择正确的版本至关重要。整个编译过程可能需要数小时,具体取决于你的机器配置。
虽然过程令人沮丧,但一旦我成功构建出一个干净的 libwebrtc 库并将其链接到 C++ 项目中,我们就拥有了与 Chrome 相同的 WebRTC 堆栈。相同的 ICE 实现、相同的 DTLS-SRTP 加密、相同的 PeerConnection API。这一切都值得。
libwebrtc编译完成后,剩下的就是底层配置了:
RTSP 接收。RTSP本身只是一个信令协议。实际的视频数据通过 RTP(实时传输协议)传输。摄像头发送 H.264 视频,该视频被分割成 NAL 单元(网络抽象层单元,H.264 的基本数据包),然后封装在 RTP 数据包中进行传输。我们的代理需要通过 RTSP 连接到摄像头,接收 RTP 流,并提取 H.264 帧。为此,我们使用了一个现有的 C++ RTSP/RTP 库,而不是从头开始实现该协议。
WebRTC output。这些 H.264 帧被送入 libwebrtc 的 PeerConnection API。由于摄像头本身就输出 H.264 格式,而且浏览器也通过 WebRTC 原生支持这种格式,因此无需进行任何转码。帧直接通过,无需重新编码。libwebrtc 处理了所有其他环节:用于 NAT 穿越的 ICE 协商、用于密钥交换的 DTLS 握手以及媒体流的 SRTP 加密。所有使 WebRTC 能够在网络间工作的复杂性都由该库处理。
信令。一个轻量级的 WebSocket 服务器,浏览器可以请求特定的摄像头流,发送 SDP offer,并接收响应。简单的请求-响应机制,没什么花哨的功能。
扇出(Fan-out)。这是 Kurento 的 Room 为我们处理的部分。代理服务器为每个摄像头维护一个 RTSP 连接。当多个操作员请求访问同一摄像头时,我们将接收到的帧扇出到他们各自的 WebRTC 对等连接。这样就不会产生重复的摄像头连接,也不会浪费摄像头网络接口的带宽。一次采集,多个观看。
仅此而已。没有转码、没有录制、没有混音,没有滤镜。我们没做的每一项功能,就等于没消耗CPU资源。
第一个工作版本大约十天就完成了。虽然还有些粗糙,但它能用,而且速度很快。
投入生产
单个代理 Pod 即可处理 128 个并发摄像头流,这是 Kurento 从未企及的数量。还需要更多吗?只需在 Kubernetes 中对 Pod 进行水平扩展即可。CPU 使用率可预测,且与活跃流的数量成正比。没有随机的突发峰值,没有导致丢帧的缓冲区溢出,也没有像我们在 Kurento Rooms 中看到的那样出现流中断的情况。
指挥中心的操作员可以使用数百个摄像头,并可配置网格布局,从单个摄像头全屏显示到 4×4 网格 16 路并发视频流,应有尽有。他们只需将摄像头从列表中拖到任意单元格,实时视频流便会在一秒内开始渲染。拖出一个,再拖入另一个,瞬间完成切换。操作体验与操作员在桌面视频管理系统 (VMS) 中熟悉的操作流程完全相同,只是现在它是在浏览器标签页中运行的。
整个系统都部署在客户本地,没有使用云端。每个客户站点都有自己的 Kubernetes 集群,运行在本地硬件上,代理服务器作为工作负载运行在集群内部。RTSP 流量始终在本地网络内传输,从而保持了低延迟,并完全消除了对互联网带宽的依赖。当有新站点上线时,我们将代理服务器部署到他们的集群中,并将摄像头指向该代理服务器。摄像头的品牌和型号并不重要,只要它们支持 RTSP 协议并输出 H.264 编码,就能正常工作。
当前的现状
如果今天让我解决同样的问题,我根本不会碰 Chromium 的源代码。生态系统已经迎头赶上了。
Pion 已发展成为一个成熟的、适用于 Go 语言的生产级 WebRTC 库。它就像当初 libwebrtc 对我们那样,是一个基础构建模块,但你无需下载海量资源,也无需与 C++ 构建系统周旋,只需编写 Go 代码并运行 go build 命令即可。搞定。
MediaMTX 和 go2rtc 都是基于 Pion 构建的即用型服务器。它们的功能与我们构建的完全相同:RTSP 输入,WebRTC 输出。单一二进制文件,零依赖。MediaMTX 可在 Kubernetes 中运行,并支持集群。go2rtc 更轻量级,在智能家居领域广受欢迎。两者都能正常工作。
WHIP/WHEP(WHIP 于 2025 年成为RFC 9725)规范了基于纯 HTTP 的 WebRTC 信令。我们之前需要设计的自定义 WebSocket 信令层现在已改为 RFC 中定义的双请求 HTTP 握手。
我们花了十天时间从 Chromium 源代码构建出来的东西,现在只剩下一个配置文件和一个二进制文件下载包。
但在 2019 年,这一切都不存在。编译 Chromium 的 WebRTC 库并围绕它编写一个最小化的桥接程序是正确的选择。有时候,最好的工程决策是意识到你需要的工具尚未被构建出来,然后自己构建其中恰到好处的部分。
从中得到的启示
先试试现有的工具。我们一开始用的是 Kurento,事实证明这是个正确的决定。只有真正证明现有工具无法胜任工作,才能着手构建定制的基础设施。但如果现有工具确实不行,就不要再修补,要认清这种不匹配的现状。
范围是关键。Kurento失败并非因为软件本身不好,而是因为它是一个通用媒体服务器,而我们需要的是一个专用的协议桥接器。那些我们从未用到的功能,实际上消耗了我们大量的 CPU 资源,并影响了系统的稳定性。当你开发自己的产品时,一定要克制住添加新功能的冲动。你跳过的每一个功能,都意味着你无需维护额外的复杂性。
生态系统的发展速度远超你的想象。2019 年需要从源代码编译 Chromium 才能实现的功能,如今只需一个 Go 二进制文件即可完成。当时的前沿技术(例如 Pion 和 WHIP)现在已成为 IETF 标准。构建你现在需要的功能,但不要指望你的自定义解决方案能够永远存在。如今,MediaMTX 可以实现我们之前的代理功能,而且可能做得更好,它还是开源的。
要意识到工具尚未出现。2019年,市面上还没有可用于生产环境的轻量级 RTSP 到 WebRTC 桥接器。如果当时花费数周时间评估那些不成熟的方案,那将是浪费时间。及早意识到这一点并致力于自主开发,使我们避免了陷入同样的陷阱。
作者:Burak Hamuryen
原文:https://post.hamuryen.com/rtsp-to-webrtc-how-we-built-a-custom-video-proxy-by-compiling-the-chrome-engine-814d35e0f91f
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/66738.html