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

AVAssetWriterInput的isReadyForMoreMediaData始终为false问题求助

解决AVAssetWriterInput.isReadyForMoreMediaData始终返回false的问题

我在尝试实时录制CVPixelBuffer时遇到了问题,assetWriterInput.isReadyForMoreMediaData始终返回false,导致无法追加缓冲区。以下是我的代码:

class VideoRecorder{

static var shared = VideoRecorder()
var avAssetWriter: AVAssetWriter?
var Adaptor: AVAssetWriterInputPixelBufferAdaptor?
var Settings: RecorderSetting?

struct RecorderSetting{
    var videoSetting: [String : Any]
    var Path: URL
}

func makeVideoSettings(width: Int, height: Int, BitRate: Double) -> [String : Any]{
    let VideoCompressionProperties = [
        AVVideoAverageBitRateKey: Double(width * height) * BitRate
    ]
    
    let videoSettings:[String : Any] = [
        AVVideoCodecKey: AVVideoCodecType.hevc,
        AVVideoWidthKey: width,
        AVVideoHeightKey: height,
        AVVideoCompressionPropertiesKey: VideoCompressionProperties
    ]
    
    return videoSettings
}

func makePath(FileName: String) -> URL{
    return URL(fileURLWithPath:
            NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/\(FileName).mp4")
}

func setup(width: Int, height: Int,
           BitRate: Double, FileName: String){
    
    let setting = makeVideoSettings(width: width, height: height, BitRate: BitRate)
    let Path = makePath(FileName: FileName)
    Settings = RecorderSetting(videoSetting: setting, Path: Path)
}

func StartSession(FirstFrame: CVPixelBuffer) throws{
    let attribute: [String : Any] = [
        kCVPixelBufferPixelFormatTypeKey as String: CVPixelBufferGetPixelFormatType(FirstFrame),
        kCVPixelBufferWidthKey as String: CVPixelBufferGetWidth(FirstFrame),
        kCVPixelBufferHeightKey as String: CVPixelBufferGetHeight(FirstFrame)
    ]
    
    if (Settings == nil){throw "Settings invalid"}
    
    let writerInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: Settings!.videoSetting)
    Adaptor =
        AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: writerInput, sourcePixelBufferAttributes: attribute)
    Adaptor?.assetWriterInput.expectsMediaDataInRealTime = true
    
    do{
        avAssetWriter = try AVAssetWriter(url: Settings!.Path, fileType: AVFileType.mp4)
        if (avAssetWriter!.canAdd(writerInput)){
            let StartTime = Date().timeIntervalSince1970.toCMTime()
            avAssetWriter?.startWriting()
            avAssetWriter?.startSession(atSourceTime: StartTime)
            try? WriteBuffer(Buffer: FirstFrame, time: StartTime)
            
        }else{
            throw "Add AVWriterInput Error"
        }
    }catch{
        throw "Initializing Error"
    }
}

func StopSession(){
    if(Adaptor?.assetWriterInput.isReadyForMoreMediaData == false){return}
    Adaptor?.assetWriterInput.markAsFinished()
    avAssetWriter?.finishWriting(completionHandler: {
        
        if let outputPath = self.Settings?.Path{
            PHPhotoLibrary.shared().performChanges({
                PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputPath)
            }) { saved, error in
                if saved {
                    try? FileManager().removeItem(at: outputPath)
                    print("Saved ")
                    /*let fetchOptions = PHFetchOptions()
                    fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]

                    let fetchResult = PHAsset.fetchAssets(with: .video, options: fetchOptions).firstObject
                    // fetchResult is your latest video PHAsset
                    // To fetch latest image  replace .video with .image*/
                }
            }
        }
    })
}

func WriteBuffer(Buffer: CVPixelBuffer, time: CMTime) throws{
    if(self.Adaptor != nil){
        if (self.Adaptor!.assetWriterInput.isReadyForMoreMediaData){
            let whetherPixelBufferAppendedtoAdaptor = self.Adaptor!.append(Buffer, withPresentationTime: time)
            if(!whetherPixelBufferAppendedtoAdaptor){
                print(avAssetWriter?.error as Any)
            }
            
        }else{
            throw "Writer Input is not Ready"
        }
    }else{
        throw "PixelBufferAdaptor invalild"
    }

}

核心原因及修复点

  • 未将AVAssetWriterInput添加到AVAssetWriter:这是最关键的错误。你创建了writerInput,但在调用startWriting()和startSession()之前,没有执行avAssetWriter?.add(writerInput)。未被添加到writer的input永远不会处于就绪状态。
  • 调用顺序完全颠倒:原代码先启动writing和session,再判断是否能添加input,逻辑完全错误。正确顺序应为:创建writer → 添加input → 启动writing → 启动session。
  • StopSession逻辑缺陷:原代码中如果isReadyForMoreMediaData为false就直接返回,不执行markAsFinished(),会导致AVAssetWriter无法正常完成写入,甚至造成资源泄漏。不管input是否就绪,停止录制时都需要标记input完成。
  • 时间戳生成不可靠:用Date().timeIntervalSince1970.toCMTime()生成的时间戳可能存在基准不一致的问题,建议使用基于帧率的连续时间戳。

修复后的完整代码

class VideoRecorder{
    static var shared = VideoRecorder()
    var avAssetWriter: AVAssetWriter?
    var adaptor: AVAssetWriterInputPixelBufferAdaptor?
    var settings: RecorderSetting?
    var baseTime: CMTime? // 记录起始时间,用于生成连续时间戳
    let fps: Int32 = 30 // 帧率,可根据需求调整

    struct RecorderSetting{
        var videoSetting: [String : Any]
        var path: URL
    }

    func makeVideoSettings(width: Int, height: Int, bitRate: Double) -> [String : Any]{
        let videoCompressionProperties = [
            AVVideoAverageBitRateKey: Double(width * height) * bitRate
        ]
        
        let videoSettings:[String : Any] = [
            AVVideoCodecKey: AVVideoCodecType.hevc,
            AVVideoWidthKey: width,
            AVVideoHeightKey: height,
            AVVideoCompressionPropertiesKey: videoCompressionProperties
        ]
        
        return videoSettings
    }

    func makePath(fileName: String) -> URL{
        let docsDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
        return URL(fileURLWithPath: docsDir).appendingPathComponent("\(fileName).mp4")
    }

    func setup(width: Int, height: Int, bitRate: Double, fileName: String){
        let setting = makeVideoSettings(width: width, height: height, bitRate: bitRate)
        let path = makePath(fileName: fileName)
        settings = RecorderSetting(videoSetting: setting, path: path)
    }

    func startSession(firstFrame: CVPixelBuffer) throws{
        guard let settings = settings else { throw "Settings invalid" }
        
        let attribute: [String : Any] = [
            kCVPixelBufferPixelFormatTypeKey as String: CVPixelBufferGetPixelFormatType(firstFrame),
            kCVPixelBufferWidthKey as String: CVPixelBufferGetWidth(firstFrame),
            kCVPixelBufferHeightKey as String: CVPixelBufferGetHeight(firstFrame)
        ]
        
        let writerInput = AVAssetWriterInput(mediaType: .video, outputSettings: settings.videoSetting)
        writerInput.expectsMediaDataInRealTime = true
        
        adaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: writerInput, sourcePixelBufferAttributes: attribute)
        
        do{
            avAssetWriter = try AVAssetWriter(url: settings.path, fileType: .mp4)
            
            guard avAssetWriter!.canAdd(writerInput) else {
                throw "Add AVWriterInput Error"
            }
            
            // 关键:先将input添加到writer
            avAssetWriter!.add(writerInput)
            
            // 启动writing和session
            try avAssetWriter!.startWriting()
            baseTime = CMTimeMake(value: 0, timescale: fps)
            avAssetWriter!.startSession(atSourceTime: baseTime!)
            
            // 写入第一帧
            try writeBuffer(buffer: firstFrame, frameIndex: 0)
            
        }catch{
            throw "Initializing Error: \(error.localizedDescription)"
        }
    }

    func stopSession(){
        guard let adaptor = adaptor, let avAssetWriter = avAssetWriter else { return }
        
        // 不管是否就绪,都标记input完成
        adaptor.assetWriterInput.markAsFinished()
        
        avAssetWriter.finishWriting(completionHandler: { [weak self] in
            guard let self = self, let outputPath = self.settings?.path else { return }
            
            if avAssetWriter.status == .completed {
                PHPhotoLibrary.shared().performChanges({
                    PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputPath)
                }) { saved, error in
                    if saved {
                        try? FileManager.default.removeItem(at: outputPath)
                        print("视频已保存到相册")
                    } else if let error = error {
                        print("保存失败:\(error.localizedDescription)")
                    }
                }
            } else if let error = avAssetWriter.error {
                print("写入失败:\(error.localizedDescription)")
            }
            
            // 重置状态,避免重复使用时的混乱
            self.avAssetWriter = nil
            self.adaptor = nil
            self.settings = nil
            self.baseTime = nil
        })
    }

    func writeBuffer(buffer: CVPixelBuffer, frameIndex: Int) throws{
        guard let adaptor = adaptor, let baseTime = baseTime else {
            throw "PixelBufferAdaptor invalid"
        }
        
        // 实时场景下适当等待input就绪,避免丢帧
        while !adaptor.assetWriterInput.isReadyForMoreMediaData {
            Thread.sleep(forTimeInterval: 0.01)
        }
        
        // 生成连续的时间戳
        let presentationTime = CMTimeAdd(baseTime, CMTimeMake(value: Int64(frameIndex), timescale: fps))
        let success = adaptor.append(buffer, withPresentationTime: presentationTime)
        
        if !success {
            if let error = avAssetWriter?.error {
                throw "追加缓冲区失败:\(error.localizedDescription)"
            } else {
                throw "追加缓冲区失败,未知错误"
            }
        }
    }
}

额外说明

  1. 命名规范:修复了原代码中不符合Swift命名规范的变量名(如Adaptor改为adaptor),提升代码可读性。
  2. 时间戳优化:使用基于帧率的连续时间戳,避免时间基准不一致导致的录制异常。
  3. 错误处理增强:增加了更详细的错误信息,便于调试定位问题。
  4. 资源重置:在stopSession完成后重置相关变量,避免重复使用时的状态混乱。

内容的提问来源于stack exchange,提问作者Shimamura Uzuki

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 08:45:46