You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 14:24:57