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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 20:18:00