iosswiftavfoundationavassetwriteravassetwriterinput

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


I am seeing a video stream and making .mp4 files so I am doing most of this correctly. My problem is that my video files are 0kb, empty. I'm using an iOS device to control a separate device with a camera. This camera is sending a video stream to the iOS device and that stream is decoded into a CMSampleBuffer then turned into a CVPixelBuffer and displayed in an UIImageView. I'm displaying the video just fine(and a separate issue is that I'm getting -12909 errors if you know anything about fixing that pls leave a comment).

I tried recording the CMSampleBuffer objects but I was told by the compiler errors that I needed to exclude output settings. So I removed those and it saves empty files now.

When the stream starts I call this:

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)")
}

And this ends it:

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()
    }
}

I determined my append operation on AVAssetWriterInput is failing

Here is my current append code, I did try CMSampleBuffer first on realtime, which im not sure why didnt work. I suspect that the realtime feature only applies to the AV components of iOS devices and not other connected devices. Then I tried this which should probably work but is not. I tried both 30 and 60fps, it's supposed to be 30 though. Am I misusing CMTime? Because I was attempting to just not use CMTime and that did not work as I mentioned.

        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
        }

Solution

  • Here was my final code solution - the final issue I had was that CMTime is used very strangely by some example projects i found on Github/Google. Also I was unable to figure out a way to send my mp4 files to the photo library - they would always be grey videos with the correct size/length. So I had to access them from the app files directory on the device.

    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)
        }
    }
    }