如何用VideoToolBox按顺序解码无时间戳的H264 B帧?
解决VideoToolBox无时间戳下带B帧的H264顺序解码问题
一、核心问题本质
H264的B帧依赖前后帧做双向预测,而VideoToolBox完全依赖时间戳(DTS/PTS)管理帧的解码顺序和输出顺序。原始码流无时间戳时,系统无法判断B帧的依赖关系,导致输出乱序或presentTimeStamp.valid为false。解决的核心是手动为每个NALU生成合理的DTS(解码时间戳)和PTS(显示时间戳),让VideoToolBox能正确调度解码与输出。
二、具体实现步骤
1. 先解析NALU类型,区分帧类型
每个NALU去掉起始码后的第一个字节,低5位是NALU类型,用来判断是I/P/B帧还是SPS/PPS:
uint8_t naluType = (naluData[0] & 0x1F); if (naluType == 5) { // I帧 } else if (naluType == 1) { // P帧 } else if (naluType == 2) { // B帧 } else if (naluType == 7 || naluType == 8) { // SPS/PPS,优先传给解码会话 }
2. 生成DTS和PTS,缓存B帧
因为B帧需要先解码前后参考帧才能正确输出,我们需要按编码顺序缓存B帧,等后续的I/P帧到来后再按解码顺序提交给VideoToolBox:
- 维护两个计数器:
dtsCounter(解码时间戳计数)、ptsCounter(显示时间戳计数),初始为0 - 维护一个B帧缓存队列,存储B帧的NALU数据和对应的PTS
具体逻辑:
// 全局/类内变量 NSMutableArray *bFrameCache = [NSMutableArray array]; uint64_t dtsCounter = 0; uint64_t ptsCounter = 0; CMTime frameInterval = CMTimeMake(1, 30); // 假设帧率30fps,可从SPS解析实际值 // 遍历处理每个NALU if (isIOrPFrame) { // 先把缓存的B帧全部提交给解码器 for (NSDictionary *bFrame in bFrameCache) { CMTime bDts = CMTimeMake(dtsCounter++, 30); CMTime bPts = [bFrame[@"pts"] CMTimeValue]; [self submitNALU:bFrame[@"data"] dts:bDts pts:bPts]; } [bFrameCache removeAllObjects]; // 提交当前I/P帧 CMTime ipDts = CMTimeMake(dtsCounter++, 30); CMTime ipPts = CMTimeMake(ptsCounter++, 30); [self submitNALU:naluData dts:ipDts pts:ipPts]; } else if (isBFrame) { // 缓存B帧,记录对应的PTS CMTime bPts = CMTimeMake(ptsCounter++, 30); [bFrameCache addObject:@{@"data": naluData, @"pts": [NSValue valueWithCMTime:bPts]}]; }
3. 给VideoToolBox传递自定义时间戳
在提交NALU到解码会话时,手动设置sampleBuffer的DTS和PTS,同时把自定义PTS通过sourceFrameRefCon传递到回调:
- (void)submitNALU:(NSData *)naluData dts:(CMTime)dts pts:(CMTime)pts { // 构造CMSampleBuffer CMFormatDescriptionRef formatDesc = ...; // 提前用SPS/PPS创建 CMSampleBufferRef sampleBuffer = NULL; // 省略sampleBuffer构造代码... // 设置DTS和PTS CMSampleBufferSetDecodeTimeStamp(sampleBuffer, dts); CMSampleBufferSetPresentationTimeStamp(sampleBuffer, pts); // 传递自定义PTS到回调 CMTime *ptsRef = malloc(sizeof(CMTime)); *ptsRef = pts; OSStatus status = VTDecompressionSessionDecodeFrame(_decompressionSession, sampleBuffer, kVTDecompressionSessionDecodeFrame_EnableTemporalProcessing, ptsRef, NULL); CFRelease(sampleBuffer); }
4. 回调中使用自定义时间戳渲染
忽略系统返回的无效presentTimeStamp,直接用传递过来的自定义PTS做排序或渲染:
void decompressionOutputCallback(void *refCon, void *sourceRefCon, OSStatus status, VTDecompressionOutputFlags flags, CVImageBufferRef imageBuffer, CMTime presentationTimeStamp, CMTime decodeTimeStamp) { // 获取自定义PTS CMTime actualPts = *(CMTime *)sourceRefCon; free(sourceRefCon); // 这里用Metal渲染imageBuffer,用actualPts做同步或排序 // ... }
三、额外注意事项
- 必须先将SPS/PPS提交给VideoToolBox解码会话,否则后续I/P帧无法解码
- 如果能从SPS解析出实际帧率,替换掉示例中的30fps,时间戳会更准确
- B帧缓存队列要设置最大容量,避免内存溢出,超过时可丢弃最早的缓存帧(会导致短暂卡顿)
内容的提问来源于stack exchange,提问作者shan yu
相关产品推荐
相关产品推荐

