1. 海康PS流与H.264裸数据的关系
第一次接触海康设备实时回调的PS流时,我完全被那一串串十六进制数据搞懵了。后来才发现,这些看似复杂的数据其实有着清晰的层次结构。简单来说,海康设备输出的PS流就像是一个俄罗斯套娃,最外层是PS包头,往里依次是系统头、映射头、PES头,最里面才是我们真正需要的H.264裸数据。
海康设备对H.264的PS封装有个特点:每个IDR帧(关键帧)前面都会带着SPS、PPS等NALU单元。所以一个完整的IDR帧PS包结构是这样的:PS头→系统头→映射头→PES头→H.264裸数据。而非关键帧就简单多了,只有PS头→PES头→H.264裸数据。这种设计既保证了关键帧的完整性,又减少了非关键帧的数据量。
2. PS流结构深度解析
2.1 PS包头详解
PS包头就像快递单,记录了这个包裹的基本信息。它的固定结构包括:
- 起始码(0x000001BA):4字节,相当于包裹的"快递单号"
- 系统时钟参考(SCR):6字节,精确到微秒级的时间戳
- 节目复用速率:3字节,表示数据流的传输速率
- 填充长度:1字节,表示后面有多少个填充字节
我遇到过一个问题:用VLC播放PS流时出现黑屏。后来发现是节目复用速率字段解析错误导致数据无法正常发送。这个字段如果全为0,就说明解析过程出了问题。
2.2 系统头和映射头
系统头(0x000001BB)只在第一个数据包出现,相当于"总包裹单"。它包含了整个流的全局信息:
- 速率界限:整个流的最大传输速率
- 音频/视频界限:同时活动的音视频流最大数量
- 各种锁定标志:保证音视频同步的重要参数
映射头(0x000001BC)是关键帧的"身份证",只有I帧才会有。它最重要的字段是stream_type:
- 0x1B表示H.264视频流
- 0x90表示G.711音频流
- 0xE0-0xEF表示视频基本流
3. PES包解析实战
3.1 PES包结构
PES包就像快递里的内包装,结构如下:
- 起始码前缀(0x000001)+流ID:3+1字节
- PES包长度:2字节
- 各种标志位:2字节
- 包头长度:1字节
- 可选字段(如PTS/DTS):可变长度
- 有效负载:H.264裸数据
解析时要注意,流ID的范围很重要:
- 0xC0-0xDF:音频流
- 0xE0-0xEF:视频流
3.2 时间戳处理
PTS(展现时间戳)和DTS(解码时间戳)是保证视频流畅播放的关键。它们的存储方式很特别:
- 分成3部分共5字节存储
- 每部分都有特定的标记位
- 时间基准是90kHz
我曾经遇到过音视频不同步的问题,就是因为没有正确处理PTS。后来发现,对于B帧,PTS和DTS可能不同,需要特别注意。
4. FFmpeg实战解码
4.1 使用FFmpeg库
用FFmpeg解码PS流最方便的方法是使用libavformat:
AVFormatContext *fmt_ctx = NULL; avformat_open_input(&fmt_ctx, "input.ps", NULL, NULL); avformat_find_stream_info(fmt_ctx, NULL); // 找到视频流 int video_stream_index = -1; for(int i=0; i<fmt_ctx->nb_streams; i++){ if(fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){ video_stream_index = i; break; } } AVPacket pkt; while(av_read_frame(fmt_ctx, &pkt) >= 0){ if(pkt.stream_index == video_stream_index){ // 处理H.264裸数据 process_h264_data(pkt.data, pkt.size); } av_packet_unref(&pkt); }4.2 手动解析PS流
有时候我们需要更精细的控制,这时可以手动解析:
void parse_ps_packet(const uint8_t *data, int size){ // 检查PS包头 if(memcmp(data, "\x00\x00\x01\xba", 4) == 0){ int stuff_len = data[13] & 0x07; int offset = 14 + stuff_len; // 检查系统头 if(memcmp(data+offset, "\x00\x00\x01\xbb", 4) == 0){ uint16_t sys_header_len = (data[offset+4]<<8)|data[offset+5]; offset += 6 + sys_header_len; } // 解析PES包 while(offset < size){ if(memcmp(data+offset, "\x00\x00\x01", 3) == 0){ uint8_t stream_id = data[offset+3]; if((stream_id & 0xE0) == 0xE0){ // 视频流 parse_pes_packet(data+offset, size-offset); } offset += 6 + data[offset+8]; // 跳过PES头 }else{ offset++; } } } }5. 常见问题排查
在实际项目中,我遇到过几个典型问题:
黑屏问题:检查节目复用速率字段是否为0,这通常意味着解析错误。
花屏问题:确保正确识别了I帧和P帧。I帧必须包含SPS/PPS。
音视频不同步:仔细处理PTS/DTS,特别是B帧的情况。
内存泄漏:使用FFmpeg时记得释放AVPacket和AVFormatContext。
有一次项目上线前,我们突然发现视频在某些设备上会卡顿。后来发现是因为没有正确处理SCR(系统时钟参考)字段,导致时间计算错误。这个教训让我明白,处理多媒体数据时,时间戳相关的字段绝对不能马虎。
对于想要深入研究的开发者,我建议先用十六进制编辑器查看实际的PS流数据,对照标准文档一个个字段分析。虽然开始会很慢,但这是理解PS流结构最有效的方法。