简介:JGB28181 是一套基于 Java 实现的 GB28181 国标平台源码,面向安防监控、视频联网领域的 Java 开发者与学习者,用于理解并实践国标信令交互与媒体流传输。资源包共 49 个文件,以 43 个 java 源文件为主体,辅以 xml、properties、yml 等配置与构建文件,整体仅 64KB,结构轻量、便于快速导入 IDE 阅读调试。使用前只需修改 config.properties 中的配置信息,编译运行即可启动。当前版本已实现设备注册、恢复、目录查询以及实时视频流(TCP 被动 / UDP)等核心功能,覆盖国标平台最常用的信令流程。已有 4241 人学习下载,说明其在同类开源实现中具备一定参考价值。对于希望深入理解 GB28181 协议栈、搭建视频监控接入平台或进行二次开发的读者,这份源码可作为入门与对照实践的起点,帮助梳理注册鉴权、目录订阅与流媒体协商等关键环节的实现思路。
1. 从一次对接失败说起:JGB28181 到底能帮你省掉多少事
去年帮一个做安防集成的朋友看问题,他们要把一批网络摄像机接入自研的监控客户端,协议用的是 GB28181。结果 SIP 注册一直失败,抓包看信令来回都对不上,折腾了三天才发现是 XML 里的设备编码字段拼错了。当时我就想,如果有一套现成的 Java 平台能直接跑起来,把注册、心跳、目录查询、实时点播这些流程都封装好,哪至于在协议细节上耗这么久。JGB28181 就是冲着这个场景来的——基于 Java 实现的 GB28181 平台,把 SIP 信令交互和媒体流处理的核心链路都搭好了,你拿到手就能跑,不用从零啃 RFC 和国标文档。它适合两类人:一是需要快速搭建视频监控接入平台的开发者,二是想拿一套可运行的代码来学习 GB28181 协议栈怎么落地的人。下面我按实际拆项目的顺序,从架构到运行再到改造,把这份资源讲透。
2. 架构拆解:SIP 信令层与媒体流处理怎么分工
2.1 整体分层与模块职责
JGB28181 的代码结构是典型的分层设计,核心思路是把信令和媒体拆开,各管各的。信令层负责 SIP 消息的收发、解析和状态维护,媒体层负责 RTP 包的接收、转发和流媒体服务对接。这种拆分的好处是,你改信令逻辑时不用动媒体代码,换流媒体方案时也不用碰 SIP 栈。
从目录结构看,大致会分成这么几块:
src/main/java/ ├── sip/ # SIP 协议栈,注册、心跳、目录、点播信令 ├── media/ # RTP 收流、转发、流媒体服务适配 ├── service/ # 业务逻辑,设备管理、通道管理、会话控制 ├── config/ # 配置加载,SIP 参数、媒体参数 ├── handler/ # 各类 SIP 消息处理器 └── util/ # XML 解析、编码转换、工具类sip包是入口,设备注册、心跳维持、目录查询这些信令都从这里进出。media包处理实际视频流,通常会用 Netty 或 MINA 做 RTP 的接收和转发。service包把信令和媒体串起来,比如收到 INVITE 请求后,创建会话、分配端口、启动收流。handler包是策略模式,每种 SIP 消息类型对应一个处理器,方便扩展。
我一般会先看sip和service这两个包,因为它们决定了平台能不能正常跟设备交互。media包反而可以晚点看,除非你要改流媒体转发逻辑。
2.2 SIP 信令栈的启动与注册流程
SIP 栈的启动是整个平台跑起来的第一步。通常会在一个SipLayer或SipServer类里初始化监听端口、加载配置、注册消息处理器。下面是一段典型的启动代码结构:
// SipServer.java public class SipServer { private SipStack sipStack; private SipProvider sipProvider; public void start() throws Exception { // 1. 创建 SIP 协议栈 SipFactory sipFactory = SipFactory.getInstance(); sipFactory.setPathName("gov.nist"); sipStack = sipFactory.createSipStack(properties); // 2. 创建监听点,绑定 IP 和端口 ListeningPoint lp = sipStack.createListeningPoint( config.getSipIp(), config.getSipPort(), "udp"); // 3. 创建 SipProvider,注册消息监听 sipProvider = sipStack.createSipProvider(lp); sipProvider.addSipListener(new SipListenerImpl()); // 4. 启动媒体服务 mediaServer.start(); } }这段代码做了四件事:初始化 SIP 协议栈、绑定监听端口、注册消息监听器、启动媒体服务。config.getSipIp()和config.getSipPort()是从配置文件读的,一般放在application.yml或sip.properties里。SipListenerImpl是核心入口,所有收到的 SIP 消息都会进到它的processRequest和processResponse方法。
设备注册流程是这样的:设备发 REGISTER 请求,SipListenerImpl收到后交给RegisterHandler处理,校验设备编码和认证信息,然后返回 200 OK。注册成功后,平台会记录设备状态,后续的心跳和目录查询都基于这个状态。这里有个容易翻车的点:设备编码必须和 SIP 用户名一致,否则认证过不去。我见过有人把设备编码写成 IP 地址,结果注册一直 401。
2.3 媒体流接收与转发链路
媒体流这块,JGB28181 通常会用 Netty 起一个 UDP 服务来收 RTP 包。设备在 INVITE 协商成功后,会把视频流推到平台指定的 IP 和端口。平台收到 RTP 包后,可以转推给流媒体服务器(比如 ZLMediaKit 或 SRS),也可以直接转发给客户端。
// MediaServer.java public class MediaServer { private EventLoopGroup group; private Channel rtpChannel; public void start() { group = new NioEventLoopGroup(); Bootstrap b = new Bootstrap(); b.group(group) .channel(NioDatagramChannel.class) .handler(new RtpHandler()); // 自定义 RTP 处理器 // 绑定收流端口,通常是一个端口范围 rtpChannel = b.bind(config.getMediaPort()).sync().channel(); } } // RtpHandler.java public class RtpHandler extends SimpleChannelInboundHandler<DatagramPacket> { @Override protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) { byte[] data = packet.content().array(); // 解析 RTP 头,提取 SSRC 和序列号 RtpPacket rtp = RtpParser.parse(data); // 根据 SSRC 找到对应的会话,转发到流媒体服务 Session session = SessionManager.getBySsrc(rtp.getSsrc()); if (session != null) { mediaForwarder.forward(session, rtp); } } }MediaServer负责启动 Netty 的 UDP 服务,绑定收流端口。RtpHandler是核心处理器,每收到一个 RTP 包就解析头部,根据 SSRC 找到对应的会话,然后转发。SSRC 是 RTP 流的唯一标识,在 INVITE 协商时由平台分配,设备推流时会带上这个值。如果 SSRC 对不上,流就找不到对应的会话,表现就是点播成功但画面出不来。
转发这块,常见做法是把 RTP 包转成 RTSP 或 RTMP 推给流媒体服务器,再由流媒体服务器分发给客户端。这样平台本身不用处理复杂的流分发逻辑,专注做信令和收流就行。如果你不想引入额外的流媒体服务,也可以直接在平台里做 RTP 转发,但并发上来后性能会吃紧。
3. 跑起来:从配置文件到第一个设备上线
3.1 环境准备与依赖安装
跑这个项目之前,先把环境理清楚。JDK 版本建议用 8 或 11,因为 SIP 栈和 Netty 的版本通常跟这两个兼容性最好。构建工具用 Maven,依赖管理方便,pom.xml里会把 SIP 栈、Netty、XML 解析这些库都列好。
# 检查 JDK 版本 java -version # 检查 Maven mvn -version # 克隆项目后,先编译 mvn clean package -DskipTests编译时如果报 SIP 栈相关的包找不到,大概率是 Maven 仓库里没有对应的依赖,需要手动装或者换仓库地址。我一般会先跑mvn dependency:resolve看看依赖能不能全部拉下来,省得编译到一半卡住。
数据库这块,项目通常会用一个轻量级的库来存设备信息和会话记录,比如 H2 或 SQLite。如果是 MySQL,需要提前建库建表,建表语句一般在resources/sql目录下。配置文件里要改数据库连接信息,不然启动时会报连接失败。
3.2 配置文件详解与参数调整
配置文件是跑起来的关键,改错一个参数就可能连不上设备。核心配置项大概有这些:
# application.yml sip: ip: 192.168.1.100 # 平台监听的 IP,必须是设备能访问到的地址 port: 5060 # SIP 端口,默认 5060 domain: 3402000000 # SIP 域,通常用行政区划编码 id: 34020000002000000001 # 平台编码,20 位 media: ip: 192.168.1.100 # 媒体收流 IP port: 30000 # 收流起始端口 port-range: 100 # 端口范围,支持 100 个并发流 device: heartbeat-timeout: 60 # 心跳超时时间,秒 register-expires: 3600 # 注册有效期,秒sip.ip和media.ip一般设成同一个,就是平台所在服务器的内网 IP。如果服务器有多个网卡,一定要写设备能访问到的那个,不然设备注册不上。sip.domain和sip.id要符合 GB28181 的编码规则,前 10 位是行政区划,后面是行业编码和序号。设备端配置的 SIP 服务器地址和端口要跟这里一致,否则信令对不上。
media.port和media.port-range决定了能同时接多少路流。每路流会占用一个端口,范围设小了并发上来后新流就分配不到端口。我一般会按实际并发数的 1.5 倍来设,留点余量。
3.3 启动服务与设备注册验证
配置改好后,直接启动主类。启动日志里会打印 SIP 栈的监听地址和媒体服务的端口范围,看到这两行基本就说明服务起来了。
# 启动项目 java -jar target/jgb28181.jar # 或者用 Maven 直接跑 mvn spring-boot:run启动成功后,拿一个支持 GB28181 的摄像机或模拟器来注册。设备端要填 SIP 服务器 IP、端口、设备编码、认证密码。注册成功后,平台日志会打印Device registered: 设备编码。如果注册失败,先看平台日志有没有收到 REGISTER 请求,收到了就是认证问题,没收到就是网络或端口问题。
验证设备是否在线,可以调平台的目录查询接口,或者直接看日志里的心跳记录。心跳是设备定期发的,平台收到后会更新设备状态。如果心跳超时,设备会被标记为离线,这时候点播会失败。
提示:第一次跑建议先用 GB28181 模拟器测试,别直接上真实摄像机。模拟器可以手动控制注册、心跳、点播这些流程,排查问题方便得多。
4. 核心模块拆解:一个 INVITE 请求从进入到出流
4.1 INVITE 信令处理与 SDP 协商
点播是 GB28181 里最核心的流程,涉及 INVITE 信令和 SDP 协商。客户端发起点播请求,平台收到后构造 INVITE 消息发给设备,设备返回 200 OK 带 SDP,平台再回 ACK 确认,然后设备开始推流。
// InviteHandler.java public class InviteHandler implements SipHandler { @Override public void handle(RequestEvent event) { Request request = event.getRequest(); // 1. 解析请求,获取设备编码和通道编码 String deviceId = SipUtils.getDeviceId(request); String channelId = SipUtils.getChannelId(request); // 2. 构造 INVITE 消息的 SDP String sdp = SdpBuilder.build( config.getMediaIp(), allocatePort(), // 分配收流端口 SessionManager.generateSsrc() // 生成 SSRC ); // 3. 发送 INVITE 给设备 ClientTransaction ct = sipProvider.getNewClientTransaction( SipUtils.buildInvite(deviceId, channelId, sdp)); ct.sendRequest(); // 4. 记录会话,等待设备推流 Session session = new Session(deviceId, channelId, ssrc, port); SessionManager.add(session); } }这段代码的关键在 SDP 构造。config.getMediaIp()是平台收流地址,allocatePort()从端口范围里分配一个空闲端口,generateSsrc()生成一个唯一标识。这三个值会通过 SDP 告诉设备,设备推流时就按这个地址和 SSRC 来发。如果 SDP 里的 IP 写错了,设备会把流推到别的地方,平台自然收不到。
SDP 协商还有个细节:y=字段(SSRC)必须和平台记录的一致。有些设备不认这个字段,会自己生成 SSRC,这时候平台就得靠端口来匹配会话,而不是 SSRC。我一般会在代码里做兼容,先按 SSRC 找,找不到再按端口找。
4.2 RTP 收流与 SSRC 匹配逻辑
设备推流后,RTP 包会打到之前分配的端口上。RtpHandler收到包后,先解析 RTP 头,拿到 SSRC,然后去SessionManager里找对应的会话。
// RtpParser.java public class RtpParser { public static RtpPacket parse(byte[] data) { // RTP 头前 12 字节是固定字段 int version = (data[0] >> 6) & 0x03; int payloadType = data[1] & 0x7F; int sequence = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF); long ssrc = ((long)(data[8] & 0xFF) << 24) | ((data[9] & 0xFF) << 16) | ((data[10] & 0xFF) << 8) | (data[11] & 0xFF); byte[] payload = Arrays.copyOfRange(data, 12, data.length); return new RtpPacket(version, payloadType, sequence, ssrc, payload); } }RTP 头的解析是固定格式,版本号、负载类型、序列号、SSRC 各占几个字节。SSRC 在偏移 8 到 11 字节的位置,用大端序读出来。拿到 SSRC 后,SessionManager.getBySsrc(ssrc)就能找到对应的会话,然后把 payload 转发给流媒体服务。
这里有个坑:PS 流和裸 H.264 流的 RTP 打包方式不一样。GB28181 通常用 PS 封装,payload 里是 PS 头加 H.264 数据。如果你直接按 H.264 解析,画面会花屏或者出不来。常见做法是把 PS 流解封装后再转推,或者直接推给支持 PS 的流媒体服务器。
4.3 流媒体转发与客户端拉流
收流之后,平台需要把流转发出去,客户端才能看到画面。转发方式有两种:一种是平台自己起 RTSP 或 RTMP 服务,客户端直接连平台;另一种是转推给外部流媒体服务器,客户端从流媒体服务器拉流。
// MediaForwarder.java public class MediaForwarder { private RTMPClient rtmpClient; public void forward(Session session, RtpPacket rtp) { // 1. 解 PS 封装,提取 H.264 数据 byte[] h264 = PsDemuxer.demux(rtp.getPayload()); // 2. 按帧组装,推给 RTMP 服务 if (h264 != null) { rtmpClient.sendVideoFrame(session.getStreamId(), h264); } } }PsDemuxer负责把 PS 流里的 H.264 数据提取出来,rtmpClient把帧推给 RTMP 服务。客户端通过 RTMP 或 HLS 地址拉流,就能看到实时画面。如果客户端拉流后黑屏,先检查 RTMP 推流有没有成功,再看流媒体服务器有没有收到流。常见问题是 PS 解封装没处理好,导致推给 RTMP 的数据不是完整的帧。
注意:转发这块的性能瓶颈通常在解封装和推流。如果并发路数多,建议把解封装和推流放到独立线程池里,别堵在 Netty 的 IO 线程上。
5. 避坑与排查:注册失败、点播黑屏、SSRC 对不上
5.1 设备注册返回 401 或超时
现象:设备端显示注册失败,平台日志里要么没有 REGISTER 请求,要么返回 401。
原因:没有收到请求通常是网络不通或端口没开;返回 401 是认证信息不匹配,设备编码、密码或 SIP 域跟平台配置对不上。
解决:先在平台服务器上用tcpdump抓包,确认 REGISTER 请求有没有到。到了就看认证字段,设备的 SIP 用户名必须是设备编码,密码要跟平台配置一致。如果设备编码长度不对,比如少了一位,认证也会失败。我一般会拿模拟器先跑一遍,确认平台配置没问题,再上真实设备。
5.2 点播成功但画面黑屏
现象:INVITE 流程走完了,设备也返回 200 OK,但客户端拉流后黑屏。
原因:最常见的是 SDP 里的收流 IP 或端口写错了,设备把流推到了别的地方;其次是 SSRC 不匹配,平台找不到对应的会话;还有可能是 PS 解封装失败,推给流媒体服务器的数据不完整。
解决:先在平台服务器上抓包,看有没有 RTP 包打到配置的端口上。有包但画面黑,就检查 SSRC 和会话是否匹配。如果 SSRC 对不上,改代码兼容按端口匹配。如果包都没有,检查 SDP 里的 IP 是不是设备能访问到的地址。防火墙也要看一眼,UDP 端口没放行的话包进不来。
5.3 多路并发时端口分配冲突
现象:单路点播正常,多路同时点播时部分流收不到。
原因:收流端口范围设小了,或者端口分配逻辑有并发问题,多个会话分到了同一个端口。
解决:把media.port-range调大,按实际并发数的 1.5 倍来设。端口分配那块要加锁,确保每个会话拿到唯一端口。我一般会用AtomicInteger做端口轮询,分配前检查端口是否已被占用,避免冲突。
5.4 心跳超时导致设备离线
现象:设备注册成功,但过一段时间就显示离线,点播也失败。
原因:心跳超时时间设得太短,或者平台没正确处理心跳消息,导致设备状态没更新。
解决:把device.heartbeat-timeout调到合理值,一般 60 到 120 秒。检查心跳处理器有没有正确更新设备的最后心跳时间。如果设备发心跳但平台没收到,还是先抓包确认网络。有些设备心跳间隔比配置的超时时间还长,这时候要么调大超时,要么让设备改心跳间隔。
5.5 媒体流花屏或卡顿
现象:画面能出来,但花屏、卡顿或者马赛克严重。
原因:RTP 包丢失或乱序,PS 解封装没处理好,或者转发时帧边界不对。
解决:先看网络有没有丢包,tcpdump抓包分析 RTP 序列号。如果丢包严重,检查网络带宽和交换机配置。PS 解封装那块,确保按完整帧推给流媒体服务器,别把一帧拆成多个包推。转发时加个缓冲队列,按序列号排序后再推,能缓解乱序问题。
6. 二次开发:从改配置到扩展新信令的实操思路
拿到这套代码后,二次开发的方向大概有三个:改配置适配自己的环境、扩展新的信令处理器、对接自己的流媒体服务。改配置最简单,按第 3 章的说明调 SIP 和媒体参数就行。扩展信令处理器稍微复杂点,但套路是固定的。
假设你要加一个设备控制信令,比如远程重启。先在handler包下新建DeviceControlHandler,实现SipHandler接口,然后在SipListenerImpl里注册这个消息类型。核心代码大概长这样:
// DeviceControlHandler.java public class DeviceControlHandler implements SipHandler { @Override public void handle(RequestEvent event) { Request request = event.getRequest(); // 解析 XML 消息体,获取控制命令 String cmd = XmlParser.parseCmd(request.getContent()); if ("reboot".equals(cmd)) { // 构造响应,返回 200 OK Response response = SipUtils.buildOkResponse(request); // 发送控制命令给设备 sipProvider.sendResponse(response); // 记录操作日志 log.info("Device reboot command sent: {}", deviceId); } } }注册处理器的时候,在SipListenerImpl的processRequest里加一个分支,判断消息类型是 MESSAGE 且内容包含控制命令,就交给DeviceControlHandler处理。这样扩展的好处是不用改核心流程,加一个处理器就多一种能力。
对接自己的流媒体服务,主要是改MediaForwarder。如果你用的是 ZLMediaKit,它提供了 HTTP API 来创建流代理,你可以在会话创建时调 API 让 ZLMediaKit 去拉流,平台本身就不用做 RTP 转发了。这种方式更省资源,但依赖外部服务。如果不想引入外部依赖,就自己维护 RTSP 或 RTMP 服务,把 RTP 包解封装后直接推给客户端。
验证改造是否成功,我一般会分三步:先用模拟器跑一遍新信令,确认流程通;再用真实设备测,看有没有兼容性问题;最后压测并发,看性能瓶颈在哪。压测的时候重点看端口分配、线程池和内存,这三个地方最容易出问题。
从那以后我每次改完信令处理器,都会先用模拟器把注册、心跳、点播、控制这几条链路完整走一遍,确认没有回归问题再上真实设备。这个习惯帮我省了不少半夜排查的功夫。希望帮到你。
本文还有配套的精品资源,点击获取