AVAssetWriterInput - 捕获音频的视频帧不足
AVAssetWriterInput - Insufficient video frames for Captured Audio
我有一个相当复杂的 AVAssetWriterInput 设置,我正在使用它来在我录制时翻转相机。基本上 运行 两个会话,当用户点击翻转相机时,我断开会话 1 与输出的连接并附加会话 2。
这真的很棒。我可以导出视频,它播放得很好。
现在我正在尝试对生成的视频做更高级的事情,一些问题突然出现,特别是导出的 AVAsset 内部的 AVAssetTracks 稍微不匹配(总是小于 1 帧)。具体来说,我正在尝试这样做:https://www.raywenderlich.com/6236502-avfoundation-tutorial-adding-overlays-and-animations-to-videos 但大部分时间最终都是全黑帧,有时在视频的开头,有时在视频的结尾,出现了一瞬间。时间各不相同,但始终小于一帧(请参阅下面的日志,1/30 或 0.033333333 秒)
我进行了一些来回调试,并设法使用我的录像机录制了一段视频,该视频始终产生尾随黑框,但是 使用教程代码我无法做到创建一个产生尾部黑框的视频。我在教程代码中添加了一些类似的日志记录(粘贴在下面),我看到的增量不超过 2/100 秒。所以最多大约是 1 帧的 1/10。有一次甚至是完美的 0。
所以我现在的感觉是,我正在录制我的视频,两个 assetInputs 开始吞噬数据,然后当我说“停止”时,它们停止了。视频输入在最后一个完整帧停止,音频输入也类似。但是由于音频输入的采样率比视频高得多,所以它们没有完美同步,我最终得到的音频多于视频。这不是问题,直到我用两条轨道组成一个资产,然后合成引擎认为我的意思是“是的,实际上所有轨道都使用 100% 的时间,即使存在不匹配”,这导致黑屏.
(编辑:这基本上就是正在发生的事情 - https://blender.stackexchange.com/questions/6268/audio-track-and-video-track-are-not-the-same-length)
我认为正确的解决方案是,与其担心构图构造和时间安排并确保一切正常,不如让捕获的音频和视频尽可能匹配。理想情况下为 0,但我可以接受大约 1/10 帧的任何内容。
所以我的问题是:如何使附加到 AVAssetWriter 的两个 AVAssetWriterInputs(一个音频和一个视频)排列得更好?有什么地方可以设置吗?我会弄乱帧率吗?我应该 trim 导出的资产到视频轨道的长度吗?我可以在停止录制时复制最后捕获的帧吗?我可以让它在不同的时间停止输入 - 基本上先停止音频然后等待视频 'catch up' 然后停止视频?还有别的吗?我在这里不知所措:|
我的记录
BUFFER | VIdeo SETTINGS: Optional(["AVVideoCompressionPropertiesKey": {
AllowFrameReordering = 1;
AllowOpenGOP = 1;
AverageBitRate = 7651584;
**ExpectedFrameRate = 30;**
MaxKeyFrameIntervalDuration = 1;
MaxQuantizationParameter = 41;
MinimizeMemoryUsage = 1;
Priority = 80;
ProfileLevel = "HEVC_Main_AutoLevel";
RealTime = 1;
RelaxAverageBitRateTarget = 1;
SoftMinQuantizationParameter = 18;
}, "AVVideoCodecKey": hvc1, "AVVideoWidthKey": 1080, "AVVideoHeightKey": 1920])
BUFFER | AUDIO SETTINGS Optional(["AVNumberOfChannelsKey": 1, "AVFormatIDKey": 1633772320, **"AVSampleRateKey": 48000**])
BUFFER | asset duration: 0.5333333333333333
BUFFER | video track duration: 0.5066666666666667
BUFFER | Audio track duration: 0.5333333333333333
**BUFFER | Asset Delta: -0.026666666666666616**
BUFFER | asset duration: 0.384
BUFFER | video track duration: 0.37333333333333335
BUFFER | Audio track duration: 0.384
**BUFFER | Asset Delta: -0.010666666666666658**
BUFFER | asset duration: 0.9405416666666667
BUFFER | video track duration: 0.935
BUFFER | Audio track duration: 0.9405416666666667
**BUFFER | Asset Delta: -0.005541666666666667**
教程记录
COMPOSE | asset duration: 0.7333333333333333
COMPOSE | video track duration: 0.7333333333333333
COMPOSE | audio track duration: 0.7316666666666667
**Delta: ~0.01667**
COMPOSE | asset duration: 1.3333333333333333
COMPOSE | video track duration: 1.3333333333333333
COMPOSE | audio track duration: 1.3316666666666668
**Delta: ~0.01667**
COMPOSE | asset duration: 1.0316666666666667
COMPOSE | video track duration: 1.0316666666666667
COMPOSE | audio track duration: 1.0316666666666667
**Delta: 0 (wow)**
TL;DR - 不要只是 AVAssetWriter.finishWriting {}
因为最后写入的帧是 T_End。相反,使用 AVAssetWriter.endSession(atSourceTime:)
将 T_End 设置为最后写入视频帧的时间。
AVCaptureVideoDataOutputSampleBufferDelegate 来救援!!
使用 AVCapture(Video|Audio)DataOutputSampleBufferDelegate 将缓冲区写入 AVAssetWriter(将委托附加到 AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput)
一旦会话开始并且您的输出开始,他们将不断地向该委托发送数据
- canWrite 是一个标志,用于跟踪您是否应该记录(将 sampleBuffers 写入 AVAssetWriter)或不
- 为了防止前导黑帧,我们需要确保第一个帧是视频帧。在我们获得视频帧之前,即使我们正在录制,我们也会忽略这些帧。 startSession(atSourceTime:) 为资产设置 T0,我们将其设置为第一个视频帧的时间
- 每次写入视频帧时,将该时间记录在单独的队列中。这释放了 delegateQueue 以仅进行帧处理//写入以及保证停止记录(将从
main
队列触发)在读取 lastVideoFrameWrite
[ 时不会发生冲突或内存问题=48=]
现在是有趣的部分!
- 为了防止尾随黑帧,我们让 AVAssetWriter 在 T_lastVideoFrameTime 结束它的会话。这会丢弃在 T_lastVideoFrameTime 之后写入的所有帧(音频和视频),确保 AVAssetWriter 中的两个 assetTracks 尽可能同步。
结果
BUFFER | asset duration: 1.8683333333333334
BUFFER | video track duration: 1.8683333333333334
BUFFER | Audio track duration: 1.868
BUFFER | Asset Delta: 0.0003333333333332966
BUFFER | asset duration: 1.435
BUFFER | video track duration: 1.435
BUFFER | Audio track duration: 1.4343333333333332
BUFFER | Asset Delta: 0.0006666666666668153
BUFFER | asset duration: 1.8683333333333334
BUFFER | video track duration: 1.8683333333333334
BUFFER | Audio track duration: 1.8682291666666666
BUFFER | Asset Delta: 0.00010416666666679397
BUFFER | asset duration: 1.435
BUFFER | video track duration: 1.435
BUFFER | Audio track duration: 1.4343541666666666
BUFFER | Asset Delta: 0.0006458333333334565
看看那些三角洲!!!!!所有亚毫秒。很不错。
代码
录制
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard CMSampleBufferDataIsReady(sampleBuffer) else {
return
}
if output == audioDataOutput {
// PROCESS AUDIO BUFFER
}
if output == videoDataOutput {
// PROCESS VIDEO BUFFER
}
// 1
let writable = canWrite
let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
if writable && sessionAtSourceTime == nil {
// 2
if output == videoDataOutput {
sessionAtSourceTime = time
videoWriter.startSession(atSourceTime: sessionAtSourceTime!)
} else {
return
}
}
if output == videoDataOutput && writable {
if videoWriterInput != nil {
if videoWriterInput.isReadyForMoreMediaData {
//Write video buffer
videoWriterInput.append(sampleBuffer)
// 3
WBufferCameraSessionController.finishRecordQueue.async {
self.lastVideoFrameWrite = time
}
}
}
} else if writable,
output == audioDataOutput,
audioWriterInput != nil,
audioWriterInput.isReadyForMoreMediaData {
//Write audio buffer
audioWriterInput.append(sampleBuffer)
}
if output == videoDataOutput {
bufferDelegate?.didOuputVideoBuffer(buffer: sampleBuffer)
}
}
停止录制
func stopRecording() {
guard isRecording else {
return
}
guard isStoppingRecording == false else {
return
}
isStoppingRecording = true
WBufferCameraSessionController.finishRecordQueue.async {
// 4
if self.lastVideoFrameWrite != nil {
self.videoWriter.endSession(atSourceTime: self.lastVideoFrameWrite)
}
self.videoWriter.finishWriting {
// cleanup, do stuff with finished file if writing was successful
...
}
...
}
}
我有一个相当复杂的 AVAssetWriterInput 设置,我正在使用它来在我录制时翻转相机。基本上 运行 两个会话,当用户点击翻转相机时,我断开会话 1 与输出的连接并附加会话 2。
这真的很棒。我可以导出视频,它播放得很好。
现在我正在尝试对生成的视频做更高级的事情,一些问题突然出现,特别是导出的 AVAsset 内部的 AVAssetTracks 稍微不匹配(总是小于 1 帧)。具体来说,我正在尝试这样做:https://www.raywenderlich.com/6236502-avfoundation-tutorial-adding-overlays-and-animations-to-videos 但大部分时间最终都是全黑帧,有时在视频的开头,有时在视频的结尾,出现了一瞬间。时间各不相同,但始终小于一帧(请参阅下面的日志,1/30 或 0.033333333 秒)
我进行了一些来回调试,并设法使用我的录像机录制了一段视频,该视频始终产生尾随黑框,但是 使用教程代码我无法做到创建一个产生尾部黑框的视频。我在教程代码中添加了一些类似的日志记录(粘贴在下面),我看到的增量不超过 2/100 秒。所以最多大约是 1 帧的 1/10。有一次甚至是完美的 0。
所以我现在的感觉是,我正在录制我的视频,两个 assetInputs 开始吞噬数据,然后当我说“停止”时,它们停止了。视频输入在最后一个完整帧停止,音频输入也类似。但是由于音频输入的采样率比视频高得多,所以它们没有完美同步,我最终得到的音频多于视频。这不是问题,直到我用两条轨道组成一个资产,然后合成引擎认为我的意思是“是的,实际上所有轨道都使用 100% 的时间,即使存在不匹配”,这导致黑屏.
(编辑:这基本上就是正在发生的事情 - https://blender.stackexchange.com/questions/6268/audio-track-and-video-track-are-not-the-same-length)
我认为正确的解决方案是,与其担心构图构造和时间安排并确保一切正常,不如让捕获的音频和视频尽可能匹配。理想情况下为 0,但我可以接受大约 1/10 帧的任何内容。
所以我的问题是:如何使附加到 AVAssetWriter 的两个 AVAssetWriterInputs(一个音频和一个视频)排列得更好?有什么地方可以设置吗?我会弄乱帧率吗?我应该 trim 导出的资产到视频轨道的长度吗?我可以在停止录制时复制最后捕获的帧吗?我可以让它在不同的时间停止输入 - 基本上先停止音频然后等待视频 'catch up' 然后停止视频?还有别的吗?我在这里不知所措:|
我的记录
BUFFER | VIdeo SETTINGS: Optional(["AVVideoCompressionPropertiesKey": {
AllowFrameReordering = 1;
AllowOpenGOP = 1;
AverageBitRate = 7651584;
**ExpectedFrameRate = 30;**
MaxKeyFrameIntervalDuration = 1;
MaxQuantizationParameter = 41;
MinimizeMemoryUsage = 1;
Priority = 80;
ProfileLevel = "HEVC_Main_AutoLevel";
RealTime = 1;
RelaxAverageBitRateTarget = 1;
SoftMinQuantizationParameter = 18;
}, "AVVideoCodecKey": hvc1, "AVVideoWidthKey": 1080, "AVVideoHeightKey": 1920])
BUFFER | AUDIO SETTINGS Optional(["AVNumberOfChannelsKey": 1, "AVFormatIDKey": 1633772320, **"AVSampleRateKey": 48000**])
BUFFER | asset duration: 0.5333333333333333
BUFFER | video track duration: 0.5066666666666667
BUFFER | Audio track duration: 0.5333333333333333
**BUFFER | Asset Delta: -0.026666666666666616**
BUFFER | asset duration: 0.384
BUFFER | video track duration: 0.37333333333333335
BUFFER | Audio track duration: 0.384
**BUFFER | Asset Delta: -0.010666666666666658**
BUFFER | asset duration: 0.9405416666666667
BUFFER | video track duration: 0.935
BUFFER | Audio track duration: 0.9405416666666667
**BUFFER | Asset Delta: -0.005541666666666667**
教程记录
COMPOSE | asset duration: 0.7333333333333333
COMPOSE | video track duration: 0.7333333333333333
COMPOSE | audio track duration: 0.7316666666666667
**Delta: ~0.01667**
COMPOSE | asset duration: 1.3333333333333333
COMPOSE | video track duration: 1.3333333333333333
COMPOSE | audio track duration: 1.3316666666666668
**Delta: ~0.01667**
COMPOSE | asset duration: 1.0316666666666667
COMPOSE | video track duration: 1.0316666666666667
COMPOSE | audio track duration: 1.0316666666666667
**Delta: 0 (wow)**
TL;DR - 不要只是 AVAssetWriter.finishWriting {}
因为最后写入的帧是 T_End。相反,使用 AVAssetWriter.endSession(atSourceTime:)
将 T_End 设置为最后写入视频帧的时间。
AVCaptureVideoDataOutputSampleBufferDelegate 来救援!!
使用 AVCapture(Video|Audio)DataOutputSampleBufferDelegate 将缓冲区写入 AVAssetWriter(将委托附加到 AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput)
一旦会话开始并且您的输出开始,他们将不断地向该委托发送数据
- canWrite 是一个标志,用于跟踪您是否应该记录(将 sampleBuffers 写入 AVAssetWriter)或不
- 为了防止前导黑帧,我们需要确保第一个帧是视频帧。在我们获得视频帧之前,即使我们正在录制,我们也会忽略这些帧。 startSession(atSourceTime:) 为资产设置 T0,我们将其设置为第一个视频帧的时间
- 每次写入视频帧时,将该时间记录在单独的队列中。这释放了 delegateQueue 以仅进行帧处理//写入以及保证停止记录(将从
main
队列触发)在读取lastVideoFrameWrite
[ 时不会发生冲突或内存问题=48=]
现在是有趣的部分!
- 为了防止尾随黑帧,我们让 AVAssetWriter 在 T_lastVideoFrameTime 结束它的会话。这会丢弃在 T_lastVideoFrameTime 之后写入的所有帧(音频和视频),确保 AVAssetWriter 中的两个 assetTracks 尽可能同步。
结果
BUFFER | asset duration: 1.8683333333333334
BUFFER | video track duration: 1.8683333333333334
BUFFER | Audio track duration: 1.868
BUFFER | Asset Delta: 0.0003333333333332966
BUFFER | asset duration: 1.435
BUFFER | video track duration: 1.435
BUFFER | Audio track duration: 1.4343333333333332
BUFFER | Asset Delta: 0.0006666666666668153
BUFFER | asset duration: 1.8683333333333334
BUFFER | video track duration: 1.8683333333333334
BUFFER | Audio track duration: 1.8682291666666666
BUFFER | Asset Delta: 0.00010416666666679397
BUFFER | asset duration: 1.435
BUFFER | video track duration: 1.435
BUFFER | Audio track duration: 1.4343541666666666
BUFFER | Asset Delta: 0.0006458333333334565
看看那些三角洲!!!!!所有亚毫秒。很不错。
代码
录制
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard CMSampleBufferDataIsReady(sampleBuffer) else {
return
}
if output == audioDataOutput {
// PROCESS AUDIO BUFFER
}
if output == videoDataOutput {
// PROCESS VIDEO BUFFER
}
// 1
let writable = canWrite
let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
if writable && sessionAtSourceTime == nil {
// 2
if output == videoDataOutput {
sessionAtSourceTime = time
videoWriter.startSession(atSourceTime: sessionAtSourceTime!)
} else {
return
}
}
if output == videoDataOutput && writable {
if videoWriterInput != nil {
if videoWriterInput.isReadyForMoreMediaData {
//Write video buffer
videoWriterInput.append(sampleBuffer)
// 3
WBufferCameraSessionController.finishRecordQueue.async {
self.lastVideoFrameWrite = time
}
}
}
} else if writable,
output == audioDataOutput,
audioWriterInput != nil,
audioWriterInput.isReadyForMoreMediaData {
//Write audio buffer
audioWriterInput.append(sampleBuffer)
}
if output == videoDataOutput {
bufferDelegate?.didOuputVideoBuffer(buffer: sampleBuffer)
}
}
停止录制
func stopRecording() {
guard isRecording else {
return
}
guard isStoppingRecording == false else {
return
}
isStoppingRecording = true
WBufferCameraSessionController.finishRecordQueue.async {
// 4
if self.lastVideoFrameWrite != nil {
self.videoWriter.endSession(atSourceTime: self.lastVideoFrameWrite)
}
self.videoWriter.finishWriting {
// cleanup, do stuff with finished file if writing was successful
...
}
...
}
}