gb28181-rs· MIT · Rust 1.80+ · 手写 SIP,不依赖任何 SIP 框架
让 Rust 程序以设备身份注册到 GB/T 28181 平台:注册与摘要认证、目录应答、INVITE 点播、RTP/PS 推流、录像回放与下载,全部内置。宿主只需要提供两样东西:视频帧,和(可选的)录像索引。
| crate | gb28181-rs(crates.io) |
| 当前版本 | 0.5.1(0.6.0 加固版在途) |
| 许可证 | MIT |
| 协议版本 | GB/T 28181-2016 / 2022 |
| 关键依赖 | tokio、serde + serde-xml-rs、encoding_rs、md-5 + sha2 |
| 测试 | 134 个(含与 Go 孪生库的逐字节金串对齐测试) |
| 仓库 | https://github.com/mickeyzzc/gb28181-rs |
它补的是哪个空
Rust 的 GB28181 生态约等于零:crates.io 上只有一个 2023 年就弃坑的 gb28181 0.1.0(下载量一千五),以及 2026 年 7 月才冒头、尚无生产证据的实验性 siprs 系列。GitHub 上唯一活跃的 Rust GB28181 项目是一个平台应用,不是库。想在 Rust 里做设备端,此前只有一条路:自己手写。
gb28181-rs 不是按规范文档新写的,而是从我们一款 Rust 摄像头固件的生产实现中整体抽取而来——那套代码在真实平台面前跑了很久,README 里列的硬化清单(digest 认证的 URI 匹配、Via branch 唯一性、本机地址检测、MANSCDP 属性/元素双形态、TCP 传输、回放控制)每一项背后都是一次真机互操作故障。
设计:只留两条接缝
这个库的 API 哲学是"库管协议,宿主管业务",宿主侧要实现的东西收敛到两个 trait:
FrameSource(必须):直播帧的来源。库在 INVITE 到来时向你要一个订阅,你把编码器输出的访问单元推进通道即可;RecordingSource(可选):录像索引。实现它,平台就能查 RecordInfo、发起回放与下载。
其余一切——REGISTER 生命周期、401 摘要认证、保活、目录/设备信息/状态应答、SDP 解析、PS 封装、RTP 打包、回放节奏控制——都是库的事。
上手:三十行接入
[dependencies]
gb28181-rs = "0.5.1"
# git 方式: gb28181-rs = { git = "https://github.com/mickeyzzc/gb28181-rs.git", tag = "v0.5.1" }
use gb28181_rs::{FrameSource, FrameSubscription, RecordingSource, SegmentMeta,
Gb28181Config, Gb28181Server, set_record_active};
// 1) 直播帧:在你的采集管线枢纽上实现 FrameSource
struct MyFrameHub { /* ... */ }
impl FrameSource for MyFrameHub {
fn subscribe_with_capacity(&self, capacity: usize) -> FrameSubscription { /* ... */ }
fn unsubscribe(&self, id: u64) { /* ... */ }
}
// 2) 录像:在你的录像索引上实现 RecordingSource
struct MyRecordings { /* ... */ }
impl RecordingSource for MyRecordings {
fn lookup(&self, start_ms: u64, end_ms: u64) -> Vec<SegmentMeta> { /* ... */ }
}
// 3) 启动设备服务。停机是优雅的:收发循环、保活任务、
// 进行中的媒体任务都会依次停下。
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut config: Gb28181Config = toml::from_str(&std::fs::read_to_string("config.toml")?)?;
// 身份字段可由宿主配置,默认值是中性的(见下文)
config.user_agent = Some("my-host/1.0 (gb28181-rs)".to_string());
let mut server = Gb28181Server::start(
config,
std::sync::Arc::new(MyFrameHub { /* ... */ }),
Some(std::sync::Arc::new(MyRecordings { /* ... */ })),
).await?;
// ... 运行你的程序;退出时:
server.shutdown().await
}
没有现成的采集管线?库自带 MockFrameHub——一个带容量上限、写满即丢的参考 FrameSource 实现,先跑通信令再接真帧。
配置要点
Gb28181Config 是 serde 结构体,TOML/JSON 都能直接反序列化。默认值刻意取自规范文档的示例值(平台地址 192.168.1.1、设备 ID 34020000001320000001、密码 12345678、端口 5060)——如果启动时发现你还在用这些示例默认值,会打一条警告,避免"拿演示配置上了生产"这类事故。
身份字段全部可配且默认中性:user_agent 默认 gb28181-rs/<版本>,device_name 默认 Camera <设备ID>,厂商/型号默认 Unknown。也就是说,这个库不会在报文里替任何产品/厂商打广告,白标设备放心用。
能力细览
信令与认证。 手写的 GB/T 28181 SIP 子集(REGISTER/INVITE/MESSAGE/BYE/ACK/OPTIONS,UDP + TCP)加 SDP。摘要认证同时支持 MD5 与 SHA-256 算法、qop=auth,cnonce 用 CSPRNG 生成;user_agent、local 地址等身份要素都可配,不会把实现细节泄到报文之外。
MANSCDP。 Catalog/DeviceInfo/DeviceStatus/RecordInfo/Keepalive 的应答构造全都有;元素式与属性式双形态都发得出;入站报文按 UTF-8 或 GB2312/GBK/GB18030 自动识别,出站按规范惯例以 GB2312 编码。20 位国标设备 ID 有专门的格式解析模块(类型码 111=IPC、118=NVR)。
媒体。 H.264/H.265 NALU → MPEG-2 PS → RTP(UDP 与 TCP),SSRC、超 64KB 访问单元的有界 PES 切分、采集时间戳穿透,全部内置。PS 封装既可以走服务器的直播路径,也可以单独拿出来用:
let ps = gb28181_rs::mux_h264_to_ps(&[&sps, &pps, &idr], true, pts, dts);
回放与下载。 实现了 RecordingSource 之后:RecordInfo 查询、按实时节奏的回放推流、全速的下载推流,以及平台通过 SIP INFO 下发的 PlaybackControl(暂停/播放/拖动/倍速)都有完整处理。正在本地录像时调用 set_record_active(true),DeviceStatus 就会如实上报 <Record>ON</Record>。
录像段格式。 库内置一个参考格式:Annex-B 裸流 + 每帧一条 PTS 记录的 .ts.jsonl 边车文件,读取器随库提供。用它录像的宿主天然获得 RecordInfo/回放支持,零格式适配。
与 Go 孪生库逐字节对齐
GB28181 的媒体面没有"差不多兼容":PS 封装差一个字段,平台端解复用就可能丢帧。gb28181-rs 与姊妹库 gb28181-go 之间有一组金串测试,把封装输出钉死成十六进制常量,两个语言的实现必须产出逐字节相同的 PS 流:
/// 小型关键帧(SPS+PPS+IDR)—— 与 Go 孪生库逐字节一致
/// (pack header + PSM + 单个带时间戳的 PES)。
#[test]
fn golden_keyframe_matches_go_twin() {
let sps: Vec<u8> = vec![0x67, 0x42, 0x80, 0x28, 0xDA, 0x01, 0xE0, 0x08];
let pps: Vec<u8> = vec![0x68, 0xCE, 0x3C, 0x80];
let idr: Vec<u8> = vec![0x65, 0x88, 0x84, 0x00, 0x4B, 0x00, 0x01, 0x00];
let ps = mux_h264_to_ps(&[&sps, &pps, &idr], true, 90_000, 90_000);
const GOLDEN: &str = "000001ba440016fc8401009c43f8000001bb000b01000000041be000000000000001e0002bc00a310005bf21110005bf21000000000167428028da01e00800000168ce3c80000001658884004b000100";
assert_eq!(hex(&ps), GOLDEN);
}
同一组测试还覆盖 P 帧和 200KB 大访问单元的四段 PES 切分(每段声明长度逐字节核对)。实际意义:Rust 设备对接 Go 平台(以及任何以 Go 实现为参照的平台)的媒体兼容性不靠祈祷,靠断言。
真机实战
这个库的抽取过程本身就是一次全链路验证:从固件源码树抽出(宿主一侧删掉了六千七百余行),随即在真机上对 GB28181 平台跑完整回归——digest 注册、目录查询、INVITE、RTP-PS 解复用、H.264 录像落盘——全绿后才切换。此后作为 Rust 摄像头产品的 GB28181 栈日常生产运行。
一个反向的教训也值得记录:我们曾有另一个 Rust 项目直接复制了这份代码的早期版本。没有库边界之后,上游的互操作修复自然到不了它那里——短短几个月,它接连复现了上游早已修完的三类问题(digest 认证的 URI 与 Request-URI 不一致、MANSCDP 字段该用属性形态、MESSAGE 缺路由头)。复制即分叉,这正是这个库存在的理由。
工程质量
- 134 个测试:信令、编解码、媒体封装、回放节奏各有专门覆盖,另有库卫生(无 panic 路径等)与服务器生命周期集成测试;
- CI 三道闸:
cargo fmt --check、cargo clippy --all-targets -- -D warnings、cargo test;另有两个附加作业——MSRV 1.80 实际构建验证、Windows 交叉检查; - 打
v*标签即自动测试并发布到 crates.io; examples/四个演示,全部可以离线或本机回环跑通:ps_mux:PS 封装/解封装/大帧切分,纯离线;device_demo:手写假平台与真实设备服务在本机对接——注册、目录、INVITE、RTP/PS 解复用、BYE,平台侧还验证 digest 应答,成功退出码 0;playback_demo:RecordInfo、节奏化回放、PAUSE/倍速 PLAY、BYE;manscdp_demo:设备 ID 解析、保活构造、各类应答报文。
cargo run --example device_demo # 推荐从这个开始:两分钟看完协议全程
边界与路线
- 刻意不做:平台端/UAS 角色、SIP over TLS/WebSocket。设备端库把一件事做透。
- 0.6.0 加固版在途(分支待发布):无 panic 构造器、端口与身份可配置化、GB2312 出入站编解码、log 门面、优雅停机,外加库卫生源码扫描守卫(禁 print 宏/品牌/panic/实验室 IP)。
- README 如实标注:API 面已趋稳但尚未冻结;生产环境(我们的摄像头产品线)每日实战中。

1560

被折叠的 条评论
为什么被折叠?



