使用 JavaCV 简单解析 GB28181 RTP PS 流并推流到 RTMP
GB28181 协议中,媒体流通常以 PS (Program Stream) 格式封装在 RTP 包中传输(RTP over UDP 或 TCP)。JavaCV(基于 FFmpeg)可以直接处理 PS 流,但对于 RTP 封装的 PS 流,需要先剥离 RTP 头,组装成完整的 PS 数据,然后喂给 FFmpegFrameGrabber 解码,再用 FFmpegFrameRecorder 转推 RTMP。
核心思路(简单实现)
- 接收 RTP 包:使用 Netty 或 Socket 接收 RTP 数据(UDP/TCP)。
- 剥离 RTP 头:RTP 头固定 12 字节(无扩展时),PS 数据从第 13 字节开始(UDP)或第 15 字节(TCP,前面有 2 字节长度 + $ 标志)。
- 组包 & 解析 PS:由于 PS 帧可能被 RTP 分包,需要按序列号(seq)排序缓存,组装完整 PS 帧。识别 PS 头(0x000001BA)、PES 头,提取 H.264/ES 数据。
- 管道喂流:使用
PipedOutputStream+PipedInputStream将组装的 PS 数据写入管道。 - JavaCV 拉流解码:
FFmpegFrameGrabber从管道读取 PS 流,设置格式为 “mpegps” 或直接 “ps”。 - 推 RTMP:
FFmpegFrameRecorder录制抓取的帧到 RTMP URL。
依赖(Maven)
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.10</version> <!-- 最新版 -->
</dependency>
示例代码(基于 CSDN/博客常见实现,简化版)
import org.bytedeco.javacv.*;
import org.bytedeco.javacpp.avcodec;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.ConcurrentLinkedDeque;
// RTP 接收处理器(示例,用 Netty 或 UDP Socket 替换)
public class RtpPsParser implements Runnable {
private PipedOutputStream pos = new PipedOutputStream();
private PipedInputStream pis = new PipedInputStream(pos, 1024 * 1024);
private ConcurrentLinkedDeque<byte[]> packetQueue = new ConcurrentLinkedDeque<>();
private boolean running = true;
// 假设这里接收到 RTP 数据包(byte[] data)
public void onRtpPacket(byte[] data, int offset, int len) {
// 剥离 RTP 头(UDP 示例,TCP 多 +2 字节长度)
byte[] psData = new byte[len - 12];
System.arraycopy(data, 12, psData, 0, psData.length);
packetQueue.offer(psData); // 简单缓存,实际需按 seq 排序去重
}
@Override
public void run() {
while (running) {
if (!packetQueue.isEmpty()) {
byte[] psChunk = packetQueue.poll();
try {
pos.write(psChunk); // 写入管道,组装 PS 流
pos.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public PipedInputStream getInputStream() {
return pis;
}
}
// 主推流类
public class Gb28181ToRtmp {
public static void main(String[] args) throws Exception {
RtpPsParser parser = new RtpPsParser();
new Thread(parser).start();
// Grabber:从管道读取 PS 流
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(parser.getInputStream());
grabber.setFormat("ps"); // 或 "mpegps"
grabber.setOption("rtsp_transport", "tcp"); // 如果是 TCP interleaved
grabber.setVideoCodec(avcodec.AV_CODEC_ID_H264);
grabber.setFrameRate(25);
grabber.setImageWidth(1920); // 根据实际调整
grabber.setImageHeight(1080);
grabber.start();
// Recorder:推 RTMP
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder("rtmp://your-server/live/stream", grabber.getImageWidth(), grabber.getImageHeight());
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("flv");
recorder.setFrameRate(grabber.getFrameRate());
recorder.setVideoBitrate(2000000);
recorder.setVideoOption("preset", "veryfast");
recorder.setVideoOption("tune", "zerolatency");
recorder.start();
Frame frame;
while ((frame = grabber.grabFrame()) != null) {
recorder.record(frame); // 直接转发帧
}
grabber.stop();
recorder.stop();
}
}
注意事项
- RTP 组包:实际需处理 seq、标记位(Marker 表示帧结束)、丢包重排序。参考博客中的
SsrcUdpHandler或SsrcTcpHandler类。 - TCP vs UDP:TCP 有
$+ 长度(2 字节),需额外剥离。 - 性能:管道缓冲设大,避免阻塞。长时间运行需处理 I 帧检测、缓存清理。
- 测试:先用 Wireshark 抓包确认 PS 头(0x000001BA),或用 FFmpeg 测试:
ffplay rtp://ip:port(需 SDP)。 - 替代简单方案:如果不手动解析 RTP,可用 ZLMediaKit/SRS(支持直接接收 GB28181 PS RTP 并转 RTMP/HLS),无需 JavaCV 编码。
此方式已验证可行(参考 eguid 等博客),适合学习/小型项目。大规模建议用成熟框架如 ZLMediaKit。需要完整 RTP 解析代码或特定厂商适配,欢迎提供更多细节!