1. 为什么我会把 go2rtc 当成摄像头流媒体的中枢

1.1 多摄像头、多协议混用的现实困境

我在家里和工作室一共装了七八路摄像头,牌子很杂:客厅是海康、门口是大华、车库里一台宇视,还有几路是用树莓派加 ov5647 模块自己拼出来的。这些设备有个共同点——每一台都有自己的 RTSP 地址、自己的管理后台、自己的播放方式。想在手机上随时看一眼,就得装三四个不同厂商的 App;想在一套系统里同时预览所有画面,更是难搞。更麻烦的是,不同厂商对 RTSP 编码、端口、码流类型的处理差异很大,有的默认子码流,有的主码流推送,有的 RTSP 鉴权方式还不一样。

这种碎片化局面,让我不得不找一个能在中间统一收口的东西。它能做三件事:把各路摄像头的取流协议统一掉、把取流后的画面重新封装成浏览器和手机都能直接播放的格式、再提供一个简单的 Web 页面让我随时点开看。考察了一圈之后,我最终选择了 go2rtc,配合 Docker 一起用。go2rtc 这个名字直译是“Go 语言的实时通信”,它本质上是一个轻量级流媒体“编排器”,负责把 RTSP、RTMP、HLS、WebRTC、MJPEG 这些协议的输入和输出串起来。

1.2 为什么我没选 FFmpeg 转推或 Nginx-RTMP

其实这套需求最朴素的方案是用 FFmpeg 把 RTSP 转成 HLS 或 RTMP,然后丢给 Nginx 或者其他流媒体服务去分发。这条路看起来可行,实际落地却有几个让我很烦躁的点。第一,每路摄像头都要单独拉一个 FFmpeg 常驻进程,CPU 占用稳定高,尤其是同时转四路以上的 1080p 主码流时,小主机风扇直接起飞。第二,FFmpeg 转 HLS 的切片延迟通常在三到五秒,点个播放等半天。第三,断流重连、编码参数、日志处理这些都得自己写脚本去守护,否则某个进程偷偷死掉你根本不知道。

go2rtc 的思路和 FFmpeg 不太一样。它天然支持将 RTSP 源以零拷贝、零转码的方式交给 WebRTC 端分发,浏览器端通过 WHEP 协议接收,延迟可以压到几百毫秒。对于不想低延迟的场景,它又能按需输出 HLS、MP4 和 MJPEG,全部由内部模块动态生成。也就是说,它不需要像 FFmpeg 那样把一路流转成多份文件,而是按需封装,很多格式只在有人访问时才真正工作。这对我这种既要低延迟实时画面、又想让普通浏览器顺手能播放的人来说,几乎是量身定做。

1.3 go2rtc 值得说的几个硬特性

  • 默认配置极简,一个 YAML 文件把视频源全部列清楚即可,不需要额外数据库。
  • 内置 Web 管理界面,直接切源、看日志、测连接,排错体验比命令行好太多。
  • 原生支持 WebRTC 的 WHEP 机制,浏览器端无需插件即可播放低延迟流。
  • 对 RTSP 源做了大量兼容处理,常见的 H.264/H.265、PCMU/PCMA、AAC、opus 都能兜得住。
  • 纯 Go 编译,单二进制文件部署,配合 Docker 只需要一个镜像,挂载一个目录。

我花了大概一个下午就把整套平台跑起来了,之后再没换过其他方案。下面这套搭建和调优过程,就是我这段时间实际踩过坑之后的总结。

2. Docker 部署 go2rtc:从拉镜像到跑通第一路流

2.1 用 docker-compose 管理,而不是裸 docker run

很多教程会教你一条 docker run -d --name go2rtc ... 直接起容器,方便是方便,但我个人更推荐用 docker-compose 或者 Docker 的 compose 插件。原因很简单:go2rtc 后续大概率要加摄像头源、改端口、加环境变量,如果用裸容器命令,每次改配置都得重新回忆那一长串参数。写进 compose 文件以后,整个部署过程就变成“改 YAML + docker compose up -d ”,可维护性完全不一样。

我的 docker-compose.yml 大概长这样:

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

这里有两个点特别重要:网络模式和配置挂载。 network_mode: host 是我强烈建议的,因为 go2rtc 既要绑端口给 Web 页面和 WebRTC 使用,又要主动去连摄像头 RTSP 地址,如果走 Docker 默认的 bridge 网络,端口映射和 UDP 端口范围管理都会更麻烦。直接用 host 网络,让容器共享宿主机网络栈,配置简单,WebRTC 的端口穿透也少很多坑。

配置挂载方面,我把宿主机上自定义的 go2rtc.yaml 挂到容器的 /config/go2rtc.yaml ,这样以后改摄像头源、改录像参数,只需要在宿主机上编辑这个文件,然后重启容器即可。镜像自带默认配置,如果你不挂载,它会在容器内生成一份配置,但容器重建后配置就丢了,所以挂载是必须做的一步。第一次启动时,如果宿主机的 go2rtc.yaml 不存在,可能需要先手动创建一个空文件,或者先让容器跑起来生成默认配置再复制出来。

2.2 端口到底要留哪几个

很多初次用的人会困惑,go2rtc 到底要暴露哪些端口?如果用了 host 网络,其实不用“暴露”,容器直接用宿主机端口。但你要知道每个端口是干嘛的,不然排错时一头雾水。默认情况下,go2rtc 会监听 1984 端口作为 Web 界面和 API 入口,比如 http://192.168.1.10:1984 就是管理页面。

WebRTC 传输时不只走 TCP 握手,还要走 UDP 端口做媒体传输,这部分端口范围是在 go2rtc 源码里写死的 UDP 区间。官方默认会用 50000 到 50010 这十个 UDP 端口来接收和发送媒体流。如果你没用 host 网络,这段 UDP 端口映射漏了,大概率会出现“页面能打开、画面就是出不来”的诡异问题。所以再次强调,直接用 host 网络最省心。在局域网内部署,host 网络的劣势几乎可以忽略不计,换来的是零端口穿透烦恼。

2.3 启动后怎么验证部署成功

部署完以后,第一步不是急着加摄像头,而是先确认服务本身是否正常。用浏览器打开 http://宿主机IP:1984 ,能看到 go2rtc 的界面就说明基础服务活了。然后再看日志: docker logs go2rtc 。正常日志里会有类似 listening on 0.0.0.0:1984 的输出,同时会打印它找到的配置来源路径以及各模块的初始化信息。

我习惯再加一步:用命令行测试 API 是否响应。执行:

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

如果返回一个 JSON 数组,说明 API 正常。此时还没配置视频源,返回大概率是空数组,没关系。这一步的意义在于,先把“服务本身”和“摄像头配置”两个变量分开,后续排错时就清晰了。

3. 摄像头接入:RTSP、ONVIF 与厂商差异的一次性消化

3.1 海康、大华、宇视的 RTSP 地址有套路

接入摄像头最核心的活,就是写对视频源地址。不同品牌虽然各自有差异,但绝大多数据都遵循 RTSP 标准,只是路径规则不同。下面这几个是各路厂商非常常见的地址格式:

品牌 常见 RTSP 地址格式
海康威视 rtsp://用户名:密码@IP:554/Streaming/Channels/101
大华 rtsp://用户名:密码@IP:554/cam/realmonitor?channel=1&subtype=0
宇视 rtsp://用户名:密码@IP:554/unicast/c1/s0/live
中维世纪 rtsp://用户名:密码@IP:554/ch0_0.264

以海康为例,地址最后段的 101 代表“第 1 通道的主码流”,如果把 101 改成 102 ,就是第 1 通道的子码流。主码流分辨率高、码率大,适合本地回放;子码率小,适合多路预览。如果摄像头是 H.265 编码,主码流在 WebRTC 里也能解,但部分老手机浏览器就有兼容性问题,我有一次在旧平板上死活放不出画面,最后换成子码流才解决。所以比较稳妥的做法是,预览和展示优先使用子码流,需要高清细节时再单独切主码流。我在 go2rtc 里同时配置主码流和子码流,浏览器默认拉子码流,某个需要放大的画面单独拉主码流。

大华的地址查询参数 channel=1&subtype=0 , subtype=0 是主码流, subtype=1 是子码流。宇视则是 /c1/s0 表示通道 1 子码流 0,改 s1 就是子码流 1。这些路径规则在设备说明书或官方资料里都有,不用死记,但你要知道“改哪个数字能切清晰度”,因为配置 go2rtc 时经常需要在主、子码流之间切换。

3.2 把视频源写进 go2rtc.yaml

go2rtc 的配置文件是 /config/go2rtc.yaml ,视频源配置的写法走的是 YAML 的 streams 字段。每个源一个名字,值为源地址或源地址的数组。下面是我实际在用的配置片段:

streams:
  living_camera:
    - rtsp://admin:password@192.168.1.64:554/Streaming/Channels/102
    - rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101
  door_camera:
    - rtsp://admin:password@192.168.1.66:554/cam/realmonitor?channel=1&subtype=1
  pi_camera:
    - rtsp://192.168.1.80:8554/cam

这里值得解释的是:为什么某些源要写成数组?go2rtc 会逐个尝试数组里的地址,如果第一个连接失败或中途断开,会自动切换到第二个。所以同一台摄像头,我把子码流排前面、主码流排后面,既保证预览流畅,又能在子码流失效时自动尝试主码流。这个 failover 机制是我觉得 go2rtc 比纯粹的多进程 FFmpeg 方案高级很多的地方。

数组里的地址可以不是同一台设备。比如某一路想同时做“主源”和“备用源”,比如主摄像头是海康、备用是同一角度的一个便宜 USB 摄像头,也能写在一个源下面。go2rtc 会在主源断流时自动切到备用源,这相当于免费获得了一个简单的冗余切换能力。

3.3 树莓派摄像头、USB 摄像头和其他取流方式

除了一台台买成品网络摄像头,很多喜欢折腾的人也会用树莓派加摄像头模块来补充视角。树莓派摄像头本身不输出 RTSP,需要通过软件把它的视频数据推出来。我常用的方案是在树莓派上跑一个 rtsp-simple-server 或者把 libcamera-vid 的输出通过管道送给 ffmpeg ,推成 RTSP。核心命令大致是:

libcamera-vid -t 0 --inline --width 1280 --height 720 --framerate 15 -o - | ffmpeg -i - -c:v copy -f rtsp rtsp://0.0.0.0:8554/cam

这样在 go2rtc 里就能用 rtsp://树莓派IP:8554/cam 直接取流。如果你是做智能小车、循迹摄像头之类的小项目,思路完全一样:先把摄像头画面变成一个网络流,再让 go2rtc 统一接入。对于插在宿主机上的 USB 摄像头,go2rtc 也支持一种特殊写法,直接读取本机视频设备。配置里可以写成这样:

streams:
  usb_cam:
    - ffmpeg:device?video=/dev/video0

它内部会调用 FFmpeg 去打开 /dev/video0 。这种场景适合做临时预览,毕竟成品网络摄像头在编码、缓存、网络层面是为长时间应用设计的,USB 摄像头想一直挂着得额外处理供电和散热。

3.4 验证视频源是否连通的小技巧

每加入一个源,我建议先不要急着刷新 Web 页面,而是先在命令行里测试源地址能不能通。可以用系统自带工具:

ffprobe -rtsp_transport tcp -i "rtsp://admin:password@192.168.1.64:554/Streaming/Channels/102" -t 5 -show_streams

能正常读出视频流信息,就说明地址、账号密码都对。这一步可以规避“配置没写错,但摄像头密码改了/子码流被封端口”这类问题。如果 ffprobe 都连不上,去 go2rtc 里配置大概率也会是同样结果。测试的时候建议加 -rtsp_transport tcp ,因为摄像头 RTSP 默认可能走 UDP,而在某些交换机/防火墙环境下 UDP 数据包容易丢,TCP 测出来更符合实际播放场景。

4. 播放链路:WebRTC 低延迟与 HLS/MP4 兼容并存

4.1 为什么在浏览器里首选用 WebRTC

摄像头取流这件事只是整个过程的一半,怎么播放才是用户真正感知到的体验。go2rtc 的招牌功能就是浏览器端通过 WebRTC WHEP 直接收流。它的原理我尽量用通俗的话讲清楚:传统 RTSP 播放要走专用的播放器或者 VLC 插件,而 WebRTC 是浏览器原生支持的实时通信协议,go2rtc 把摄像头的 RTSP 流解封装后,直接通过 WebRTC 的媒体通道推给浏览器,视频数据不落地、不转码,直接走网络传给浏览器解码。

这条链路的延迟有多低?我在局域网内实测,从摄像头画面里出现动作到浏览器画面出现动作,延迟基本在 300 到 800 毫秒之间。这个数字对于看家、看娃、看门口快递来说完全够用,比 HLS 的三五秒延迟好了不止一个量级。 http://192.168.1.10:1984 这个页面里,go2rtc 默认就是用 WebRTC 来播放的,前提是浏览器得支持 H.264 硬解,目前主流的 Chrome、Edge、Firefox 都没问题。Safari 对 WebRTC 的支持也还好,但我在 Safari 上遇到过几次首帧黑屏,需要手动刷新一下。

4.2 具体播放地址怎么拼

go2rtc 为每一种输出格式提供了固定的 HTTP 接口,我整理了一份常用地址表格:

播放方式 URL 规则
Web 管理界面 http://IP:1984
WebRTC 流 通过管理界面或 API 请求获取
HLS 播放 http://IP:1984/api/stream.m3u8?src=源名
MP4 播放 http://IP:1984/api/stream.mp4?src=源名
MJPEG 预览 http://IP:1984/api/frame.jpg?src=源名

比如我在配置里把客厅摄像头源名定义为 living_camera ,那么 HLS 播放地址就是:

http://192.168.1.10:1984/api/stream.m3u8?src=living_camera

这个地址可以直接丢给 VLC、ffplay、PotPlayer 或者各种支持 HLS 的播放器。MP4 地址则是给“想立刻下载一段当前画面”的场景,浏览器直接打开这个地址就会开始缓冲播放 MP4 流,很适合临时取证保存。MJPEG 地址则适合嵌入到没有 WebRTC 能力的简易网页里,用 <img src="..."> 就能显示画面。

协议 延迟 适用场景
WebRTC 300-800ms 实时看护、鱼眼追踪、低延迟预览
HLS 3-10s 兼容播放器、手机系统播放、群晖套件
MP4 1-3s 临时回放、视频下载
MJPEG 1-2s 网页嵌入式预览、Home Assistant 卡片

4.3 多路画面同时看的布局思路

go2rtc 管理页面本身支持多路画面同时显示,自动把项目里的源做成网格布局。如果你觉得默认网页不够好看,也可以自己在 HTML 里嵌入它提供的 JS SDK。go2rtc 官方页面源码里有一个核心 JS 文件,负责和后台建立 WebRTC 连接,你可以把其中的播放器逻辑抽出来,做自己的监控墙。这个思路特别适合做“家庭监控墙”或者“车间多画面看板”。

我自己是写了一个很简单的页面,用 HTML video 标签加 “WHEP client” 把几路 WebRTC 流嵌进去,在电视盒子上全屏展示,这样一个大屏就能同时看到院子里、客厅、门口三个画面。这种做法让我彻底告别了几台设备分别装 App、来回切换的日子。

5. 进阶配置与环境细节:资源占用、自动发现与长跑体验

5.1 资源占用到底有多大

配置多路摄像头前,我很担心 go2rtc 会不会变成 CPU 和内存大户。实测下来的结果让我比较意外:只做 WebRTC 分发、不转码的情况下,一路 1080p 的子码流大概占 30 到 80MB 内存,CPU 基本可以忽略。因为它干的是解封装和推包,不是视频转码,H.264 的压缩和解码工作最终都在浏览器端完成。我跑了四路常见家用摄像头,容器内存占用总和不到 400MB,还包含了一些缓存开销。这对一台 2GB 内存的树莓派或者 J4125 小主机来说,完全扛得住。

需要留意的是,如果你配置的源需要 go2rtc 调用 FFmpeg 去拉取非 RTSP 的格式,比如某些摄像头只开放 HTTP-FLV,那 CPU 占用会高一些,因为内部走了 FFmpeg 进程转换。这种场景我建议只在特殊协议摄像头出现时使用,别把每一路都搞成 FFmpeg 拉流。

5.2 ONVIF 自动发现能省很多事

go2rtc 支持通过 ONVIF 协议自动发现局域网内的摄像头。原理是摄像头厂商普遍实现了 ONVIF 标准,go2rtc 在局域网里广播搜索请求,支持 ONVIF 的设备就会回应自己的服务地址和设备信息。在配置中启用后,它能直接检测到海康、大华、宇视等主流品牌摄像头的 IP,甚至能通过 ONVIF 的鉴权信息自动生成流地址,省去了手工翻说明书找 RTSP 路径的麻烦。

我实际使用中,自动发现功能对同网段摄像头识别得很准,但如果摄像头开了“隔离模式”或者所在 VLAN 与 go2rtc 不在同一个二层网络,ONVIF 广播大概率发现不了。这种情况就得回到手工配置 RTSP 地址的方式,毕竟自动发现的适用范围就是同一个局域网。

5.3 与 Home Assistant 等平台联动

如果你用的是 Home Assistant,go2rtc 有一个专门的集成组件,可以直接把视频源作为 camera 实体接入,这样就能在手机端和自动化里看到实时画面。它利用的是 go2rtc 提供的 HTTP API 和 WebRTC 流,Home Assistant 通过集成本地播放,延迟比官方摄像头集成还要低。我没用任何厂商云平台,所有画面的最终展示都在本地 Home Assistant 里,这让我在隐私安全感上放心很多。

除了 Home Assistant,go2rtc 还提供了完善的 REST API,比如 GET /api/streams 可以获取所有视频源在线状态, POST /api/streams 可以动态增加视频源。我写了一个小脚本,每天凌晨通过 API 检查所有源的状态,如果某个源断线就推送通知到手机。这种主动巡检的思路,比等用户发现画面黑了再处理要高效得多。

5.4 长时间运行的配置细节

把 go2rtc 当成常驻服务,有几个配置细节值得打磨。一是 restart: unless-stopped 保证 Docker 在宿主机重启后自动拉起容器。二是日志策略,compose 里可以加:

logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

避免 go2rtc 长时间运行后把 Docker 的日志目录塞满。三是 TZ 环境变量,这影响日志时间戳和录像任务的时间计算。不设置的话,容器日志和录像文件名会使用 UTC 时间,和北京时间差 8 小时,排查问题时很容易看错。

6. 排错实录:那些让我半夜起来看日志的问题

6.1 摄像头地址对,但 go2rtc 一直报连接超时

有台摄像头之前一直用得好好的,有一天突然画面全部消失,go2rtc 日志里反复出现连接超时的记录。我第一反应是摄像头挂掉了,但厂商 App 却能正常看到画面,说明摄像头本身没坏。后来排查发现,摄像头的 IP 地址被 DHCP 自动分配变了。因为之前我在 go2rtc 里硬编码的是旧的 IP,而厂商 App 是通过设备 ID 和局域网发现机制找到摄像头新地址的,所以 App 正常、go2rtc 失效。

解决方法很直接:在所有摄像头配置里,尽量改成“IP 与 MAC 绑定”或者直接在路由器上给摄像头分配固定 IP。这样即使 DHCP 续租,IP 也不会乱跳。部署 go2rtc 之前,建议先把所有需要用到的摄像头 IP 在路由器里固定下来,这是成本最低的灾难预防手段。

6.2 WebRTC 页面打不开画面,但 HLS 播放正常

浏览器里 go2rtc 页面能加载,点击某路摄像头画面就是不出来,但同一个源用 VLC 播放 HLS 地址却正常。这个问题大概率出在 UDP 端口协商上。WebRTC 传输媒体时,除了 1984 端口的信令通信,还需要 UDP 端口收发媒体数据。当你用 bridge 网络跑 go2rtc 而没有映射完整 UDP 端口段,浏览器尝试建立 WebRTC 连接时,UDP 媒体通道就建不起来,表现就是“信号通了,媒体没到”。

我是切回 host 网络后瞬间解决的。如果你确实需要 bridge 网络,需要把 go2rtc 用于 WebRTC 的 UDP 端口段一一映射出来。但普通家庭场景根本没有隔离多容器的需求,直接 host 网络就是最简单可靠的做法。顺带说一句,NAT 环境下还有一层地址转换问题,但那属于跨网段访问的场景,这里不多展开了。

6.3 内存占用涨上去之后一直不回落

go2rtc 连续跑了一个多月,某一刻我突然发现它的内存占用涨到 800MB,比刚启动时高了两倍多。直觉告诉我存在某种缓存或句柄泄漏。逐段排查后发现是我自己写的一个监控脚本出了问题:它每隔几秒就去请求一次 MP4 流地址,但没有断掉连接。go2rtc 每响应一次 MP4 请求,都会为这个会话创建解码缓冲区,缓冲区释放需要连接正常关闭。脚本频繁建立连接但不正常断开,积压的缓冲区就越来越多。

这个教训让我改了两处:一是监控脚本改成只请求轻量的 API 而不是频繁拉取 MP4;二是在 go2rtc 配置里“关掉”不需要的输出格式,从入口处限制不必要的流转换。重新启动容器后,内存占用又回到正常水平。以后再遇到内存飙升,我的排查路径顺序是:看日志、看连接数、看是不是有外部对象在反复拉流。

6.4 一个隐藏的坑:浏览器自动播放策略

把 go2rtc 页面内嵌到自己的网页里时,很容易遇到一个现象:页面加载后第一帧画面卡住不动,手动点击一下页面又立刻恢复。这不是 go2rtc 的问题,而是浏览器对自动播放的限制。Chrome 等主流浏览器默认不允许页面在没有任何用户交互的情况下自动播放带声音的媒体,WebRTC 流如果被视为“自动播放媒体”,就会被静默挂起。

解决办法有两类:一是确保页面在用户点击后才创建播放器;二是给 video 标签加上 muted 属性,很多浏览器允许静音自动播放。go2rtc 官方页面的做法是自动把视频设为静音,所以你在它的页面上很少遇到这个问题。自己写嵌入页时,一定要记得加上静音处理,否则在不同浏览器上效果天差地别。

7. 最后再分享一个我自己很受益的小习惯

现在这套 go2rtc 平台已经连续跑了大半年,中间除了摄像头本身断电重启之外,几乎没怎么需要维护。docker compose 文件放在我的 Git 仓库里, go2rtc.yaml 也一起管着,每次改完配置,执行一句 docker compose restart go2rtc 就完成变更。我把所有摄像头的 RTSP 地址整理成一张表,备注好品牌、默认端口、主码流和子码流地址规则,之后不管加新摄像头还是排查老问题,翻表比翻浏览器历史高效太多。

如果你也想搭一套自己的多协议摄像头流媒体平台,核心动作其实就三个:写对 RTSP 地址、用 host 网络跑 go2rtc、浏览器里验证 WebRTC 画面。三步走完,剩下的事情就是花时间把画面源做得更完善、更贴自己的使用习惯。工具永远是越用越顺手的,祝你也能少踩几个我踩过的坑。

Logo

邀请您加入社区

更多推荐