Spring Boot中解码RTSP摄像头H264流至前端的技术求助
Spring Boot中RTSP摄像头H264流解码并推送到前端的解决方案
核心问题拆解
- 从WebSocket接收RTSP摄像头输出的H264编码字节流(以MP4封装的MDAT盒形式传输)
- 需要适配前端播放格式,替代失败的JavaCV/FFmpeg方案
- 当前尝试通过Spring SSE发送提取的MDAT内容,但未达到预期效果
可行落地方案
1. 先确认H264流的封装与NALU边界
MDAT中的H264数据是连续的NALU单元,每个NALU以0x00000001或0x000001的起始码分隔。直接发送原始MDAT内容可能缺少边界标识,导致前端无法解析。
提取NALU的Java代码示例
// 从MDAT字节数组中提取带起始码的NALU列表 public List<byte[]> extractNalus(byte[] mdatData) { List<byte[]> naluList = new ArrayList<>(); int offset = 0; int dataLen = mdatData.length; while (offset < dataLen) { // 查找起始码 int startCodePos = findStartCode(mdatData, offset); if (startCodePos == -1) break; // 查找下一个起始码,确定当前NALU长度 int nextStartCodePos = findStartCode(mdatData, startCodePos + 4); int naluLen = (nextStartCodePos == -1) ? dataLen - startCodePos : nextStartCodePos - startCodePos; byte[] nalu = Arrays.copyOfRange(mdatData, startCodePos, startCodePos + naluLen); naluList.add(nalu); offset = startCodePos + naluLen; } return naluList; } private int findStartCode(byte[] data, int startOffset) { // 查找4字节起始码 for (int i = startOffset; i < data.length - 3; i++) { if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 0 && data[i+3] == 1) { return i; } } // 查找3字节起始码 for (int i = startOffset; i < data.length - 2; i++) { if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) { return i; } } return -1; }
2. 前端直接播放H264(无需后端解码)
后端无需解码H264,只需将提取的NALU通过WebSocket以二进制帧发送,前端使用MediaSource API配合video标签播放,这是最高效的方案:
Spring Boot WebSocket二进制发送代码
@Controller public class CameraStreamController { @Autowired private SimpMessagingTemplate messagingTemplate; // 处理从WebSocket接收的摄像头流 @MessageMapping("/camera/stream") public void handleCameraStream(byte[] payload) { // 假设payload为MDAT盒内容,提取NALU List<byte[]> nalus = extractNalus(payload); for (byte[] nalu : nalus) { // 以二进制帧发送到前端订阅主题 messagingTemplate.convertAndSend("/topic/camera/feed", nalu); } } }
前端播放JavaScript代码
const video = document.getElementById('camera-video'); const mediaSource = new MediaSource(); video.src = URL.createObjectURL(mediaSource); let sourceBuffer; mediaSource.addEventListener('sourceopen', () => { // 匹配摄像头H264 profile,示例为Baseline profile sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E"'); const socket = new WebSocket('ws://localhost:8080/ws'); socket.binaryType = 'arraybuffer'; socket.onmessage = (event) => { if (!sourceBuffer.updating) { sourceBuffer.appendBuffer(event.data); } }; });
3. 若必须后端解码(如需处理帧数据)
如果需要对帧进行处理(比如人脸识别),改用FFmpeg的JNI绑定而非JavaCV全量包,减少依赖问题:
- 引入FFmpeg Java绑定依赖:
org.bytedeco:ffmpeg-platform - 初始化FFmpeg解码器,将H264字节流解码为RGB/YUV帧
- 将帧编码为前端支持的格式(JPEG/WEBP)后发送
简化FFmpeg解码示例
import org.bytedeco.ffmpeg.global.avcodec; import org.bytedeco.ffmpeg.global.avutil; import org.bytedeco.ffmpeg.*; public class H264Decoder { private AVCodecContext codecCtx; public H264Decoder() { avutil.av_log_set_level(avutil.AV_LOG_ERROR); AVCodec codec = avcodec.avcodec_find_decoder(avcodec.AV_CODEC_ID_H264); codecCtx = avcodec.avcodec_alloc_context3(codec); avcodec.avcodec_open2(codecCtx, codec, null); } public AVFrame decode(byte[] h264Data) { AVPacket pkt = new AVPacket(); pkt.data(avutil.av_malloc(h264Data.length)); pkt.size(h264Data.length); avutil.av_memcpy(pkt.data(), h264Data, h264Data.length); AVFrame frame = avutil.av_frame_alloc(); int ret = avcodec.avcodec_send_packet(codecCtx, pkt); if (ret >= 0) { ret = avcodec.avcodec_receive_frame(codecCtx, frame); if (ret >= 0) { return frame; } } avutil.av_frame_free(frame); return null; } }
调试验证步骤
- 用FFmpeg验证提取的MDAT内容是否为有效H264:
若能生成可播放的MP4,说明流本身有效,问题出在传输/解析逻辑。ffmpeg -f h264 -i mdat_dump.bin -c copy test.mp4 - 检查前端MediaSource的codec参数是否与摄像头H264 profile匹配。
内容的提问来源于stack exchange,提问作者ashif-ismail
相关产品推荐
相关产品推荐

