go2rtc Bubble 私有流协议解析:从 dvr163 / eseecloud 摄像头接入到帧级协议实现
【免费下载链接】go2rtcUltimate camera streaming application项目地址: https://gitcode.com/GitHub_Trending/go/go2rtc
本篇以 go2rtc 仓库中的 internal/bubble/README.md 为主体,完整覆盖 Bubble 私有流格式的接入配置(URL 语法、参数缺省规则),并结合 pkg/bubble/client.go 与 pkg/bubble/producer.go 的源码实现,讲清从 TCP 握手、认证包构造、媒体包解包到 RTP 封帧的完整链路。读完后你将能够:为 dvr163.com / eseecloud.com 系设备正确配置bubble://源,理解该私有协议的报文结构与默认值来源,并知道 go2rtc 如何把它转成内部可复用的视频/音频轨。
1. Bubble 是什么
Bubble 是少数 dvr163.com 与 eseecloud.com 品牌摄像头/NVR 使用的私有流传输格式(内部包注释中给出的线索:请求 URL 为/bubble/live?ch=0&stream=0,响应Content-Type为video/bubble,见 client.go 的包注释)。它对外的传输载体是普通 TCP + HTTP 请求头,但其后的数据流是自定义的二进制包头封装,既不是标准 RTSP 也不是 MJPEG,因此需要专门的 producer 才能被 go2rtc 拉流。
go2rtc 在 v1.6.1 版本引入了该格式(README 中标注new in v1.6.1)。协议支持视频编码为 H.264 或 H.265(HEVC),音频为 G.711 A-law(PCMA),这一点可以从协议实现中交叉印证:pkg/README.md 的协议一览表里bubble一行的编码列为h264, hevc, pcm_alaw,传输协议列为http。
2. 快速开始:配置一个 bubble 源
按照 internal/bubble/README.md 给出的配置规则:
username、password、port、ch、stream在取默认值时可以省略;- 不同通道 / 不同码流应配置成不同的 stream 条目。
streams: camera1: bubble://username:password@192.168.1.123:34567/bubble/live?ch=0&stream=0URL 各部分的含义与默认行为(以源码为准):
| 字段 | 示例 | 说明 |
|---|---|---|
| scheme | bubble:// | 触发 bubble 协议的 URL 处理器,见 internal/bubble/bubble.go |
| username / password | username:password | URL userinfo 中的账号密码;源码中当 URL 未携带用户时,用户名默认填充为admin(client.go) |
| host | 192.168.1.123 | 设备地址 |
| port | 34567 | 设备私有流端口,README 说明其为默认值时可省略 |
| path | /bubble/live | 私有 HTTP 请求路径,设备端据此返回video/bubble |
ch | 0 | 通道号,对应源码中的c.channel,默认 0 |
stream | 0 | 码流号(如主/子码流),对应源码中的c.stream,默认 0 |
ch与stream的解析逻辑在 Dial() 中:先解析stream(缺省为"0"),再据此从设备返回的 XML 中匹配对应码流条目,最后解析ch。
配置完成后,该 stream 即可被 go2rtc 的标准消费端复用(RTSP、WebRTC、HLS 等),这与其它协议源的行为一致;协议本身只负责“生产”媒体轨。
3. 协议注册:从 main.go 到 bubble.Dial
Bubble 的接入遵循 go2rtc “internal 薄壳 + pkg 协议实现”的分层:
- 程序入口 main.go 中注册了
{"bubble", bubble.Init}; - internal/bubble/bubble.go 的
Init()调用streams.HandleFunc("bubble", ...),把bubble://这一 scheme 映射到 producer 构造函数; - 构造器直接调用 pkg/bubble 的 Dial() 完成拨号,返回的
*Client实现了core.Producer接口,交由 streams 层管理生命周期。
*Client内嵌core.Listener并提供GetMedias()/GetTrack()/Start()/Stop()/Close()等接口(client.go),源码注释标注其为旧式接口(Deprecated: should be rewritten to core.Connection),从源码结构看后续版本可能迁移到core.Connection抽象。
4. 连接与认证流程(Dial 阶段)
Dial() 实现了整个握手,共四步,全部超时统一为 5 秒(const Timeout = time.Second * 5):
第 1 步:TCP 连接 + HTTP GET。解析 URL 后net.DialTimeout建立 TCP 连接,然后用 go2rtc 自实现的轻量 HTTP 客户端发送GET /bubble/live?ch=0&stream=0 HTTP/1.1请求(client.go),要求响应状态为 200,否则报wrong response。
第 2 步:读取 1024 字节 XML 描述。响应体先读固定 1024 字节,源码注释特别指出:部分设备恰好返回 1024 字节,但有些设备返回 923 字节,因此这里是“尽力读取”而非精确按长度读取(client.go)。该 XML 描述了设备能力,形如:
<bubble version="1.0" vin="1"><vin0 stream="2"> <stream0 name="720p.264" size="2304x1296" x1="yes" x2="yes" x4="yes" /> <stream1 name="360p.265" size="640x360" x1="yes" x2="yes" x4="yes" /> <vin0> </bubble>(见 client.go 的注释示例。)
第 3 步:发送 48 字节认证包。结构为size uint32(=44) + 未知 4 字节 + username 20 字节 + password 20 字节,用户名不足 20 字节以零填充;未提供用户名时默认写admin(client.go)。该包以PacketAuth(0x00)命令字、固定时间戳0x0E16C271封装发出。
第 4 步:校验认证响应并解析码流编码。设备回一个 44 字节负载的PacketAuth包,源码校验b[4] == 3 && b[8] == 1两个魔数,不满足则报wrong auth response(client.go)。随后用正则<stream{N} [^>]+从 XML 中取出所选码流条目,按名字是否包含.265决定视频编码为 H.265 还是 H.264(client.go)。
命令字常量定义在 client.go:
| 常量 | 值 | 用途 |
|---|---|---|
SyncByte | 0xAA | 每个包的同步头 |
PacketAuth | 0x00 | 认证包 |
PacketMedia | 0x01 | 媒体数据包 |
PacketStart | 0x0A | 开始播放(Play)命令 |
5. 报文封装:Write / Read
Bubble 所有二进制交互共用同一套 10 字节包头封装,大端序(client.go):
0xAA | size uint32 | cmd byte | timestamp uint32 | payload (size = 5 + len(payload))Write(command, timestamp, payload)负责按上述布局打包并写 socket;Read()先读满 10 字节头,校验首字节必须为0xAA(否则wrong start byte),再按size - 1 - 4读出 payload,返回命令字与负载。
包头中size字段统计的是“cmd + timestamp + payload”的长度(即5 + len(payload)),这是解析该协议时最容易被算错的一处。
6. 开始播放与媒体数据处理(Play / Handle)
Start()的调用链是Play()→Handle()(producer.go)。
Play 命令。Play() 发送PacketStart(0x0A)包,16 字节负载为小端序三个 uint32:channel、stream、1(表示 opened),时间戳魔数为0x0E16C2DF。源码注释特别强调“yeah, there's no mistake about the little endian”——认证与包头是大端,而 Play 负载是小端,属于该私有协议的既定怪异之处。
Handle 主循环。Handle() 无限读取包,仅处理PacketMedia,其余命令字直接丢弃。媒体包 payload 的前 6 字节为公共头:size uint32 + type 1b + channel 1b,其中type的语义为1= 关键帧、2= 普通帧、0= 音频。
- 视频(b[4] > 0):
b[6:]是 AnnexB 格式的一帧,经annexb.EncodeToAVCC转成 AVCC(长度前缀)格式后封装为 RTP 包(90kHz 时间戳)写入视频轨; - 音频(b[4] == 0):负载前 36 字节是音频元信息头(条目数、大小、PTS、G.711 魔数
g711、采样率、采样宽度,见 client.go 的注释),实际 PCM A-law 数据从b[6+36:]开始,按帧长累加 RTP 时间戳后写入音频轨。
只有在对应 receiver 已建立(videoTrack/audioTrack非 nil)时才写帧,未就绪的帧直接丢弃。
7. Producer 接口:媒体轨声明与 API 输出
GetMedias() 向 streams 层声明两条只接收(recvonly)媒体轨:
| 轨 | 编码 | 时钟 | 负载类型 |
|---|---|---|---|
| 视频 | H.264 或 H.265(Dial 阶段由 XML 判定) | 90000 Hz | PayloadTypeRAW(RAW 视频负载) |
| 音频 | PCMA(G.711 A-law) | 8000 Hz | PayloadType 8 |
GetTrack()按需创建core.Receiver并回填videoTrack/audioTrack指针,供 Handle 主循环写帧(producer.go)。Stop()则先关闭所有 receiver 再断连。
MarshalJSON() 把连接状态序列化进 go2rtc 的/api接口:format为bubble、protocol为http,并附带远端地址与累计接收字节数(Recv),便于在 API/Net 页面观察这条私有流的实时状态。
bubble也已列入 www/schema.json 的合法 scheme 白名单,Web UI 中配置时可获得语法校验。
8. 使用限制与注意事项
- 超时:拨号、每次读写均使用 5 秒超时(
Timeout常量),弱网环境下认证失败会以超时错误呈现; - 设备兼容性:1024 字节 XML 头部存在“923 字节”的变体设备,解析依赖设备在头部返回正确的
<streamN>条目,若设备固件行为偏离,Dial 阶段会报wrong auth response或正则匹配失败; - 编码范围:仅支持 H.264/H.265 视频 + G.711 A-law 音频(
GetMedias的声明范围),其余编码的 bubble 设备不在当前实现覆盖内; - 同品牌其它协议:eseecloud 设备另有标准的 eseecloud:// 接入方式(
eseecloud://user:pass@host:80/livestream/12),当设备走标准流时优先使用该协议;bubble 面向的是私有video/bubble数据面。
9. 相关文件索引
| 文件 | 作用 |
|---|---|
| internal/bubble/README.md | 本文主体:bubble 源的配置说明 |
| internal/bubble/bubble.go | 注册bubble://scheme |
| pkg/bubble/client.go | 连接、认证、报文读写、Play/Handle 核心实现 |
| pkg/bubble/producer.go | 媒体轨声明、producer 生命周期与 JSON 状态 |
| main.go | 模块初始化注册点 |
| pkg/README.md | 协议能力一览表(bubble 行) |
【免费下载链接】go2rtcUltimate camera streaming application项目地址: https://gitcode.com/GitHub_Trending/go/go2rtc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考