swift AVAssetWriterInput -捕获的音频的视频帧不足

zc0qhyus  于 2022-12-10  发布在  Swift
关注(0)|答案(2)|浏览(197)

我有一个中等复杂的AVAssetWriterInput设置,我使用它可以在录制时翻转摄像机。基本上运行两个会话,当用户点击翻转摄像机时,我断开会话1与输出的连接,并连接会话2。
这真的很好用。我可以导出视频,播放起来也很好。
现在,我尝试对生成的视频进行更高级的处理,但出现了一些问题,特别是导出的AVAsset内部的AVAssetTracks略微不匹配(总是小于1帧)。https://www.raywenderlich.com/6236502-avfoundation-tutorial-adding-overlays-and-animations-to-videos但是有相当一部分时间会出现一个全黑帧,有时在视频的开头,有时在视频的结尾,出现一瞬间。时间会有所不同,但总是小于一帧(见下面的日志,1/30或0.033333333s)
我做了一些来回调试,我设法用我的录像机录制了一段视频,但是 * 使用教程代码,我还不能创建一个视频,产生一个尾随黑色帧 *。我添加了一些类似的日志(粘贴到下面的内容)到教程代码中,我看到的增量不超过2/100秒。所以最多大约是1帧的1/10。有一次甚至是完美的0。
我现在的感觉是,我录制视频时,两个assetInputs开始吞噬数据,然后当我说“停止”时,它们停止。视频输入在最后一个完整帧停止,音频输入也是如此。但由于音频输入的采样速率比视频高得多,所以它们不能完美同步,最终得到的音频比视频多。这不是“t一个问题,直到我组成一个资产与两个轨道,然后合成引擎认为我的意思是“是的,实际上使用100%的所有时间为所有轨道,即使有不匹配”,这导致黑屏。
(Edit:基本上就是这样-https://blender.stackexchange.com/questions/6268/audio-track-and-video-track-are-not-the-same-length
我认为正确的解决方案是,不要担心合成结构和时间,并确保它是正确的,只要使捕获的音频和视频尽可能好地匹配。理想情况下为0,但我可以使用任何大约1/10帧的内容。
所以我的问题是:如何创建两个AVAssetWriterInputs(一个音频和一个视频),连接到AVAssetWriter的队列是否更好?是否存在某个设置?我是否弄乱了帧速率?我是否应该将导出的资源修剪到视频轨道的长度?当我停止录制时,我是否可以复制最后捕获的帧?我是否可以使用它,以便输入在不同的时间停止-基本上是先停止音频,然后等待视频“赶上”,然后停止视频?还有别的吗?我在这里不知所措的想法:|

我的日志记录

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)**
5vf7fwbs

5vf7fwbs1#

TL;DR - don't just AVAssetWriter.finishWriting {} because then the last written frame is T_End. Instead, use AVAssetWriter.endSession(atSourceTime:) to set T_End to be the time of the last written video frame.
AVCaptureVideoDataOutputSampleBufferDelegate TO THE RESCUE!!
Use AVCapture(Video|Audio)DataOutputSampleBufferDelegate to write buffers to the AVAssetWriter (attach delegates to AVCaptureVideoDataOutput and AVCaptureAudioDataOutput)
Once the session is started and your outputs are going they're going to constantly be spitting out data onto this delegate

  1. canWrite is a flag that tracks whether you should be recording (writing sampleBuffers to the AVAssetWriter) or not
  2. In order to prevent leading black frames we need to make sure the first frame is a video frame. Until we get a video frame even if we're recording we ignore the frames. startSession(atSourceTime:) sets T0 for the asset, which we're setting to be the time of the first video frame
  3. Every time a video frame is written, record that time on a separate queue. This frees up the delegateQueue to only do frame processing//writing as well as guaranteeing that stopping recording (which will be triggered from the main queue) will not have collisions or memory issues when reading the lastVideoFrameWrite
    Now for the fun part!
  4. In order to prevent trailing black frames, we have the AVAssetWriter end its session at T_lastVideoFrameTime. This discards all frames (audio and video) that were written after T_lastVideoFrameTime ensuring that both assetTracks inside the AVAssetWriter are as synced up as possible.

RESULTS

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

LOOK AT THOSE DELTAS!!!!! all sub millisecond. Very nice.

CODE
To Record

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

Stop Recording

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
             ...
        }
    ...
    }
}
jc3wubiy

jc3wubiy2#

要删除最后一帧,请参阅nickneedsaname的答案
删除第一帧。我们需要在获得第一个图像帧后开始会话。

// if sessionAtSourceTime == nil {
//     sessionAtSourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
//     videoWriter.startSession(atSourceTime: sessionAtSourceTime!)
// }

if output == videoDataOutput, videoWriterInput.isReadyForMoreMediaData {
    // ---> MOVE TO HERE
    if sessionAtSourceTime == nil {
        sessionAtSourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
        videoWriter.startSession(atSourceTime: sessionAtSourceTime!)
    }
    
    // Write video buffer
    videoWriterInput.append(sampleBuffer)
    self.lastVideoFrameWriteTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
} else if sessionAtSourceTime != nil && output == audioDataOutput, audioWriterInput.isReadyForMoreMediaData {
    // Write audio buffer
    self.audioWriterInput.append(sampleBuffer)
}

相关问题