AVAssetWriter 正在 iOS 设备上的自定义视频流中录制空的 0kb .mp4 文件

AVAssetWriter is recording empty, 0kb, .mp4 files from a custom video stream on iOS device

我正在观看视频流并制作 .mp4 文件,所以我做的大部分都是正确的。我的问题是我的视频文件是 0kb,是空的。我正在使用 iOS 设备来控制带有摄像头的单独设备。此相机正在向 iOS 设备发送视频流,该流被解码为 CMSampleBuffer,然后转换为 CVPixelBuffer 并显示在 UIImageView 中。我正在很好地显示视频(另一个问题是我收到 -12909 错误,如果您知道有关修复的任何信息,请发表评论)。

我尝试记录 CMSampleBuffer 对象,但编译器错误告诉我我需要排除输出设置。所以我删除了那些,它现在保存了空文件。

当流开始时,我称之为:

func beginRecording() {
    handlePhotoLibraryAuth()
    createFilePath()
    guard let videoOutputURL = outputURL,
        let vidWriter = try? AVAssetWriter(outputURL: videoOutputURL, fileType: AVFileType.mov) else {
            fatalError("AVAssetWriter error")
    }
    let vidInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: nil)

    guard vidWriter.canAdd(vidInput) else {
        print("Error: Cant add video writer input")
        return
    }
    vidInput.expectsMediaDataInRealTime = true
    vidWriter.add(vidInput)
    guard vidWriter.startWriting() else {
        print("Error: Cant write with vid writer")
        return
    }
    vidWriter.startSession(atSourceTime: CMTime.zero)

    self.videoWriter = vidWriter
    self.videoWriterInput = vidInput
    self.isRecording = true
    print("Recording: \(self.isRecording)")
}

到此结束:

func endRecording() {
    guard let vidInput = videoWriterInput, let vidWriter = videoWriter else {
        print("Error, no video writer or video input")
        return
    }
    vidInput.markAsFinished()
    vidWriter.finishWriting {
        print("Finished Recording")
        self.isRecording = false
        guard vidWriter.status == .completed else {
            print("Warning: The Video Writer status is not completed, status: \(vidWriter.status)")
            return
        }
        print("VideoWriter status is completed")
        self.saveRecordingToPhotoLibrary()
    }
}

我确定我在 AVAssetWriterInput 上的追加操作失败了

这是我当前的附加代码,我确实首先实时尝试了 CMSampleBuffer,但我不确定为什么不起作用。我怀疑实时功能仅适用于 iOS 设备的 AV 组件,不适用于其他连接的设备。然后我尝试了这个应该可以工作但不是。我尝试了 30 和 60fps,但应该是 30。我在滥用 CMTime 吗?因为我试图不使用 CMTime,但没有像我提到的那样工作。

        if self.videoDecoder.isRecording,
            let videoPixelBuffer = self.videoDecoder.videoWriterInputPixelBufferAdaptor,
            videoPixelBuffer.assetWriterInput.isReadyForMoreMediaData {
            print(videoPixelBuffer.append(frame, withPresentationTime: CMTimeMake(value: self.videoDecoder.videoFrameCounter, timescale: 30)))
            self.videoDecoder.videoFrameCounter += 1
        }

这是我的最终代码解决方案 - 我遇到的最后一个问题是我在 Github/Google 上发现的一些示例项目使用 CMTime 的方式非常奇怪。此外,我无法找到一种方法将我的 mp4 文件发送到照片库 - 它们总是带有正确 size/length 的灰色视频。所以我不得不从设备上的应用程序文件目录访问它们。

import UIKit
import AVFoundation
import AssetsLibrary

final class VideoRecorder: NSObject {

var isRecording: Bool = false
private var frameDuration: CMTime = CMTime(value: 1, timescale: 30)
private var nextPTS: CMTime = .zero
private var assetWriter: AVAssetWriter?
private var assetWriterInput: AVAssetWriterInput?
private var path = ""
private var outputURL: URL?

private func createFilePath() {
    let fileManager = FileManager.default
    let urls = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
    guard let documentDirectory: NSURL = urls.first as NSURL? else {
        print("Error: documentDir Error")
        return
    }
    let date = Date()
    let calendar = Calendar.current
    let month = calendar.component(.month, from: date)
    let day = calendar.component(.day, from: date)
    let hour = calendar.component(.hour, from: date)
    let minute = calendar.component(.minute, from: date)
    let second = calendar.component(.second, from: date)
    guard let videoOutputURL = documentDirectory.appendingPathComponent("MyRecording_\(month)-\(day)_\(hour)-\(minute)-\(second).mp4") else {
        print("Error: Cannot create Video Output file path URL")
        return
    }
    self.outputURL = videoOutputURL
    self.path = videoOutputURL.path
    print(self.path)
    if FileManager.default.fileExists(atPath: path) {
        do {
            try FileManager.default.removeItem(atPath: path)
        } catch {
            print("Unable to delete file: \(error) : \(#function).")
            return
        }
    }
}

public func startStop() {
    if self.isRecording {
        self.stopRecording() { successfulCompletion in
            print("Stopped Recording: \(successfulCompletion)")
        }
    } else {
        self.startRecording()
    }
}

private func startRecording() {
    guard !self.isRecording else {
        print("Warning: Cannot start recording because \(Self.self) is already recording")
        return
    }
    self.createFilePath()
    print("Started Recording")
    self.isRecording = true
}

public func appendFrame(_ sampleBuffer: CMSampleBuffer) {
    // set up the AVAssetWriter using the format description from the first sample buffer captured
    if self.assetWriter == nil {
        let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer)
        guard self.setupAssetWriter(format: formatDescription) else {
            print("Error: Failed to set up asset writer")
            self.assetWriter = nil
            return
        }
    }
    guard self.assetWriter != nil else {
        print("Error: Attempting to append frame when AVAssetWriter is nil")
        return
    }
    // re-time the sample buffer - in this sample frameDuration is set to 30 fps
    var timingInfo = CMSampleTimingInfo.invalid // a way to get an instance without providing 3 CMTime objects
    timingInfo.duration = self.frameDuration
    timingInfo.presentationTimeStamp = self.nextPTS
    var sbufWithNewTiming: CMSampleBuffer? = nil
    guard CMSampleBufferCreateCopyWithNewTiming(allocator: kCFAllocatorDefault,
                                                sampleBuffer: sampleBuffer,
                                                sampleTimingEntryCount: 1, // numSampleTimingEntries
                                                sampleTimingArray: &timingInfo,
                                                sampleBufferOut: &sbufWithNewTiming) == 0 else {
        print("Error: Failed to set up CMSampleBufferCreateCopyWithNewTiming")
        return
    }
    
    // append the sample buffer if we can and increment presentation time
    guard let writeInput = self.assetWriterInput, writeInput.isReadyForMoreMediaData else {
        print("Error: AVAssetWriterInput not ready for more media")
        return
    }
    guard let sbufWithNewTiming = sbufWithNewTiming else {
        print("Error: sbufWithNewTiming is nil")
        return
    }
    
    if writeInput.append(sbufWithNewTiming) {
        self.nextPTS = CMTimeAdd(self.frameDuration, self.nextPTS)
    } else if let error = self.assetWriter?.error {
        logError(error)
        print("Error: Failed to append sample buffer: \(error)")
    } else {
        print("Error: Something went horribly wrong with appending sample buffer")
    }
    // release the copy of the sample buffer we made
}

private func setupAssetWriter(format formatDescription: CMFormatDescription?) -> Bool {
    // allocate the writer object with our output file URL
    let videoWriter: AVAssetWriter
    do {
        videoWriter = try AVAssetWriter(outputURL: URL(fileURLWithPath: self.path), fileType: AVFileType.mp4)
    } catch {
        logError(error)
        return false
    }
    guard formatDescription != nil else {
        print("Error: No Format For Video to create AVAssetWriter")
        return false
    }
    // initialize a new input for video to receive sample buffers for writing
    // passing nil for outputSettings instructs the input to pass through appended samples, doing no processing before they are written
    let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: nil, sourceFormatHint: formatDescription)
    videoInput.expectsMediaDataInRealTime = true
    guard videoWriter.canAdd(videoInput) else {
        print("Error: Cannot add Video Input to AVAssetWriter")
        return false
    }
    videoWriter.add(videoInput)
    
    // initiates a sample-writing at time 0
    self.nextPTS = CMTime.zero
    videoWriter.startWriting()
    videoWriter.startSession(atSourceTime: CMTime.zero)
    self.assetWriter = videoWriter
    self.assetWriterInput = videoInput
    return true
}

private func stopRecording(completion: @escaping (Bool) -> ()) {
    guard self.isRecording else {
        print("Warning: Cannot stop recording because \(Self.self) is not recording")
        completion(false)
        return
    }
    self.isRecording = false
    guard assetWriter != nil else {
        print("Error: AssetWriter is nil")
        completion(false)
        return
    }
    assetWriterInput?.markAsFinished()
    assetWriter?.finishWriting() {
        self.assetWriter = nil
        self.assetWriterInput = nil
        self.path = ""
        self.outputURL = nil
        completion(true)
    }
}
}