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