简介:一套以Go语言编写的跨平台摄像头流媒体服务端源码包,面向流媒体开发者、智能家居集成者及嵌入式设备开发者,解决多协议接入、低延迟传输与统一输出等问题。核心功能覆盖RTSP、RTMP、HTTP-FLV、WebRTC、MSE/HLS、MP4、MJPEG及HomeKit协议转换,支持FFmpeg实时转码、多源音轨混合与编解码协商,并可在Windows、macOS、Linux及ARM设备上零配置运行。压缩包共363个文件,体积约779KB,其中293个Go源码文件构成主要逻辑,另有Markdown文档、HTML演示页面、Dockerfile及YAML/JSON配置,便于阅读部署与二次开发。已有84人学习/下载,适合需要快速搭建流媒体网关或研究视频传输协议实现的开发者。包含完整测试用例和构建脚本,可辅助验证与扩展;通过研读源码,可掌握从拉流、转码到推流、播放的完整链路设计,以及零延迟传输和HomeKit摄像头接入等高级特性的实现方法。

1. 为什么先解决 RTSP 接入,再谈低延迟

做摄像头接入的人都有这个体验:RTSP 协议把 H.264/H.265 码流从摄像机拉到服务器很容易,但要用 Web 页面直接看却很难。浏览器不认 rtsp://,常见的解决办法是先转成 HLS,延迟却往往从几百毫秒恶化为三到十秒;而 WebRTC 延迟虽低,又要处理信令、DTLS、SRTP 和 ICE 协商。这套源码想解决的问题就是这个:把 RTSP、RTMP、DVRIP、HTTP-FLV、USB 摄像头等来源统一收进来,再按客户端能力自动输出 WebRTC、HLS、MSE/MP4、MJPEG 或 HomeKit 流。它不像 NVR 那样做录像存储,而是做协议翻译层。适合智能家居集成、边缘 ARM 设备上的摄像头接入、直播流测试,以及需要把摄像头画面推给 YouTube 或 Telegram 的场景。源码用 Go 编写,构建产物是单个可执行文件,你只要写好配置再启动,一张网卡上就能同时跑多个协议。


2. 从源码文件看核心模块:AnnexB、AMF 与 ONVIF 的边界

2.1 文件名暴露了模块划分

解开压缩包后先别急着跑,里面几个文件名很说明问题。 annexb_test.go 对应 H.264/H.265 的 Annex-B 格式处理; amf_test.go 对应 RTMP 的 AMF 元数据; onvif_test.go 对应摄像头自动发现; ffmpeg.go 则是外部转码进程的封装。一个流媒体网关要稳定,不是只靠 FFmpeg 转码,而是要把这些协议各自的“坑”在源码层消化掉。

annexb_test.go 值得先看。H.264 在 RTSP、TS 里通常以 Annex-B 形式存在,也就是用 00 00 00 01 分隔 NAL 单元;而 WebRTC、MSE 里用的是带长度前缀的 AVCC 格式。转换时最典型的问题是 SPS/PPS 丢失:有的摄像头只在流开始时发一次 SPS/PPS,WebRTC 播放端第一次收到的却是关键帧,画面就会黑屏。测试文件里针对这种边界情况做了覆盖,实际使用时要留意日志里是否出现 sps pps 缺失。

amf_test.go 处理的 AMF 是 RTMP 协议用来传递 onMetaData 、音频采样率、视频编码格式等信息的二进制结构。做 RTMP 推流时,解析错一个字段长度,后续视频帧全部错位。所以这类测试文件通常会在长度字段、字符串结束符、浮点编码这些地方下功夫。

我常用的模块对照表如下:

文件 对应能力 容易出现的问题
annexb_test.go H.264/H.265 Annex-B/AVCC 转换 SPS/PPS 缺失、SEI 放置位置错误
amf_test.go RTMP AMF0/AMF3 元数据解析 字符串长度溢出、ECMA array 解析
onvif_test.go ONVIF 设备发现与能力读取 多网卡时 WS-Discovery 发错接口
ffmpeg_test.go FFmpeg 进程生命周期与参数构造 参数转义错误、进程退出码未处理
ffmpeg.go 实际调用 FFmpeg 进行转换 超时机制缺失导致拉流挂死

2.2 输入适配器与输出端分离

从架构上看,这套程序把“输入”和“输出”完全解耦。输入适配器负责把 RTSP、RTMP、DVRIP、HTTP-FLV、MJPEG、USB 摄像头等来源转换成内部统一的音视频轨道;输出端则根据客户端请求,从内部轨道里取出数据再封装成对应协议。正因为这一层抽象,它可以做到不转码就完成跨协议转发:RTSP 拉到的 H.264,直接打包给 WebRTC;RTMP 推进来的 AAC,不需要解码再编码,直接送进 HLS。

关键点在于内部轨道记录的是编解码器信息和元数据,而不是具体协议。做这种设计需要大量测试来保证解析器健壮。比如 ONVIF 设备的媒体服务地址不一定在默认的 rtsp://ip:554/onvif1 ,需要通过 GetStreamUri 动态获取。使用 ONVIF 自动发现时,如果主机有多个网卡,WS-Discovery 探测包可能发到错误的网段,导致摄像头明明在同一局域网却找不到。此时我会先确认设备 IP 能通,再检查程序是否绑定在正确的网卡上。

调试输入源时,先用 FFmpeg 自带工具确认原始码流信息:

ffprobe -v error -show_streams -select_streams v:0 \
  -show_entries stream=codec_name,width,height,avg_frame_rate,pix_fmt \
  -rtsp_transport tcp "rtsp://192.168.1.64:554/stream1"

这里 -rtsp_transport tcp 强制使用 TCP 拉流,适合网络不稳定或 UDP 被防火墙拦截的情况; -select_streams v:0 只看第一个视频流。如果这里都出不来编码参数,后面无论走 WebRTC 还是 HLS 都不会正常。输出结果里看到 h264 和 yuv420p 是最省事的组合;看到 mjpeg 则说明设备只提供 JPEG 帧,需要再做一步 FFmpeg 转码才能进 WebRTC。

FFmpeg 适配层在源码里承担的是“最后兜底”角色。遇到不支持 H.264 的老旧摄像头,或需要把音频从 PCMU 转成 Opus 时,它会按需拉起 ffmpeg 进程。测试文件里大量覆盖的是退出码和超时等待,因为 FFmpeg 卡在 TCP 连接上不退出是拉流稳定性的大敌。


3. 快速上线:用配置文件把一个 RTSP 源同时发布到多协议

3.1 最小配置与启动流程

在 Windows 上可以直接执行 build.cmd 得到可执行文件;Linux 和 macOS 上,推荐直接用 Go 工具链构建,或者使用根目录的 Dockerfile 。 hardware.Dockerfile 则会把 FFmpeg 的硬件编解码器一起打进去,适合 Jetson、树莓派这类设备。

# 在本机构建 Linux x86_64 版本
go build -o go2rtc .

# 交叉编译 ARM64 版本,放到树莓派上跑
GOOS=linux GOARCH=arm64 go build -o go2rtc-arm64 .

build.cmd 与 Dockerfile 的本质都是先编译 Go 代码,再决定是否附带 FFmpeg。由于源码对外部依赖很少,交叉编译基本不会卡在 CGO 上;如果你的摄像头源只需要 RTSP 和 WebRTC,连 FFmpeg 都可以不要。

最小配置只需要一个 YAML 文件,我在实际项目里一般这样起:

# go2rtc.yaml
api:
  listen: ":1984"

rtsp:
  listen: ":8554"

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

streams:
  front_door:
    - "rtsp://admin:password@192.168.1.64:554/stream1?tcp"

api.listen 是控制面和播放面入口, rtsp.listen 让程序变成一个 RTSP 服务端,其他设备可以从 rtsp://localhost:8554/front_door 拉流。 streams 里的 front_door 是这个流的唯一 ID,后面所有 HTTP 请求都用 src=front_door 引用它。 ?tcp 是 RTSP 传输方式,建议默认开启,避免 UDP 丢包导致的马赛克。

启动后,立刻验证流是否已经处于运行状态:

curl -s http://127.0.0.1:1984/api/streams | python3 -m json.tool

这个接口返回的是一个 JSON 结构。如果 front_door 出现在 streams 列表里且状态是 running ,说明 RTSP 拉流成功;如果一直停在 waiting ,说明源还没连上,需要回去查 IP、端口、用户名或网络。也可以用浏览器直接打开 http://127.0.0.1:1984/ ,页面里能看到所有流和对应的播放链接,这对第一次使用的人最直观。

3.2 HTTP API 是多协议输出的统一入口

这套源码把 WebRTC、HLS、MSE、MP4、MJPEG 都挂在了同一个 HTTP 服务上,不同协议只是 URL 不同。我常用的端点如下:

播放协议 请求方式 适用场景
WebRTC GET /api/webrtc?src=front_door 实时监控、低延迟浏览器播放
HLS GET /api/hls?src=front_door 兼容性优先,VLC 可直接打开
MSE/MP4 GET /api/mse?src=front_door Safari/Chrome 上的低延迟播放
MJPEG GET /api/mjpeg?src=front_door 低成本画面预览,不支持音频
快照 GET /api/frame?src=front_door 抓帧做告警或健康检查

比如在 Home Assistant 里要显示摄像头画面,不一定需要完整的流媒体协议,直接请求一帧 JPEG 反而更省资源:

curl -o snapshot.jpg "http://127.0.0.1:1984/api/frame?src=front_door"

frame 端点会从内部轨道中取当前最新的一帧做 JPEG 编码, src 参数指定流 ID。用这个方法做“画面存活探测”很实用:写一个 cron 定时拉图,连续失败三次就重启流服务,能覆盖大多数摄像头死机场景。

HLS 播放时,延迟高低取决于切片长度和播放列表长度。如果源码的 HLS 实现允许配 hls 段参数,我会把切片缩到 0.5 到 1 秒,播放列表保留 3 片,这样延迟能压到 2 到 4 秒。需要强调的是,HLS 再怎么优化也做不到 WebRTC 那种低延迟,因为它本质上是 TCP 加切片,切片边界天然会引入缓冲。若要做门禁对讲、无人机画面这种交互场景,还是走 WebRTC 更合适。


4. 更低延迟:WebRTC 与 MSE/MP4 的选型边界

4.1 WebRTC 为什么是默认低延迟方案

WebRTC 直接走 UDP 上的 RTP/RTCP,配合 DTLS-SRTP 加密,浏览器端从收到第一个视频帧到渲染的时间非常短。源码在信令层做了一次封装,你并不需要自己维护 WebSocket 协商,直接用 /api/webrtc 就能获得一个可播放的会话。

在 SDP 协商里,源码会对摄像头支持的编码格式和浏览器端支持的编码格式做交集。只要两端都支持 H.264,就不会触发转码,CPU 占用很低;如果摄像头只支持 MJPEG,但浏览器请求的是 WebRTC,源码会启动 FFmpeg 转成 H.264 再推给浏览器。这个“自动匹配”逻辑是源码最值钱的部分,它让不同编码格式的摄像头能对客户端透明。

H.265 的情况比较特殊:Safari 在 WebRTC 里支持 H.265,Chrome 目前默认不支持。如果你的摄像头是 H.265 源,并且只在 iPhone、Mac 的 Safari 上播放,我会在流配置里显式保留原始 H.265,避免无谓转码:

streams:
  orchard_cam:
    - "rtsp://admin:pass@192.168.1.100:554/stream1?tcp"
    - "ffmpeg:orchard_cam#video=h265#video=copy#audio=copy"

这里 video=h265 告诉 FFmpeg 层优先使用 H.265, video=copy 表示不做视频重编码。这样 Safari 可以直接硬解 H.265,延迟非常低。其他浏览器打开同一个 URL 时,因为协商不到 H.265,系统会自动回退到 H.264 转码。需要提醒的是,H.265 转码对 CPU 要求高,单片树莓派跑 1080p H.265 软转码会比较吃力,最好用带硬件编码的版本。

4.2 MSE 和 MP4 是 WebRTC 之外的“降级选项”

MSE/MP4 走的是 HTTP 拉流,本质是把 fMP4 分段推给浏览器。它的延迟低于 HLS,但高于 WebRTC,因为仍然依赖 TCP 和播放缓冲。MSE 的优势是兼容性广、信令简单,不需要 WebSocket 协商,很多低功耗设备上比 WebRTC 稳定。

协议 传输方式 典型延迟 浏览器兼容性 是否支持音频
WebRTC UDP/DTLS-SRTP 0.1~0.4 秒 现代浏览器均支持 支持
MSE/MP4 HTTP 分段拉取 0.5~2 秒 Safari/Chrome/Firefox 支持
HLS HTTP 切片拉取 3~10 秒 几乎所有平台 支持
MJPEG HTTP 连续 JPEG 0.2~1 秒 所有平台 不支持

做技术选型时,我的原则是:对讲、报警联动、云台操控画面用 WebRTC;常规回放和兼容性演示用 MSE;多平台播放器兼容测试用 HLS。MJPEG 只在纯画面预览场景使用,比如卡片式摄像头列表,因为它每一帧都是完整 JPEG,带宽消耗大,但解码器负担小。

多路音轨混合是另一个常被忽略的细节。源码允许多个来源同时出现在同一个流定义里,例如把摄像头的内置麦克风和门口对讲机的音频合并成一路。实际操作时,我会让 FFmpeg 做 amix 混音,并对音量做归一化。这样 WebRTC 客户端不需要知道音频来自哪几个设备,拿到手就是一条稳定的 AAC 或 Opus 流。

如果 WebRTC 画面卡顿,优先怀疑的不是编码器,而是 ICE 网络路径。局域网内不需要配置 STUN;跨设备、跨网段访问时,要在配置里加 STUN 服务地址。公网访问如果 NAT 类型严格,还需要 TURN。没有 TURN 时,WebRTC 会在 STUN 失败后尝试 TCP 中继,延迟会上升,但至少能通。


5. 把周边能力接起来:FFmpeg、USB 摄像头、HomeKit 与外网访问

5.1 FFmpeg 适配层怎么处理 USB 摄像头

源码的输入源不只针对网络摄像头。USB 摄像头、本地视频文件、甚至电视采集卡都能通过 FFmpeg 进入统一轨道。配置里用 ffmpeg: 前缀,后面跟设备和参数。

streams:
  usb_cam:
    - "ffmpeg:video=/dev/video0#video=h264#audio=0"
  recording_file:
    - "ffmpeg:file=/data/capture.mp4#video=copy#audio=copy"

video=/dev/video0 指定 Linux 下的 v4l2 设备, audio=0 表示不需要音频,避免 USB 摄像头没有麦克风时 FFmpeg 卡住; video=h264 表示强制输出 H.264,内部轨道就会认为这是一个 H.264 源。 file= 则用来读本地文件,当文件里已经是 H.264/AAC 时用 copy 直接封装,不转码。

不同操作系统下设备名不一样,我常用表如下:

操作系统 视频输入参数 音频输入参数
Linux video=/dev/video0 audio=hw:0 或 audio=0
macOS video=0 audio=0
Windows video=USB Camera audio=Mikrofon

在 Windows 下设备名常常带空格,配置里最好用引号包住。遇到 USB 摄像头输出格式不是 YUYV 或 MJPEG 的情况,FFmpeg 层会自动处理像素格式转换,但要注意 CPU 占用会明显上升。此时我会在硬件 Docker 镜像里使用硬件编码器,例如 h264_v4l2m2m ,把编码工作从 CPU 卸载到 VPU。

5.2 HomeKit 接入与外网穿透

这套源码的卖点之一是“全球首个支持 HomeKit 摄像头流”。HomeKit 摄像头走的是 HAP 协议,视频流协商过程和 WebRTC 完全不同。初次启动时,程序会在日志里打印配对码或二维码,用 iOS 家庭 App 扫描即可添加。添加成功后,iOS 通过 HomeKit 的媒体协商拿到 RTSP 源的 H.264 流,原生渲染,不需要额外 App。

典型配置是把指定 RTSP 源开放给 HomeKit:

streams:
  living_room:
    - "rtsp://admin:pass@192.168.1.22/stream1?tcp"

homekit:
  streams:
    - living_room

homekit.streams 列出允许被 HomeKit 扫描到的流 ID,避免把实验室或公共区域摄像头暴露到家庭中枢。需要注意,HomeKit Secure Video 的录像和转码功能依赖 Apple TV 或 HomePod 中枢,这套源码只负责把实时视频暴露给 HomeKit,不会替你完成云端录像。

外网访问时,最直接的办法是通过 ngrok 把 API 端口暴露出去。需要注意的是 WebRTC 默认走 UDP,而普通的 HTTPS 隧道只转发 TCP。我会先让 ngrok 转发 1984 端口,再在 WebRTC 配置里限制 ICE 使用 TCP relaying,这样公网浏览器也能建立会话。代价是走 TCP 中继时延迟比局域网 UDP 路径高一些,但至少可观看。

ngrok http 1984

ngrok 返回的域名会指向本地 API 服务。之后把 http://your-ngrok-domain/api/webrtc?src=living_room 发给远程协作者,对方就能在浏览器里看到实时画面。这个方法同样适合临时调试 Home Assistant 的外部接入,不需要改路由器端口映射。


6. 实际操作:从延迟验证到 RTSP 拉流排错

先验证延迟。最笨但有效的方法是对着摄像头拍一个毫秒级秒表,然后用手机或另一块屏幕打开 WebRTC 画面,拍下延迟差。局域网内 WebRTC 正常应该在 300 毫秒以内,HLS 在 3 到 5 秒之间。如果 WebRTC 超过 1 秒,看两件事:第一,是否走了 TURN 中继;第二,浏览器是否因为 CPU 不足关闭了硬解,导致解码缓存增加。

RTSP 拉流不成功是最常见的问题。日志里出现 read tcp ... i/o timeout ,原因通常是 TCP 连接建立后摄像头没回 RTSP 响应,此时换用 FFmpeg 适配层拉流,看是否能突破设备兼容限制。另一个常见错误是 RTSP URL 里密码包含 @ 或 : ,没有做 URL 编码。比如密码 p@ss 要写成 p%40ss ,否则解析器会在第一个 @ 处误判主机地址。

# 先确认摄像头码流能正常用 ffplay 打开吗
ffplay -rtsp_transport tcp -fflags nobuffer "rtsp://admin:pass@192.168.1.64/stream1"

-fflags nobuffer 减少 ffplay 的缓冲时间,便于快速判断卡顿是源端问题还是播放端问题。如果 ffplay 播放流畅,但走 WebRTC 卡顿,问题大概率在 ICE、带宽或浏览器解码;如果 ffplay 本身马赛克,就要考虑摄像头子码流分辨率过高、无线信号丢包等问题。

日志里还有一些关键词值得关注:

日志片段 含义 处理方向
codec: h264 已经识别出编码格式 无需处理
webrtc: candidate ICE 候选地址交换 检查是否有公网可路由地址
ffmpeg: exit code 1 FFmpeg 进程退出 查看 FFmpeg stderr 输出
no supported codec 编解码协商失败 手动转码或更换容器格式

遇到 no supported codec 时,先确认源端输出到底是什么格式。老旧摄像头常见的是 MJPEG over HTTP,这种源直接进 WebRTC 会被拒绝,正确做法是用 FFmpeg 拉 HTTP 并把视频转成 H.264:

# 直接从 HTTP 抓帧转 H264,再进入内部轨道
ffmpeg -i "http://192.168.1.64/mjpeg" -c:v libx264 -preset veryfast \
  -maxrate 1500k -bufsize 3000k -f mpegts "udp://127.0.0.1:1234"

maxrate 和 bufsize 是 CBR 风格码率控制的关键参数, maxrate 决定峰值码率, bufsize 决定解码器缓冲区大小。设置不当会出现画面快速运动时糊成一片。转码完成后,再把 tcp://127.0.0.1:1234 作为源填入流配置,就能把不支持 H.264 的摄像头接进统一网关。

最后留下一个我常用的检查命令:启动程序后,用 curl 拉取流状态,再用 ffprobe 从网关的 RTSP 输出端口确认一遍编码参数。

curl -s http://127.0.0.1:1984/api/streams | python3 -c "import sys,json; print(json.load(sys.stdin)['front_door'])"

看到 "type": "active" 说明拉流正常;随后用 ffprobe rtsp://127.0.0.1:8554/front_door 再次确认输出流里 codec 是否为 h264 。如果输出流变成 h264 但源明明是 MJPEG,说明 FFmpeg 转码链路已经生效;如果输出流是空,就回到源端排查网络和 RTSP URL。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

邀请您加入社区

更多推荐