搞监控这块最烦的一件事不是装摄像头,而是把不同品牌的摄像头统一接到一个平台上看。之前我家里一海康、一大华,外加两个杂牌 IPC,官方 App 各管各的,电脑上想集中看就得同时开好几个客户端。后来试过 NVR、试过拿 VLC 开四路窗口,要么绑定品牌生态,要么延迟高到没法用,要么 CPU 被拉满。最后让我把这摊事真正收拾干净的,是 go2rtc——一个 Go 语言写的轻量流媒体网关。它最大的价值就四个字:多协议。RTSP、RTMP、HTTP、ONVIF 这些源都能接入,WebRTC、HLS、MSE、RTSP、RTMP 这些输出协议也都支持,再配合 Docker 部署到 NAS 或迷你主机上,十几分钟就能搭出一个摄像头多协议流媒体平台。这套方案我跑了几个月,过程中踩了不少坑,也总结出一些规律,这篇就完整梳理一遍。

1. 摄像头接入层的一堆破事,go2rtc 是怎么收拾干净的

1.1 一个真实的接入需求画像

先说清楚我到底要解决什么问题。很多人一看"多协议流媒体平台"就觉得高大上,其实落到我这种场景里就是一堆琐碎需求:

  • 设备端有三个品牌以上的摄像头,海康、大华各一台,还有两台杂牌 IPC,其中一台居然只有 MJPEG over HTTP,连 RTSP 都不太标准。
  • 树莓派上还挂了一个 CSI 摄像头,跑着 rtsp-simple-server 往外吐流。
  • 消费端非常分裂:手机上有时候想用系统播放器直接打开 RTSP,有时候要在浏览器里无插件看实时画面,还有一套自己写的自动化脚本需要按 RTMP 地址去推流。
  • 延迟要求也不一样。门口有人按门铃这种场景,最好半秒内出画面;而平时回放或者大屏轮巡,两三秒的延迟完全可以接受。

如果在 go2rtc 之前让我做这件事,我的方案大概率是:每个摄像头各自用 FFmpeg 推流到一个中心 RTMP 服务,然后前端用 flv.js 拉流播放,再单独起一个 HLS 服务应付移动端。这一套下来要维护的东西太多了,而且任何一个摄像头断流,还得写守护脚本去重推。更麻烦的是,这件事情搭完只解决了从摄像头到播放器这一段,后面要接 Home Assistant、要接录像系统,又得再做一层适配。

所以我的核心诉求其实是:能不能有一个组件,把"接入"和"输出"彻底解耦。无论摄像头是 RTSP 还是 RTMP 还是 MJPEG,无论播放端要 WebRTC 还是 HLS 还是 RTSP,我只需要面对一套配置、一个 API。go2rtc 就是这样被选中并留了下来。

1.2 从 RTSP 网关到多协议转换:go2rtc 的定位逻辑

go2rtc 这个名字看起来像是"又一个 RTSP 网关",但它做的事比网关多一层。它把视频源抽象成 stream,每个 stream 有一个名字,比如 front_door 、 backyard 。这个名字就是对外暴露的统一标识:

浏览器 -> WebRTC -> go2rtc -> RTSP -> 海康摄像头
手机App -> RTSP  -> go2rtc -> ONVIF -> 大华摄像头
直播平台 -> RTMP -> go2rtc -> FFmpeg -> 杂牌MJPEG设备

我一开始没理解为什么它要设计成这个抽象,后来用久了才明白,这恰恰是它解决"多协议"问题的关键。传统做法是每个协议写一套转码链路,而 go2rtc 是先把源抓回来,变成内部统一的媒体流,再按需用不同输出协议吐出去。这样你做一次接入,所有输出端都能用,不用每加一个播放端就去改一遍源头。

另一个让它区别于普通网关的点是:go2rtc 本身不做重型转码。它内部实现了 RTSP 客户端、RTSP 服务端、WebRTC、MSE、HLS、MP4 等一堆协议的收发,但视频编码格式基本保持原样,只有在必要的时候才拉起 FFmpeg 子进程做转码。这种"能复制就不转码"的设计,跟我前面踩过的坑正好对应。NVR 也好,自写脚本也好,最容易犯的错就是不管三七二十一先把所有摄像头的流都解码再重新编码,结果 CPU 瞬间被吃满,画质还变差。go2rtc 默认走的是零拷贝路径,所以在树莓派 4B 这种弱设备上也能同时接好几路 1080P。

再提一点容易被忽略的:go2rtc 是 Go 写的单二进制程序,Docker 镜像很小,内存占用常年只有几十 MB。这点对 NAS 用户非常友好,我家那台老群晖跑着一堆容器,给 go2rtc 分到的资源很有限,它依然稳稳当当。

2. Docker 部署之前,先把这几个环境问题想明白

2.1 机器选型:内存、CPU 和存储的真实需求

先给结论:go2rtc 本体不吃配置,哪怕树莓派 3B 都能跑,但如果你要转码,那就得另算账。我目前跑在群晖 DS920+ 上,内存占用稳定在 60MB 上下,CPU 几乎可以忽略——因为我摄像头主码流是 H.264,浏览器和手机端都能硬解,全程走 stream copy,不转码。

不过内存低不代表可以完全不管硬件。有两个地方需要认真评估:

第一,如果有些摄像头是 H.265/HEVC 编码,你的播放端可能会遇到兼容问题。老一点的浏览器、某些智能电视自带的播放器,对 HEVC 的支持非常差。这时候你只能转码成 H.264,而转码的 CPU 开销是实打实的。我实测过,一台四核 x86 小主机,720P H.264 转 720P H.264,单路转码占用大概在 15% 到 25% 之间,勉强能跑两三路;换成 1080P 转 1080P,四核基本就被吃完了。如果摄像头本身能输出 H.264 子码流,我强烈建议接入的时候直接用子码流,别在主码流上做转码。

第二,内存占用虽然低,但并发播放时 go2rtc 会为每路输出维护缓冲队列。我之前试过同时开四个浏览器窗口外加手机 App 拉流,内存也会冲到 200MB 以上。对一台 NAS 来说这不是问题,但如果你打算跑在 512MB 内存的旧路由器上,就要谨慎一点。

存储方面,go2rtc 本身不负责录像,所以不需要给它大容量存储。它默认只会写日志,日志级别调到 info 之后,一天也就几 MB。我习惯把日志输出到 Docker 的 json-file,配合 logrotate 限制大小,不用特别处理。

2.2 端口、目录与网络模式怎么规划不算白干

部署之前先规划好端口和目录,能省掉后面 90% 的排查时间。go2rtc 主要用这几个端口、路径和协议:

用途 默认值 说明
Web UI / API TCP 1984 浏览器管理界面,也是 REST API 入口
RTSP 服务端 TCP 8554 对外输出 RTSP 流,例如 rtsp://主机IP:8554/stream名
RTP over RTSP UDP 8555 部分 RTSP 传输方式需要的端口
WebRTC 媒体 大范围 UDP 浏览器或客户端通过 UDP 与 go2rtc 交换媒体数据

这里我建议直接用 host 网络模式,不要用 bridge + ports 映射。原因有两个:

一是 WebRTC 的媒体传输是动态 UDP 端口,如果你把容器放在 bridge 网络里,就得手动映射一长串 UDP 端口,而且很多 NAT 环境下动态端口映射会丢包、延迟抖动,排查起来特别痛苦。host 模式下 go2rtc 直接绑宿主机网络栈,所有 UDP 端口天然可用,省掉无数麻烦。

二是 ONVIF 设备发现依赖 UDP 组播。桥接模式下,容器发出的组播包不一定能到达摄像头所在的物理网段,导致 Web UI 里扫描不到设备。host 模式就是宿主机本机发组播,基本一次就能扫到。

目录规划更简单,只需要一个配置文件挂载点。我这边固定放在 /opt/go2rtc :

/opt/go2rtc/
├── docker-compose.yml
└── go2rtc.yaml

配置文件用 go2rtc.yaml 这个名字,是因为启动时容器会去默认位置找它。你也可以在 compose 里用 command 参数覆盖,但没必要。

3. 第一套可运行实例:Compose 文件、配置解析与连通性验证

3.1 docker-compose.yml 逐段拆解

直接上我已经在用的 compose 文件:

services:
  go2rtc:
    image: alexxit/go2rtc:latest
    container_name: go2rtc
    restart: unless-stopped
    network_mode: host
    volumes:
      - /opt/go2rtc:/config
    environment:
      - TZ=Asia/Shanghai

几个配置项单独讲一下。

alexxit/go2rtc 是官方镜像,支持 amd64、arm64、armv7 多架构,树莓派和群晖都能直接用,不需要自己编译。 latest 标签虽然方便,但我后来在跑稳定性测试时会固定到具体版本号,防止更新引入不兼容。

restart: unless-stopped 配合 network_mode: host 特别重要。host 模式下容器与宿主机网络栈共享,如果没有这个重启策略,宿主机一重启 go2rtc 就彻底停了,你还得手动 docker start 一次。NAS 上很多容器都是这么挂掉的。

TZ 环境变量是给日志时间用的,默认 UTC 会让你看日志的时候总是对不上时间,排查问题时很难受。顺手设成 Asia/Shanghai。

配置文件路径我挂载到容器的 /config 目录,go2rtc 默认会读 /config/go2rtc.yaml ,所以文件名必须严格叫这个。如果你的宿主机目录结构不一样,路径改掉就行。

启动就三条命令:

mkdir -p /opt/go2rtc
cd /opt/go2rtc
docker compose up -d

跑完看日志:

docker logs -f go2rtc

正常情况下日志里会显示监听端口、加载的配置文件名,以及识别到的设备信息。

3.2 config.yaml:摄像头接入的三种写法与适用场景

go2rtc 的配置文件格式是 YAML,核心结构是 streams 映射。每个 stream 的名字由你定,这个名字会直接出现在各输出协议的 URL 里,所以最好用可读性强的英文,比如 gate 、 garage 。

第一种也是最常用的写法:直接写 RTSP 地址。

streams:
  front_door:
    - rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101

海康摄像头的 RTSP 路径一般是 /Streaming/Channels/101 ,其中 101 表示通道 1 的主码流, 102 表示通道 1 的子码流。大华的常见路径是 /cam/realmonitor?channel=1&subtype=0 , subtype=0 是主码流, subtype=1 是子码流。这两个地址是接入摄像头时最常遇到的,先记下来能省很多事。

第二种写法:通过 FFmpeg 源接入,适用于 go2rtc 内置 RTSP 客户端解不了码的杂牌设备。

streams:
  old_ipc:
    - ffmpeg:rtsp://admin:pass@192.168.1.66:554/live0#video=copy#audio=copy

这里 ffmpeg: 前缀表示让 go2rtc 拉起 FFmpeg 子进程去拉流。 #video=copy 的意思是视频流不重新编码,直接拷贝, #audio=copy 同理。我一开始以为这个 # 是注释,其实它是 go2rtc 规定的参数分隔符,千万别理解成 YAML 注释,否则配置会整段失效。

为什么会有这种写法?因为 go2rtc 内置的 RTSP 客户端走的是自己实现的一套解析逻辑,对标准 RTSP 完全没问题,但一些低端 IPC 的 RTSP 实现不标准,握手阶段就卡住。把拉流这步丢给 FFmpeg,兼容性立刻提升一大截。代价是多一个 FFmpeg 子进程,内存和 CPU 会增加一些,但换来的稳定性值得。

第三种:ONVIF 自动扫描。如果你不知道摄像头的准确 RTSP 地址,可以先在 Web UI 里用 ONVIF 扫描。go2rtc 支持通过 UDP 组播发现同网段支持 ONVIF 的摄像头,并且在界面上直接显示探测到的 RTSP 地址。扫描到之后,把它填回配置文件的 streams 里固定下来就行。

我实际用到的最完整配置大概是这样的:

log:
  level: info

streams:
  front_door:
    - rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101
    - ffmpeg:rtsp://192.168.1.64:554/Streaming/Channels/102#video=copy
  backyard:
    - ffmpeg:rtsp://admin:password@192.168.1.66:554/live0#video=copy
  pi_cam:
    - rtsp://192.168.1.80:8554/csi0

webrtc:
  ice_servers:
    - urls:
        - stun:stun.l.google.com:19302

你可能会问, front_door 下面为什么写两个条目?go2rtc 支持给同一个 stream 配置多个源,它会在第一个源断流时自动切换到第二个。我这边主码流用于录像和高清回放,子码流作为应急兜底,一旦主码流断了,平台还能继续输出低清晰度画面,保证监控不黑屏。这个多源切换机制在实战里非常实用。

3.3 启动后的验证链路:从 Web 界面到 API 拉流

配置好之后,先别急着接各种各样的业务系统,按下面的顺序验证一遍,确保链路是通的。

第一步,浏览器打开 http://主机IP:1984 。go2rtc 自带一个管理界面,左侧是播放器,右侧是 stream 列表和日志。如果你能看到摄像头画面正在播放,说明接入成功一半。画面上方还会显示当前用的输出协议,Web UI 默认会优先走 MSE 或 WebRTC。

第二步,用命令行验证 API。

curl http://127.0.0.1:1984/api/streams

返回的 JSON 里会列出所有 stream 的当前状态,包括源地址、是否在线、延时时长等。如果某个 stream 显示 offline,直接看对应源的日志就能定位。

第三步,验证 HLS 输出:

curl -I "http://127.0.0.1:1984/api/stream.m3u8?src=front_door"

只要能返回 200 OK 和 application/vnd.apple.mpegurl ,HLS 链路就没问题。这个地址可以直接填到支持 HLS 的播放器里,也可以给 iOS 上的 App 用。

第四步,用 FFplay 验证 RTSP 服务端输出:

ffplay "rtsp://127.0.0.1:8554/front_door"

go2rtc 把每个 stream 暴露成一个 RTSP 地址,这个能力很有用:其他老设备、硬件解码器、甚至不支持 WebRTC 的播放器,都可以直接用这个地址拉流。

我遇到最多的问题是:Web UI 画面能出,但 curl 一个 API 返回 404。原因几乎都是 URL 里的 stream 名字没写对,或者端口被宿主机防火墙挡了。先确认 curl 能访问 1984,再确认 stream 名和配置里完全一致,基本能解决。

4. 输出端的协议玩法:WebRTC 低延迟之外的选择题

4.1 WebRTC:低延迟拉流最香的场景,但不是万能药

我最初选中 go2rtc,有一个决定性理由:它原生支持 WebRTC 输出,而且这个协议不是作为附加功能实现的,而是和 RTSP、HLS 平级的一等公民。

WebRTC 的延迟能做到多低?我实际测试局域网环境下,从摄像头采集到浏览器画面显示,延迟基本在 0.2 到 0.5 秒之间。HLS 通常是 3 到 10 秒,RTSP 在 VLC 里开也要 1 秒左右的缓冲。对"门铃响了看门口是谁"这种场景,半秒延迟和 3 秒延迟的体验差距非常明显。

浏览器原生支持 WebRTC,不需要装插件,不需要额外开发,这是它能当 Web UI 默认播放协议的原因。go2rtc 在 Web UI 里会自动做一些降级:能开 WebRTC 就走 WebRTC,开不了就退回 MSE,MSE 不行再尝试 HLS。这个降级策略非常实用,因为同一个网络里不同设备的浏览器版本差异很大。

但 WebRTC 也有它不省心的一面。它走 UDP,媒体面是点对点传输,一旦经过 NAT 或者复杂网络,就需要 ICE 帮助打洞。我在家里的局域网里完全没问题,但在公司网络或者 4G/5G 环境下,有时候会看到画面卡在"connecting"状态。这种时候就要检查两件事:

第一, stun 服务器有没有配。我配置里就预留了一个 Google 的 STUN 地址:

webrtc:
  ice_servers:
    - urls:
        - stun:stun.l.google.com:19302

没有 STUN,WebRTC 连局域网内的设备都可能识别不了自己的公网地址,媒体协商容易失败。

第二,UDP 端口是不是被限制。有些企业 WiFi 会对 UDP 做 QoS 限制或直接丢弃,WebRTC 在这种网络里很难稳定。这个没有完美的软件解法,只能看实际网络环境决定是否改用 MSE 或 HLS。这也是我为什么强调输出端不要死磕一种协议,多一种输出,多一条活路。

4.2 MSE、HLS、RTMP 输出的取舍和典型用途

go2rtc 除了 WebRTC,还提供了几种常见输出协议。我按照实际使用频率整理了一张表:

协议 延迟 兼容性 典型场景
WebRTC 0.2~0.5秒 浏览器/App 原生支持 门铃对话、多路同屏、对讲
MSE 1~2秒 现代浏览器支持好 Web UI 的兜底播放方案
HLS 3~10秒 全平台兼容性最好 iOS、微信、电视播放
RTSP 0.5~1秒 专业播放器/硬件解码器 老系统、监控墙、录像机取流
RTMP 1~3秒 直播平台和推流工具支持 B站、YouTube 等平台推送

MSE 对我来说是一个"安静但可靠"的方案。它会先把流封装成 MP4 或 WebM 分片,浏览器用 MediaSource API 播放,延迟比 HLS 低,兼容性又比 WebRTC 好。go2rtc:Web UI 里 MSE 经常是实际工作的那个——尤其是当 WebRTC 因为 NAT 问题没打通的时候。

HLS 则是我在移动端分享场景最爱用的。把 api/stream.m3u8?src=front_door 这个地址发给家人,iPhone 自带的 Safari 和安卓的浏览器基本都能直接播放,不需要装任何 App。代价就是延迟高,所以凡是强调"实时"的场景我都避着它走。

RTMP 输出对直播场景特别有用。go2rtc 内部集成了 RTMP 服务端,默认监听 1935 端口,配置好之后可以直接作为 RTMP 源,被 OBS、FFmpeg 或者直播平台采集。如果你想把摄像头画面推到直播平台,思路是在 go2rtc 的 stream 上加一个输出配置,让平台直接从 go2rtc 拉 RTMP 流,没必要再用 OBS 对着屏幕录,那样既占 CPU 又容易丢帧。

5. 折腾这么久,总结一份能直接抄的避坑清单

5.1 IPC 兼容性问题:RTSP 地址没写对就全盘皆输

摄像头接入最土但也最常见的坑,就是 RTSP 地址写不对。这里说的"不对"不是指少了空格,而是地址里的路径和参数跟厂商私有约定有关,同一个品牌不同型号都可能不一样。

我在配置里踩得最深的一个坑是:海康的 101 表示主码流,有些新款机器能同时支持 101 、 102 、 201 、 202 ,但老型号不一定。如果你拿 201 的去填结果总是连不上,先降级到 101 试试。大华的 RTSP 地址里带 channel 和 subtype 参数,参数顺序错了也不行。

还有一个特别容易被忽略的:摄像头密码含特殊字符时,RTSP URL 里的密码要做 URL 编码。比如密码是 abc@123 ,直接写进 URL 会解析错误,因为 @ 是 URL 里的特殊字符,需要用 %40 替换。这个不起眼的细节当年让我查了整整一个晚上。

排查 RTSP 连接问题,我建议先脱离 go2rtc,用 FFprobe 直接验证摄像头地址:

ffprobe "rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101"

如果 FFprobe 能解出视频流信息、能看到编码格式和分辨率,说明地址没问题,问题在 go2rtc 这一层;如果 FFprobe 都连不上,那就是地址或者密码问题,先把源头搞定再回过来谈平台配置。

另外,很多 IPC 默认只允许 2 到 3 路同时拉流。当你接了 go2rtc 之后又在 VLC 里手动打开同一个摄像头的 RTSP 地址,很容易触发摄像头的最大连接数限制,导致 go2rtc 的流突然断掉。这个现象特别容易误判成 go2rtc 不稳定,实际上是被 VLC 抢占连接名额了。

5.2 转码的代价:CPU 占用与画质损失怎么平衡

go2rtc 的设计哲学是"能复制就不转码",我实际用下来深刻认同。但这不是说转码完全不能碰,而是要知道边界在哪里。

什么情况必须转码?三个典型场景:编码格式不兼容、分辨率需要调整、需要叠加文字或画面处理。其中第一个最常见:老浏览器和部分智能电视不支持 HEVC 解码,这时候 H.265 的摄像头流如果不转成 H.264,播放端根本出不了画面。第二个场景是带宽控制,你在外面用 4G 看家里摄像头,假如摄像头只有 4K 主码流,流量会非常恐怖,这时候转成 720P 甚至 480P 的 H.264 就非常划算。

但转码的代价是实打实的。我实测在树莓派 4B 上转一路 720P H.264,CPU 占用就已经很高,再多跑一路就基本卡顿。换成 x86 四核 CPU 的小主机,720P 转 720P 能跑 2 到 3 路,1080P 转 720P 也就 1 到 2 路。硬解转码需要 GPU 或者 Intel Quick Sync,在群晖这种机箱里不一定好配置,所以我的原则是:能用子码流就不用转码。

实际操作上,接入时优先用摄像头的子码流,因为子码流本来就是厂商为预览和多路访问优化的低分辨率流,不用额外消耗 CPU。主码流留给录像和需要高画质的单路播放。如果遇到必须转码的流,配置长这样:

streams:
  hevc_cam:
    - ffmpeg:rtsp://admin:password@192.168.1.70:554/stream0#video=h264#video=scale=1280:720

这里 video=h264 是让 FFmpeg 转码成 H.264, video=scale=1280:720 是调整分辨率。注意,这两个参数是一起作用的:先解码 HEVC 原始流,缩放分辨率,再编码成 H.264,CPU 开销比单独转码更高。能用子码流解决的问题,尽量别这么干。

5.3 从外网访问时的关键排查链路

我一直跟朋友强调:在家里局域网搭好只是第一步,真正想在外面随时看监控,Networking 部分才会暴露问题。下面这条排查链路对我来说每周都会用上。

现象一:局域网内 Web UI 播放正常,但从外网访问同一个域名时画面一直转圈。

先别怀疑 go2rtc。这种场景我先确认端口有没有通。go2rtc 的 1984 端口只是承载 Web UI 和 API,真正传输媒体数据走的是其他端口。如果你只映射了 1984 端口,Web UI 能打开,但播放时视频流走不通,画面上会一直 loading。所以我一般用 host 网络模式,把所有 UDP 端口都放出去,少一个端口少一层障碍。

现象二:浏览器能打开 Web UI,但 WebRTC 一直显示 connecting。

这大概率是 WebRTC 的 UDP 媒体端口没通,或者 NAT 穿透没成功。局域网内正常、外网不正常,就说明媒体协商成功但数据传输失败。排查方法很简单,浏览器打开 Web UI 后按 F12 看 Console 日志,go2rtc 会把 ICE 候选状态打出来。如果日志里全是 failed、timeout 这种词,基本可以确认是 UDP 不通。这时候要么调整网络设备放行 UDP,要么在 Web UI 上切到 MSE 或 HLS,先保证能看到画面,再回头折腾低延迟。

现象三:在外网用 HLS 播放非常流畅,但 WebRTC 始终不通。

这种环境很适合直接用 HLS 作为远程查看方案。HLS 走的是 HTTP/HTTPS 协议,不需要额外的 UDP 媒体端口,只要反向代理把 1984 端口的 HTTP 请求转发出去,外网就能稳定看。延迟虽然有几秒,但稳定胜过一切。

还有一个安全层面的提醒,我把 go2rtc 的 1984 端口通过反向代理暴露到公网时,一定会加 HTTPS 和访问认证。WebRTC 的 getUserMedia 在非 HTTPS 环境下会被浏览器禁用,你在外网用 HTTP 打开 Web UI 会发现摄像头完全无法启动,这不是 go2rtc 的问题,是浏览器的安全策略。套一层 HTTPS 反向代理之后,这个问题会连同认证一起解决。

最后再分享一个小技巧:go2rtc 支持在配置里给同一个 stream 配置多个源,我用这个机制实现了一个简单的断流自愈。主源是主码流,备用源是子码流,主源一旦断掉,go2rtc 会在几百毫秒内自动切到备用源。如果你有重要的摄像头不允许长时间黑屏,这个多源配置能力值得优先研究。我后来把大部分摄像头都改成了至少两个源,稳定性明显上了一个台阶。

Logo

邀请您加入社区

更多推荐