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)
}
}
}
我正在观看视频流并制作 .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)
}
}
}