如何在HTML5浏览器中无插件低延迟播放RTSP H264流?
解决Java服务端无外部依赖提取RTSP中H264流的方案
我完全懂你的纠结——既要满足浏览器/平台兼容、低延迟、无插件、无转码这些硬要求,又不想塞进ffmpeg或GStreamer这种笨重的工具,还得在Java服务端干净利落地搞定RTSP到H264裸流的提取。其实核心思路就是手动解析RTSP协议栈,剥离RTP封装,重组H264的NAL单元,全程用Java原生代码实现,不需要任何外部程序。
核心原理:RTSP/RTP承载H264的结构
RTSP直播流里的H264数据是通过RTP包传输的,通常有两种封装方式:
- 单一NAL单元模式:一个RTP包直接装一个完整的H264 NAL单元(比如SPS、PPS、IDR帧)
- FU-A分片模式:当NAL单元太大时,会被拆成多个RTP分片传输,需要后续重组
另外,为了避免UDP丢包问题,我们优先用RTSP over TCP传输,此时RTP包会被封装在RTSP的Interleaved帧里,格式很固定:$ + 通道号(1字节) + 数据长度(2字节大端) + RTP数据。
Java服务端实现步骤
1. 建立RTSP连接并协商参数
不需要用重型框架,直接用Java原生Socket实现RTSP的核心交互流程:
- 发送
OPTIONS请求:确认服务端支持的方法(必须包含PLAY、SETUP等) - 发送
DESCRIBE请求:拿到SDP协议描述,从中提取关键信息:- H264的基础参数:
profile-level-id、SPS、PPS(这些是客户端MSE初始化必需的) - RTP的动态负载类型(通常是96/97)、传输通道信息
- H264的基础参数:
- 发送
SETUP请求:指定用TCP传输(Transport: RTP/AVP/TCP;interleaved=0-1),协商RTP流的传输通道 - 发送
PLAY请求:启动流传输
2. 解析RTSP Interleaved帧,提取RTP包
服务端开始推送数据后,你会持续收到Interleaved格式的帧,处理逻辑很简单:
- 读取前3字节:第一个字节是
$,第二个是通道号(通常0是视频流),第三、四字节是大端序的数据长度 - 根据数据长度读取后续的RTP包内容,剥离RTSP的封装层,得到纯RTP包
3. 重组RTP包为完整的H264 NAL单元
针对两种RTP封装模式分别处理:
- 单一NAL单元模式:RTP负载的第一个字节就是NAL单元的类型标识(比如0x05是IDR帧,0x07是SPS),直接把整个负载作为H264 NAL单元输出即可
- FU-A分片模式:RTP负载开头有2个额外头部(FU indicator和FU header),需要:
- 读取FU indicator的第3位(NRI)和最后5位(NAL类型),FU header的第1位(是否为分片起始)、第2位(是否为分片结束)
- 去掉这2个头部字节,把分片的负载内容拼接起来
- 当收到分片结束标记时,组装成完整的NAL单元
- 注意校验RTP序列号,确保分片的顺序正确,避免丢包导致的帧损坏
4. 推送H264流到客户端
提取出的H264 NAL单元(包括SPS、PPS、普通帧)可以直接通过WebSocket推送给客户端:
- 先推送SPS和PPS(客户端MSE必须先拿到这些初始化数据才能解码)
- 后续持续推送普通的IDR帧和P帧
- 客户端用
mux.js或MP4Box.js把H264裸流封装成碎片化MP4(fMP4),再喂给MSE实现HTML5 Video播放
额外建议
- 优先把fMP4封装放在客户端做:这样服务端只需要传H264裸流,带宽占用更低,也减轻了服务端的CPU压力,符合你的带宽约束和低延迟要求
- 参考核心RFC文档:不用全啃,重点看RFC 2326(RTSP)的Interleaved传输部分、RFC 6184(H264 over RTP)的封装规则、RFC 3550(RTP)的基础结构
- 避免引入重型依赖:全程用Java原生IO和字节处理,代码量其实不大,可控性强,完全符合你“开箱即用、无外部程序”的要求
内容的提问来源于stack exchange,提问作者Daniel
相关产品推荐
相关产品推荐

