
简介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 SimpleChannelInboundHandlerDatagramPacket { 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 包解封装后直接推给客户端。验证改造是否成功我一般会分三步先用模拟器跑一遍新信令确认流程通再用真实设备测看有没有兼容性问题最后压测并发看性能瓶颈在哪。压测的时候重点看端口分配、线程池和内存这三个地方最容易出问题。从那以后我每次改完信令处理器都会先用模拟器把注册、心跳、点播、控制这几条链路完整走一遍确认没有回归问题再上真实设备。这个习惯帮我省了不少半夜排查的功夫。希望帮到你。本文还有配套的精品资源点击获取