Docker+go2rtc统一接入多种摄像头:从RTSP到WebRTC低延迟播放实战
如果你跟我一样家里或办公室的摄像头不止一台海康的 RTSP、大华的 RTSP、树莓派上挂的 USB 摄像头、甚至还有几台 POE 摄像头混在同一台交换机上那你大概率经历过这种崩溃时刻——每个品牌都有自己的 App 和 SDK想在浏览器里统一看实时画面要么装插件要么把 HLS 拉到十几秒延迟VLC 能看但一到外网访问就卡成 PPT。我后来把整套方案换成了Docker go2rtc只维护一个 yaml 文件就把多协议摄像头源统一成了一套流媒体平台摄像头还是原来的摄像头但所有画面都能直接在浏览器里点开看延迟可以压到一秒以内而且容器重启、配置热加载、录像留存都变得非常省心。这篇文章就完整记录我从选型到落地、再到踩坑修复的整个过程适合手里有任意品牌摄像头、想把它们统一管理起来的读者参考。1. 为什么我在一堆流媒体方案里最终选了 go2rtc1.1 先从一次凌晨 1 点的监控接线说起我之前搞了一套临时监控当时手头设备很杂一台大华 POE 摄像头、一台海康 WiFi 摄像头、一块树莓派 CSI 摄像头还有两个 USB 摄像头插在闲置笔记本上。理想状态是打开一个网页六个画面整整齐齐铺在屏幕上点哪路放大哪路。实际状态是大华登录它的 App、海康登录另一个 App、树莓派画面靠 VLC 手动拉流、USB 摄像头我甚至要先开 OBS 才能看到。更麻烦的是我后来想把其中两路画面接到一个自建的小页面上发现浏览器根本没法直接播放 RTSP 流逼着我先去搞清楚 HLS、MSE、WebRTC 这些名词之间的关系。那一刻我意识到真正缺的不是摄像头是一个能站在摄像头和各种播放器之间的翻译层。它得听懂 RTSP、RTMP、MJPEG、甚至直接读 USB 设备然后统一输出成浏览器能消费的格式。市面上能干的工具不少但配置简单、资源占用低、还能用 Docker 一把梭的选择其实不多。1.2 go2rtc 到底做了什么go2rtc 是一个用 Go 写的轻量级流媒体程序核心思路非常直白把摄像头或视频源的输入协议RTSP、RTMP、HLS、MJPEG、WebRTC、USB/V4L2 等和输出协议WebRTC、HLS、MP4、MJPEG、RTSP、RTMP 等解耦开。你只需要在配置文件里告诉它哪一路流叫什么名字、从哪里拉它就会自动把源拉起并用你想要的协议暴露出来。比如streams: dahua_main: rtsp://user:pass192.168.1.108:554/cam/realmonitor?channel1subtype0 hik_sub: rtsp://user:pass192.168.1.64:554/Streaming/Channels/102 usb_cam: ffmpeg:v4l2:///dev/video0#videoh264写好这些之后浏览器访问http://IP:1984/api/webrtc?srcdahua_main就能低延迟播放访问http://IP:1984/api/mjpeg?srcusb_cam就能拿到一个纯 MJPEG 图片流接口。同一路源想用 WebRTC 看实时、想用 HLS 做回放、想用 MP4 拉一段录像go2rtc 都能提供。它自带的 Web UI 也有点东西打开 1984 端口就是管理界面能看到每路流的连接状态、拉流日志、当前并发数甚至可以直接在页面上切换源。对做硬件、做自动化、做智慧社区项目的人来说这种给摄像头统一出接口的思路非常实用。1.3 与 FFmpeg Nginx、ZLMediaKit、MediaMTX 的实际对比先说最常见的 FFmpeg Nginx 方案。FFmpeg 把 RTSP 转成 HLS 切片丢给 Nginx浏览器拉 m3u8。这套方案的优点是全开源、可裁剪空间大缺点是延迟随切片数量堆叠通常在 5 到 15 秒而且一旦涉及多路摄像头FFmpeg 进程管理、断线重推、切片过期清理全要自己写脚本时间成本很高。ZLMediaKit 是很多商用流媒体平台的内核功能非常强WebRTC、SRT、GB28181 都支持。但它的配置面相对宽依赖项也多如果你只是想在局域网里把几路摄像头统一看看用 ZLMediaKit 多少有点杀鸡用牛刀。MediaMTX前身 rtsp-simple-server也很轻主打 RTSP 中继和协议转换。但它更多是把 RTSP 再分发给 RTSP 客户端在浏览器播放方面需要额外搭配 HLS 或 WebRTC 插件组件。go2rtc 和这几个选手相比最大的优势是原生把 WebRTC 集成得非常顺滑。WebRTC 的 P2P 特性让局域网内播放延迟可以压到 0.3 到 1 秒而且不需要浏览器装任何插件。配置成本又低一个 yaml 文件管所有源。实际体验下来它非常适合作为家庭/小型工作室的流媒体中枢。方案技术栈浏览器低延迟播放配置复杂度资源占用多路源管理FFmpeg NginxFFmpeg Nginx难需 HLS/MSE 自行处理高高弱ZLMediaKitC支持中高中高强MediaMTXGo需要配合其他服务低低中go2rtcGo原生 WebRTC开箱即用低极低强2. Docker 跑起来两种部署方式手把手配置2.1 用 docker run 一行命令先点火假设你已经在宿主机上装好了 DockerWindows 上可以用 Docker DesktopLinux 直接用 Docker Engine最快验证 go2rtc 的方式是直接跑一个容器docker run -d \ --name go2rtc \ --restart unless-stopped \ -p 1984:1984 \ -p 8555:8555/udp \ -v $(pwd)/go2rtc.yaml:/config/go2rtc.yaml \ alexxit/go2rtc:latest这里有两个端口必须搞清楚1984是 go2rtc 的 Web UI 和 API 端口8555/udp是 WebRTC 建立 P2P 连接时用的 UDP 端口。很多人只映射了 1984结果浏览器能看到页面但播放转圈就是因为 WebRTC 的 UDP 协商端口没放出来。跑起来之后浏览器打开http://宿主机IP:1984能看到一个极简的 Web UI。如果你还没写 go2rtc.yaml 或者挂载目录里没有这个文件容器会使用默认配置启动UI 里会显示空白这是正常的下一步就把配置补齐再重启容器。2.2 用 docker-compose 把配置固化下来docker run 适合验证真要长期跑我强烈建议用 docker-compose。这样配置、目录、重启策略都固化成文件重装系统或迁移机器时一条命令恢复。我实际用的 compose 文件长这样services: go2rtc: image: alexxit/go2rtc:latest container_name: go2rtc restart: unless-stopped ports: - 1984:1984 - 8555:8555/udp volumes: - ./config:/config - ./media:/media environment: - TZAsia/Shanghai注意我把配置目录挂载成./config录像目录挂载成./media。这样做的好处是 config 目录可以整体备份media 目录可以按需清理录像文件两个目录职责分离等哪天要接入外部存储或做冷备直接改挂载点就行。启动命令docker compose up -d如果你想修改 go2rtc.yaml 后马上生效不需要重启整个容器go2rtc 支持配置热加载。我一般改完配置文件后执行docker kill -s HUP go2rtc这句命令向容器内进程发送 HUP 信号go2rtc 会自动重新读取配置文件。如果改动导致配置语法错误它会在日志里告诉你不会直接把容器搞挂。2.3 宿主机选型与 Docker Desktop 的坑位如果你的宿主机是 Linux我建议你在 compose 里直接开启network_mode: host让 go2rtc 共享宿主机网络。这样 WebRTC 的端口协商会简单很多不需要手动映射 8555/udp也不容易出现候选地址是容器内网 IP 导致外部连不上的问题。services: go2rtc: image: alexxit/go2rtc:latest container_name: go2rtc restart: unless-stopped network_mode: host volumes: - ./config:/config - ./media:/media但如果你用的是 Windows 或 macOS 的 Docker Desktopnetwork_mode: host是不生效的必须走端口映射。而且 Docker Desktop 默认跑在虚拟机里WebRTC 的 UDP 端口映射偶尔会出现页面能看画面等半天不出来的现象。遇到这种问题优先检查 Windows 防火墙有没有放行 8555/udp其次是 Docker Desktop 的端口绑定是否正常。另外提一句很多人在 Windows 上装 Docker Desktop 失败报类似 virtualisation support not detected 的错本质是 Windows 的虚拟化平台/WSL2 没开。解决办法是去 BIOS 里确认 VT-x 已启用然后在 Windows 功能里把适用于 Linux 的 Windows 子系统和虚拟机平台勾上重启后再装 Docker Desktop。这个步骤卡住的话后面所有容器都跑不起来属于典型的先修底座问题。3. 摄像头接入从能看到画面到稳定不丢流3.1 最常见的 RTSP 摄像头海康、大华、POE 直连RTSP 是目前安防摄像头事实上的标准协议海康、大华、宇视这类大厂都支持但各家 RTSP 地址格式差异很大。先看最常见的两种海康威视的 RTSP 地址分主码流和子码流主码流rtsp://用户名:密码摄像头IP:554/Streaming/Channels/101 子码流rtsp://用户名:密码摄像头IP:554/Streaming/Channels/102大华的 RTSP 地址带 query 参数channel 表示通道号subtype 表示码流类型主码流rtsp://用户名:密码摄像头IP:554/cam/realmonitor?channel1subtype0 子码流rtsp://用户名:密码摄像头IP:554/cam/realmonitor?channel1subtype1我把这些源写进 go2rtc.yaml 时通常按场点_通道_码流的规则命名比如streams: livingroom_main: rtsp://admin:pass192.168.1.64:554/Streaming/Channels/101 livingroom_sub: rtsp://admin:pass192.168.1.64:554/Streaming/Channels/102 frontdoor_h265: rtsp://admin:pass192.168.1.80:554/cam/realmonitor?channel1subtype0命名规则看起来是小事但当你接入 20 路源之后名字能不能一眼看懂直接决定排查问题效率。我见过有人用cam1、cam2这种命名最后根本记不清哪台是哪台。接入 RTSP 源之前建议先用 VLC 或者 ffprobe 验证一次地址正确性避免把配置错误当成 go2rtc 问题ffprobe -rtsp_transport tcp rtsp://admin:pass192.168.1.64:554/Streaming/Channels/101能读到流信息地址基本没问题。另外注意海康部分摄像头默认没开启 RTSP 鉴权或者需要登录 Web 管理页面开启RTSP 服务大华摄像头第一次激活时也要先在 Web 页面设置密码并启用 ONVIF/RSTP 开关。这一步不做go2rtc 这边永远提示认证失败。3.2 非 RTSP 设备USB 摄像头、CSI 摄像头、MJPEG 老设备并不是所有摄像头都带 RTSP。很多 USB 摄像头插到设备上只有 V4L2 设备节点树莓派的 CSI 摄像头ov5647 之类也是一样。go2rtc 本身不会直接读 V4L2但它内置了与 FFmpeg 的联动能力可以在源地址里显式调用 ffmpeg 进行采集和转换。streams: usb_cam: ffmpeg:v4l2:///dev/video0#videoh264 rpi_csi: ffmpeg:v4l2:///dev/video0#videoh264#video_size1280x720如果摄像头本身输出 MJPEG就需要让 ffmpeg 先解码再编码成 H264否则后续 WebRTC 播放兼容性会差很多streams: old_mjpeg_cam: ffmpeg:http://192.168.1.99:8080/video.mjpg#videoh264到这一步你会发现go2rtc 对无效协议的处理方式就是把它丢给 FFmpeg 去做脏活累活。这种设计很聪明协议生态太大了它只负责主流流媒体协议遇到边缘场景就动态拉起一个 ffmpeg 进程。关于 USB 摄像头我想多说一句如果插在设备上的时候物理端口顺序变了/dev/video0可能变成/dev/video1导致 go2rtc 报设备不存在。比较靠谱的做法是给 USB 设备写 udev 规则或者直接用/dev/v4l/by-id/下的稳定链接。3.3 多源混合把局域网摄像头、本地 USB、远程拉流写进一个配置我的一个真实配置里同时包含了三类源局域网 RTSP 摄像头、USB 摄像头、另一个内网网段的 RTSP 拉流。完整的 yaml 大概是这个感觉log: level: info api: listen: :1984 streams: # 局域网海康 office_main: rtsp://admin:pass192.168.1.64:554/Streaming/Channels/101 office_sub: rtsp://admin:pass192.168.1.64:554/Streaming/Channels/102 # 局域网大华 yard_main: rtsp://admin:pass192.168.1.108:554/cam/realmonitor?channel1subtype0 # 笔记本 USB 摄像头 usb_cam: ffmpeg:v4l2:///dev/video0#videoh264 # 远程站点的 RTSP 拉流通过专线或内网打通 remote_site: rtsp://vision:secret10.0.0.20:554/Streaming/Channels/101 recordings: dir: /media/recordings多源混接时最容易被忽略的是码流设计。同一个摄像头的主码流和子码流主码流通常 4Mbps 以上、分辨率 4K/1080p子码流通常 512kbps 到 1Mbps、分辨率 D1/CIF。如果你只是用来实时监看完全没必要全部拉主码流手机端和预览墙可以统一用子码流只有需要清晰回放时才切主码流。这个习惯能省一大截交换机带宽。3.4 用 ONVIF 做设备级接入管理RTSP 地址虽然好写但当你面对一台不知道具体型号、不知道 RTSP 路径的陌生摄像头时手写地址就成了折磨。ONVIF 是安防设备的通用标准协议支持设备发现和媒体配置查询。go2rtc 的 Web UI 里可以启用 ONVIF 扫描对局域网内的 ONVIF 设备做自动发现。实际做法是先在配置里开一个 ONVIF 服务端iserver: onvif: listen: :8555然后在 Web UI 里就能看到 ONVIF 设备发现入口。扫描到设备后go2rtc 会自动列出该设备支持的媒体流地址你只需要填入账号密码它就能生成可用的拉流源。对大规模部署来说这个功能可以帮你省去逐个翻摄像头 Web 管理页面找 RTSP 地址的时间。不过要注意ONVIF 发现依赖设备开启 ONVIF 功能。海康摄像头的 Web 页面里有接入或网络服务选项里面有个ONVIF开关必须打开并且最好单独给 Onvif 用户授权。4. 浏览器零插件低延迟播放WebRTC 的正确打开方式4.1 为什么浏览器播放 RTSP 这么费劲现代浏览器基于安全考虑不会直接支持 RTSP 这种私有流协议。浏览器原生能播的格式是 HLS、DASH、渐进式 MP4、WebM 这些。过去各家安防厂商的做法是给浏览器装 ActiveX 或 NPAPI 插件Chrome 抛弃 NPAPI、Edge 抛弃 ActiveX 之后这套方案彻底退出历史舞台了。于是很多人转向 HLS服务端把 RTSP 转成 TS 切片浏览器用 hls.js 播放。HLS 的优势是兼容性极好劣势是延迟高。切片时长通常 2 到 6 秒播放器还要缓冲几个切片最终延迟十几秒很常见。对看回放无所谓对实时操控摄像头云台、看门口快递这种场景十几秒延迟能急死人。WebRTC 的思路完全不同。它走的是 P2P 通路浏览器通过 SDP 协商和 ICE 找到可用的 UDP 通道直接传输 SRTP 加密媒体流。因为不需要经过服务器转封装和切片缓冲端到端延迟通常在 0.3 到 1 秒。go2rtc 内置了 WebRTC 模块基于 Pion 实现对外暴露的接口就是api/webrtc?src...。4.2 播放地址的生成规则与 WebRTC 配置配置好源之后每一路源可以通过以下地址访问输出协议播放地址格式适用场景WebRTChttp://host:1984/api/webrtc?srccamera1实时预览低延迟优先HLShttp://host:1984/api/hls?srccamera1/index.m3u8兼容老设备或回放MJPEGhttp://host:1984/api/mjpeg?srccamera1嵌入式 iframe、图片刷新MP4http://host:1984/api/mp4?srccamera1duration60拉取最近时长的录像文件如果 WebRTC 连接建立不上排查顺序一般是先确认 8555/udp 是不是被防火墙挡了然后看 go2rtc 日志里 WEBRTC 相关的 candidate 信息确认客户端拿到的候选地址是不是宿主机的真实 IP。在 Docker 端口映射场景下如果容器内外网 IP 不一致WebRTC 的候选人会出现看起来有地址但连不上的灵异现象。这时候可以在配置里手动指定 candidatewebrtc: candidates: - 192.168.1.10:8555192.168.1.10 是宿主机 LAN 地址端口对应 Docker 映射出来的 8555。加了这条之后浏览器拿到的就是宿主机真实 IPUDP 通道能正常穿透。4.3 浏览器兼容性与 H265 转码问题H264 编码在几乎所有浏览器上都能通过 WebRTC 顺畅播放但 H265HEVC就不一样了。go2rtc 虽然可以直接拉 H265 的 RTSP 流但浏览器侧的 WebRTC 支持取决于浏览器是否内置 H265 解码能力。Chrome 桌面版对 HEVC 硬件解码的支持一直很暧昧所以如果你想省心播放源优先选 H264。配置了海康主码流却是 H265 的时候我一般直接在 go2rtc 里加一个供浏览器播放的转码副本streams: office_h265_raw: rtsp://admin:pass192.168.1.64:554/Streaming/Channels/101 office_browser: - rtsp://admin:pass192.168.1.64:554/Streaming/Channels/101 - ffmpeg:office_h265_raw#videoh264#video_paramspresetveryfast#video_paramscrf23浏览器播放office_browser这路VLC 或需保留原画质的系统拉office_h265_raw那路。这种一源二吃的做法在我的多设备场景里非常管用。代价是 FFmpeg 转码需要持续消耗 CPU如果转码分辨率很高建议在视频参数里限制输出尺寸比如加上#video_size1280x720。5. 录像、多路并发、分发与生产环境进阶玩法5.1 录像落盘MP4 文件自动保存go2rtc 支持录像功能配置里加一段recordings即可recordings: dir: /media/recordings然后每一路 stream 可以通过 URL 参数或 API 启停录像也可以配置按计划循环录像。默认情况下录像文件会以流的名字和时间为文件名保存为 MP4。对家用场景我倾向于不录主码流、只录子码流一天下来文件体积可控画面也足够事后看清楚事件全貌。录像文件的时间戳命名对后续脚本检索很重要。我在现场就是这么处理/media/recordings/office_sub/2025-01-20_14-30-00.mp4配合定时任务或外部脚本可以把超过 7 天的录像定期清理或转存冷备。go2rtc 本身不承担 NVR 的角色它更擅长把流安全地存下来至于怎么组织和检索留给传统的自动化脚本处理。5.2 一个源喂饱所有消费者fanout 机制实测这是 go2rtc 给我印象最深的地方。很多家用摄像头的 RTSP 并发连接数非常有限便宜的型号只能撑 4 到 6 路并发。如果全家五个人同时打开 App 看同一台摄像头摄像头的 CPU 直接拉满画面卡顿甚至直接重启。go2rtc 处理这个问题的方式是一个上游连接多路下游分发。它只和摄像头保持一路 RTSP 连接然后用 WebRTC、HLS、MJPEG 等协议分发给所有客户端。我实测过同一个摄像头源两个 WebRTC 浏览器客户端、一个 HLS 播放器、一个 MJPEG 轮询页面同时在跑摄像头端始终只有 go2rtc 这一路 RTSP 连接。这就让摄像头压力极低。对那种老旧海康、大华摄像头来说等于变相延长了使用寿命也让整个系统可以支撑更多的并发观看端。5.3 和自建页面、OBS、Home Assistant 联动的思路因为 go2rtc 的 API 都是标准 HTTP所以前端集成特别简单。我的自建监控页面里就是用video标签直接加载 WebRTC 流地址video autoplay muted controls playsinline/video script const video document.querySelector(video); video.src /api/webrtc?srcoffice_main; video.play(); /script如果你想把 go2rtc 的画面接入 OBS不推荐用浏览器捕获那一套直接在你自己的场景里用 MJPEG 源或 HLS 源添加媒体源OBS 就能稳定拉流。MJPEG 的延迟比 HLS 低一点画面变化不是特别频繁的监控场景完全够用。如果你玩 Home Assistantgo2rtc 可以说是它的标配兄弟组件。Home Assistant 的流媒体组件可以直接把 go2rtc 的源注册为摄像头实体然后在 HA 的仪表盘里展示实时画面同时还能联动自动化触发录像。这块展开又是另一篇文章我只说一句go2rtc 在设计之初就和 HA 有深度整合配置里那个homeassistant:区块不是摆设是给 HA 发现实体用的。6. 部署和生产使用中我踩过的坑6.1 RTSP 地址里的鉴权地狱海康、大华的 RTSP 鉴权有 Digest 和 Basic 两种方式go2rtc 都能处理但最头疼的是密码里有特殊字符。比如密码写成abc123URL 里的会被解析成用户名和密码的分隔符导致鉴权失败。正确做法是手动编码密码 abc123 - 编码为 abc%40123所以实际地址长这样streams: cam_auth_test: rtsp://admin:abc%40123192.168.1.64:554/Streaming/Channels/101另一个坑是旧款大华摄像头对 RTSP 地址里的特殊字符兼容性差有时候直接改 URL 还不生效需要去摄像头 Web 页面里改 RTSP 端口或子码流编号。遇到配置看起来没问题但就是拉不起来的情况先用 VLC 把完整 URL 试一遍VLC 能放就能确定问题是出在 go2rtc 侧VLC 也不能放就回到摄像头管理页面找原因。6.2 日志怎么看快速定位是哪一层出了问题go2rtc 的日志输出非常规范。在 docker compose 里用docker logs -f go2rtc能看到每一条拉流、每个播放会话的建立和关闭记录。排查问题时我习惯把日志级别临时调到 debuglog: level: debug然后重启或 HUP 一下。如果源地址有问题日志里会打印 RTSP 相关的错误如果是 WebRTC 连接失败会打印 WEBRTC 模块的错误。日志里还会显示每个 source 当前的连接数看到client2说明当前有两个客户端在看这路流。我最常遇到的问题是所有配置都正确但 go2rtc 拉流时报connection refused。这时候先 ping 摄像头 IP、再用 nc 探测端口nc -zv 192.168.1.64 554端口不通就直接去查摄像头到 go2rtc 这台设备的网络链路别在 yaml 配置里瞎调。6.3 常见故障速查表最后整理一个我在实际部署中积累的速查表方便你按图索骥现象大概率原因解决办法Web UI 能开但画面转圈8555/udp 未映射或防火墙拦截检查 UDP 8555 放行必要时配置 webrtc.candidates海康摄像头拉流提示 auth failRTSP 服务未开启或账号权限不足登录摄像头 Web开启 RTSP/ONVIF单独建一个专用于拉流的账号USB 摄像头显示 device not found设备节点变化或权限不足用 /dev/v4l/by-id/ 稳定链接把 go2rtc 容器加--device/dev/video0或privileged权限浏览器播放 H265 黑屏浏览器不支持 HEVC 解码添加 FFmpeg 转码副本输出 H264 供浏览器拉流录像文件为空或文件碎片录像目录权限不对确认 /media 挂载目录有写权限docker 容器内进程非 root 时用 chown 调整配置改动后不生效忘了 HUP 或重启docker kill -s HUP go2rtc观察日志确认 reload 完成布局到最后我再分享一个自己习惯的做法把所有摄像头的 RTSP 地址、账号、主/子码流信息集中写到一个单独的文件里在 go2rtc.yaml 里不直接填密文而是通过环境变量或启动参数注入。这样即使配置文件被截图或误发出去也不会直接把摄像头账号密码暴露给所有人。用 Docker 部署时可以在environment里挂载敏感变量配置里用${CAMERA_PASS}这种变量引用。等你接的摄像头超过 10 台就会理解这操作有多重要。