使用AVAssetWriter将UIImage转为视频时,长视频出现黑帧闪烁问题
解决长视频生成时的黑帧闪烁问题
哥们,这种被同一个bug卡几周的滋味太难受了!我之前也碰到过类似的情况——用图片序列生成视频时,短视频正常,长视频到一定时长就开始出黑帧,不同机型触发时间还不一样。你遇到的问题大概率和CVPixelBuffer的内存管理、尺寸不匹配或者时间戳计算错误有关,结合你贴的代码,咱们一步步来解决:
先看你代码里的潜在坑
你提供的fillPixelBufferFromImage函数有几个容易被忽略的问题:
- 硬编码了720x720的尺寸,如果输入图片和这个尺寸不匹配,绘制时可能出现空白/拉伸,累积到一定程度就会触发黑帧
- 没有用
defer确保CVPixelBuffer一定会解锁,一旦中间代码抛出异常,会导致后续帧无法正确写入 - 直接按buffer尺寸绘制图片,没有考虑图片的原始比例,可能导致渲染异常
针对性修复方案
1. 修复像素填充函数,适配动态尺寸
把硬编码的尺寸改成从pixelBuffer获取,同时确保图片按比例绘制,还要用defer保证解锁:
// MARK: - Fill Pixel Buffer - private func fillPixelBufferFromImage(image: UIImage, pixelBuffer: CVPixelBuffer) { CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) // 用defer确保不管中间出什么问题,buffer都会被解锁 defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) } guard let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer) else { return } let rgbColorSpace = CGColorSpaceCreateDeviceRGB() // 从pixelBuffer获取真实的宽高和字节数,不要硬编码 let bufferWidth = CVPixelBufferGetWidth(pixelBuffer) let bufferHeight = CVPixelBufferGetHeight(pixelBuffer) let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) guard let context = CGContext( data: pixelData, width: bufferWidth, height: bufferHeight, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue ) else { return } // 按比例缩放图片,避免拉伸导致的渲染问题 let imageSize = image.size let scale = min(CGFloat(bufferWidth)/imageSize.width, CGFloat(bufferHeight)/imageSize.height) let scaledSize = CGSize(width: imageSize.width*scale, height: imageSize.height*scale) let drawRect = CGRect( x: (CGFloat(bufferWidth)-scaledSize.width)/2, y: (CGFloat(bufferHeight)-scaledSize.height)/2, width: scaledSize.width, height: scaledSize.height ) context.draw(image.cgImage!, in: drawRect) }
2. 修复时间戳计算,避免溢出
黑帧的另一个常见原因是时间戳不连续或整数溢出,尤其是处理几百上千帧的时候。一定要用CMTime的API来计算时间,不要直接用循环索引:
// 假设你的帧率是12fps,定义每帧的时长 let frameDuration = CMTimeMake(value: 1, timescale: 12) var currentTime = CMTime.zero // 在循环添加每帧时: for image in imageArray { // 获取或创建pixelBuffer guard let pixelBuffer = adaptor.pixelBufferPool?.createPixelBuffer() else { continue } fillPixelBufferFromImage(image: image, pixelBuffer: pixelBuffer) // 用连续的时间戳写入 adaptor.append(pixelBuffer, withPresentationTime: currentTime) currentTime = CMTimeAdd(currentTime, frameDuration) }
3. 优化内存,减少碎片化
- 重用CVPixelBuffer:用
AVAssetWriterInputPixelBufferAdaptor的pixelBufferPool来获取可重用的buffer,不要每次都新建,减少内存分配 - 用
autoreleasepool包裹每帧处理:避免UIImage和临时对象占用过多内存,防止内存不足导致渲染失败
for image in imageArray { autoreleasepool { guard let pixelBuffer = adaptor.pixelBufferPool?.createPixelBuffer() else { return } fillPixelBufferFromImage(image: image, pixelBuffer: pixelBuffer) adaptor.append(pixelBuffer, withPresentationTime: currentTime) currentTime = CMTimeAdd(currentTime, frameDuration) } }
4. 监控写入状态,排查错误
在写入过程中加入状态检查,一旦写入失败能及时看到错误信息:
if writer.status == .failed { print("视频写入出错:\(writer.error?.localizedDescription ?? "未知错误")") // 这里可以加入错误处理逻辑 }
额外测试建议
- 用Xcode的Memory Graph Debugger观察内存变化,看黑帧出现时是否有内存突然飙升或泄漏
- 尝试降低视频分辨率(比如改成480p),如果黑帧消失,说明是旧机型的硬件性能限制,可以考虑分批次渲染再拼接视频
内容的提问来源于stack exchange,提问作者masaldana2
相关产品推荐
相关产品推荐

