仅定位到第0帧后Pixel Data才为NULL的视频帧缓存问题
排查AVPlayer逐帧缓存时第0帧Pixel Buffer为NULL的问题
我之前在做视频帧提取的时候也碰到过类似的坑,结合你贴的代码来看,问题大概率出在第0帧的时间戳处理、AVPlayerItem的加载状态或者seek后的帧渲染时机上,下面给你一步步拆解解决:
可能的原因分析
- 第0帧时间戳不匹配:你用
CMTimeMakeWithSeconds(0, self.player.currentItem.asset.duration.timescale)生成第0帧的时间,但kCMTimeZero本质是CMTimeMake(0, 1),如果asset的timescale和AVPlayerItem实际使用的timescale不一致,就会导致时间戳不匹配,找不到对应的帧。 - AVPlayerItem未就绪:如果在player item还没进入
AVPlayerItemStatusReadyToPlay状态就去seek和提取帧,很可能因为资源未加载完成而返回NULL。 - seek后未等待帧渲染:调用
seekToTime:后,AVPlayer需要时间完成帧的渲染,如果你立刻调用copyPixelBufferForItemTime:,此时帧还没准备好,自然返回NULL。
针对性解决方案
1. 修正第0帧的时间戳
直接使用系统定义的kCMTimeZero作为第0帧的时间,避免浮点计算和timescale不匹配的问题:
// 第0帧直接用kCMTimeZero,替换原来的计算逻辑 CMTime frameTime = kCMTimeZero;
2. 确保AVPlayerItem就绪后再操作
在开始缓存前,先检查player item的状态,未就绪则通过KVO监听等待:
- (void)startFrameCaching { AVPlayerItem *currentItem = self.player.currentItem; if (currentItem.status != AVPlayerItemStatusReadyToPlay) { [currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; return; } // 开始缓存第0帧及后续帧 [self cacheFrameAtIndex:0]; } // KVO监听状态变化 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"status"]) { AVPlayerItemStatus status = [[change objectForKey:NSKeyValueChangeNewKey] integerValue]; if (status == AVPlayerItemStatusReadyToPlay) { [object removeObserver:self forKeyPath:@"status"]; [self cacheFrameAtIndex:0]; } } }
3. Seek后等待帧渲染完成再提取
在seekToTime:的完成回调里,监听AVPlayerItemNewFrameReadyForDisplayNotification通知,确保帧已经渲染完成后再调用copyPixelBufferForItemTime::
- (void)cacheFrameAtIndex:(NSInteger)frame { if (frame >= self.totalFrames) return; // 假设totalFrames是你要缓存的总帧数 CMTime frameTime; if (frame == 0) { frameTime = kCMTimeZero; } else { CGFloat frameTimeSeconds = 1.0 / self.fps * frame; // 使用player currentItem的timescale,而非asset的,避免不匹配 frameTime = CMTimeMakeWithSeconds(frameTimeSeconds, self.player.currentItem.currentTime.timescale); } __weak typeof(self) weakSelf = self; [self.player seekToTime:frameTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) { if (!finished) { // 定位失败,处理错误 [weakSelf cacheFrameAtIndex:frame+1]; return; } // 等待帧渲染完成的通知 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; __block id frameReadyObserver; frameReadyObserver = [nc addObserverForName:AVPlayerItemNewFrameReadyForDisplayNotification object:weakSelf.player.currentItem queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { [nc removeObserver:frameReadyObserver]; CVPixelBufferRef pixelBuffer = [weakSelf.videoOutput copyPixelBufferForItemTime:frameTime itemTimeForDisplay:nil]; if (pixelBuffer != NULL) { // 这里处理你的缓存逻辑 completion(pixelBuffer); // 记得释放像素缓冲,避免内存泄漏 CVPixelBufferRelease(pixelBuffer); } // 继续缓存下一帧 [weakSelf cacheFrameAtIndex:frame+1]; }]; }]; }
额外注意事项
- 内存管理:
CVPixelBufferRef是Core Foundation对象,使用后必须调用CVPixelBufferRelease释放,否则会造成内存泄漏。 - 性能优化:逐帧seek的效率较低,如果是长视频,建议考虑使用
AVAssetImageGenerator来提取帧,它的性能会更优。
内容的提问来源于stack exchange,提问作者mcornell
相关产品推荐
相关产品推荐

