使用AVAssetWriter录制视频如何移除首帧黑/空白帧
问题根因
你当前的实现存在3个核心问题导致首帧黑屏:
- 写入会话启动时机错误:首次触发
.start状态就启动会话,若此时触发captureOutput的是音频帧,会话起始时间会早于第一帧视频帧的时间戳,播放器解码时前一段无有效视频帧就会显示黑屏 - 未校验第一帧视频帧的写入结果:
append(_:withPresentationTime:)方法有布尔返回值,写入失败时你依然标记hasWrittenFirstVideoFrame = true,导致后续音频正常写入但无对应首帧视频 - 像素缓冲格式兼容性问题:32ARGB格式在部分设备上的H.264编码支持度不佳,可能导致首帧视频编码失败被丢弃
修复方案
1. 调整状态逻辑,延后写入会话启动时机
去掉.start状态下直接初始化写入器的逻辑,改为仅标记录制启动状态,等第一帧有效视频帧处理完成后再初始化写入器、启动会话,保证会话起始时间和第一帧视频的时间戳完全对齐。
2. 校验写入结果,修正像素缓冲格式
添加第一帧写入成功的判断,将像素缓冲格式改为通用兼容的32BGRA。
3. 修正音频帧写入逻辑
保证音频帧时间戳与会话时间轴对齐,避免时间偏移。
修改后核心代码
// 新增变量:标记录制已触发但未初始化写入器 var isWaitingForFirstVideoFrame = false func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { let timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer).seconds if output == _videoOutput { if connection.isVideoOrientationSupported { connection.videoOrientation = .portrait } guard let cvImageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } let ciImage = CIImage(cvImageBuffer: cvImageBuffer) guard let filteredCIImage = applyFilters(inputImage: ciImage) else { return } self.ciImage = filteredCIImage guard let cvPixelBuffer = getCVPixelBuffer(from: filteredCIImage) else { return } self.cvPixelBuffer = cvPixelBuffer self.ciContext.render(filteredCIImage, to: cvPixelBuffer, bounds: filteredCIImage.extent, colorSpace: CGColorSpaceCreateDeviceRGB()) metalView.draw() } switch _captureState { case .start: // 仅做标记,等待第一帧视频再初始化写入器 isWaitingForFirstVideoFrame = true _captureState = .capturing return case .capturing: // 等待第一帧视频帧时,仅处理视频输出 if isWaitingForFirstVideoFrame { guard output == _videoOutput else { return } guard let outputUrl = tempURL else { return } let writer = try! AVAssetWriter(outputURL: outputUrl, fileType: .mp4) let videoSettings = _videoOutput!.recommendedVideoSettingsForAssetWriter(writingTo: .mp4) let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) videoInput.mediaTimeScale = CMTimeScale(bitPattern: 600) videoInput.expectsMediaDataInRealTime = true let pixelBufferAttributes = [ kCVPixelBufferCGImageCompatibilityKey: NSNumber(value: true), kCVPixelBufferCGBitmapContextCompatibilityKey: NSNumber(value: true), kCVPixelBufferPixelFormatTypeKey: NSNumber(value: Int32(kCVPixelFormatType_32BGRA)) ] as [String:Any] let adapter = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoInput, sourcePixelBufferAttributes: pixelBufferAttributes) if writer.canAdd(videoInput) { writer.add(videoInput) } let audioSettings = _audioOutput!.recommendedAudioSettingsForAssetWriter(writingTo: .mp4) as? [String:Any] let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings) audioInput.expectsMediaDataInRealTime = true if writer.canAdd(audioInput) { writer.add(audioInput) } _filename = outputUrl.absoluteString _assetWriter = writer _assetWriterVideoInput = videoInput _assetWriterAudioInput = audioInput _adapter = adapter _time = timestamp writer.startWriting() writer.startSession(atSourceTime: CMTime(seconds: timestamp, preferredTimescale: CMTimeScale(600))) // 写入第一帧视频 let time = CMTime(seconds: timestamp, preferredTimescale: CMTimeScale(600)) guard _assetWriterVideoInput?.isReadyForMoreMediaData == true, _adapter?.append(self.cvPixelBuffer, withPresentationTime: time) == true else { // 第一帧写入失败,重置状态避免异常 _captureState = .idle return } hasWrittenFirstVideoFrame = true isWaitingForFirstVideoFrame = false return } // 正常录制逻辑 if output == _videoOutput { if _assetWriterVideoInput?.isReadyForMoreMediaData == true { let time = CMTime(seconds: timestamp, preferredTimescale: CMTimeScale(600)) _ = _adapter?.append(self.cvPixelBuffer, withPresentationTime: time) } } else if output == _audioOutput { if _assetWriterAudioInput?.isReadyForMoreMediaData == true, hasWrittenFirstVideoFrame { _ = _assetWriterAudioInput?.append(sampleBuffer) } } break case .end: guard _assetWriterVideoInput?.isReadyForMoreMediaData == true, _assetWriter!.status != .failed else { break } _assetWriterVideoInput?.markAsFinished() _assetWriterAudioInput?.markAsFinished() _assetWriter?.finishWriting { [weak self] in guard let output = self?._assetWriter?.outputURL else { return } self?._captureState = .idle self?._assetWriter = nil self?._assetWriterVideoInput = nil self?._assetWriterAudioInput = nil self?.isWaitingForFirstVideoFrame = false self?.previewRecordedVideo(with: output) } default: break } }
额外可选优化
如果修改后仍偶现黑屏,可在停止录制时主动追加一帧和最后一帧视频相同的像素缓冲,pts设为最后一帧pts+0.033s(对应30fps的一帧时长),避免最后一帧编码被截断。
内容的提问来源于stack exchange,提问作者Chris
相关产品推荐
相关产品推荐

