使用 AVCaptureSession 和 AVAssetWriter 在翻转相机时无缝录制音频

Posted

技术标签:

【中文标题】使用 AVCaptureSession 和 AVAssetWriter 在翻转相机时无缝录制音频【英文标题】:Seamless audio recording while flipping camera, using AVCaptureSession & AVAssetWriter 【发布时间】:2016-11-08 19:21:42 【问题描述】:

我正在寻找一种在前后摄像头之间切换时保持无缝音轨的方法。市场上的许多应用程序都可以做到这一点,例如 SnapChat……

解决方案应使用 AVCaptureSession 和 AVAssetWriter。此外,它不应明确使用 AVMutableComposition,因为在 AVMutableComposition 和 AVCaptureSession ATM 之间存在bug。另外,我负担不起后期处理时间。

目前,当我更改视频输入时,录音会跳过并变得不同步。

我将包含可能相关的代码。

翻转相机

-(void) updateCameraDirection:(CamDirection)vCameraDirection 
    if(session) 
        AVCaptureDeviceInput* currentInput;
        AVCaptureDeviceInput* newInput;
        BOOL videoMirrored = NO;
        switch (vCameraDirection) 
            case CamDirection_Front:
                currentInput = input_Back;
                newInput = input_Front;
                videoMirrored = NO;
                break;
            case CamDirection_Back:
                currentInput = input_Front;
                newInput = input_Back;
                videoMirrored = YES;
                break;
            default:
                break;
        

        [session beginConfiguration];
        //disconnect old input
        [session removeInput:currentInput];
        //connect new input
        [session addInput:newInput];
        //get new data connection and config
        dataOutputVideoConnection = [dataOutputVideo connectionWithMediaType:AVMediaTypeVideo];
        dataOutputVideoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
        dataOutputVideoConnection.videoMirrored = videoMirrored;
        //finish
        [session commitConfiguration];
    

样本缓冲区

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection 
    //not active
    if(!recordingVideo)
        return;

    //start session if not started
    if(!startedSession) 
        startedSession = YES;
        [assetWriter startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
    

    //Process sample buffers
    if (connection == dataOutputAudioConnection) 
        if([assetWriterInputAudio isReadyForMoreMediaData]) 
            BOOL success = [assetWriterInputAudio appendSampleBuffer:sampleBuffer];
            //…
        

     else if (connection == dataOutputVideoConnection) 
        if([assetWriterInputVideo isReadyForMoreMediaData])         
            BOOL success = [assetWriterInputVideo appendSampleBuffer:sampleBuffer];
            //…
        
    

也许调整音频采样时间戳

【问题讨论】:

我相信即使您切换到前置摄像头,Snapchat 也会使用后置摄像头音频。尝试继续使用后置摄像头的音频? 我想我确实尝试过,但不能肯定地说。好主意,谢谢。 是的,值得一试,我知道我正在修理一部使用前置麦克风的 Siri 无法工作的 iPhone。有趣的是,Snapchat 会录制带有音频的前置视频。祝你好运,让我知道你想出什么我想听的! 谢谢,当我回到这里并寻找可靠的解决方案时会在这里发布。我最终只是重新定时音频,所以有一点差距 atm.. Tinypop 应用程序。 【参考方案1】:

嘿,我遇到了同样的问题,发现在切换相机后,下一帧被推得太远了。这似乎在之后的每一帧都发生了变化,从而导致视频和音频不同步。我的解决方案是在切换相机后将每个错位的帧移到正确的位置。

抱歉,我的答案将在 Swift 4.2 中

您必须使用AVAssetWriterInputPixelBufferAdaptor 才能将样本缓冲区附加到指定的演示时间戳。

previousPresentationTimeStamp 是前一帧的演示时间戳,currentPresentationTimestamp 是你猜到的当前帧的演示时间戳。 maxFrameDistance 在测试时运行良好,但您可以根据自己的喜好进行更改。

let currentFramePosition = (Double(self.frameRate) * Double(currentPresentationTimestamp.value)) / Double(currentPresentationTimestamp.timescale)
let previousFramePosition = (Double(self.frameRate) * Double(previousPresentationTimeStamp.value)) / Double(previousPresentationTimeStamp.timescale)
var presentationTimeStamp = currentPresentationTimestamp
let maxFrameDistance = 1.1
let frameDistance = currentFramePosition - previousFramePosition
if frameDistance > maxFrameDistance 
    let expectedFramePosition = previousFramePosition + 1.0
    //print("[mwCamera]: Frame at incorrect position moving from \(currentFramePosition) to \(expectedFramePosition)")

    let newFramePosition = ((expectedFramePosition) * Double(currentPresentationTimestamp.timescale)) / Double(self.frameRate)

    let newPresentationTimeStamp = CMTime.init(value: CMTimeValue(newFramePosition), timescale: currentPresentationTimestamp.timescale)

    presentationTimeStamp = newPresentationTimeStamp


let success = assetWriterInputPixelBufferAdator.append(pixelBuffer, withPresentationTime: presentationTimeStamp)
if !success, let error = assetWriter.error 
    fatalError(error.localizedDescription)

另外请注意 - 这很有效,因为我保持帧速率一致,因此请确保您在整个过程中完全控制捕获设备的帧速率。

I have a repo using this logic here

【讨论】:

好简洁的回应!感谢分享。如您所知,这是一个非常古老的帖子。我目前不在这方面工作。但如果解决方案有效,我相信它会对看到它的人有所帮助。【参考方案2】:

我确实设法找到了一个中间解决方案来解决我在 Woody Jean-louis 解决方案中使用 is repo 找到的同步问题。

结果与 instagram 的结果相似,但效果似乎更好一些。基本上我所做的是防止 assetWriterAudioInput 在切换相机时附加新样本。无法确切知道何时发生这种情况,所以我发现在切换之前和之后,captureOutput 方法每 0.02 秒 +-(最多 0.04 秒)发送视频样本。

知道了这一点,我创建了一个 self.lastVideoSampleDate,每次将视频样本附加到 assetWriterInputPixelBufferAdator 时都会更新它,并且我只允许将音频样本附加到 >assetWriterAudioInput 是日期低于 0.05。

 if let assetWriterAudioInput = self.assetWriterAudioInput,
            output == self.audioOutput, assetWriterAudioInput.isReadyForMoreMediaData 

            let since = Date().timeIntervalSince(self.lastVideoSampleDate)
            if since < 0.05 
                let success = assetWriterAudioInput.append(sampleBuffer)
                if !success, let error = assetWriter.error 
                    print(error)
                    fatalError(error.localizedDescription)
                
            
        
  let success = assetWriterInputPixelBufferAdator.append(pixelBuffer, withPresentationTime: presentationTimeStamp)
            if !success, let error = assetWriter.error 
                print(error)
                fatalError(error.localizedDescription)
            
            self.lastVideoSampleDate = Date()

【讨论】:

【参考方案3】:

解决此问题最“稳定”的方法是在切换源时“暂停”录制。

但您也可以使用空白视频和无声音频帧来“填补空白”。 这是我在项目中实现的。

因此,创建布尔值以阻止在切换摄像头/麦克风时附加新 CMSampleBuffer 的功能,并在延迟一段时间后将其重置:

let idleTime = 1.0
self.recordingPaused = true
DispatchQueue.main.asyncAfter(deadline: .now() + idleTime) 
  self.recordingPaused = false

writeAllIdleFrames()

writeAllIdleFrames方法中你需要计算你需要写多少帧:

func writeAllIdleFrames() 
    let framesPerSecond = 1.0 / self.videoConfig.fps
    let samplesPerSecond = 1024 / self.audioConfig.sampleRate
    
    let videoFramesCount = Int(ceil(self.switchInputDelay / framesPerSecond))
    let audioFramesCount = Int(ceil(self.switchInputDelay / samplesPerSecond))
    
    for index in 0..<max(videoFramesCount, audioFramesCount) 
        // creation synthetic buffers
        
        recordingQueue.async 
            if index < videoFramesCount 
                let pts = self.nextVideoPTS()
                self.writeBlankVideo(pts: pts)
            
            
            if index < audioFramesCount 
                let pts = self.nextAudioPTS()
                self.writeSilentAudio(pts: pts)
            
        
    

如何计算下一个PTS?

func nextVideoPTS() -> CMTime 
    guard var pts = self.lastVideoRawPTS else  return CMTime.invalid 
    
    let framesPerSecond = 1.0 / self.videoConfig.fps
    let delta = CMTime(value: Int64(framesPerSecond * Double(pts.timescale)),
                       timescale: pts.timescale, flags: pts.flags, epoch: pts.epoch)
    pts = CMTimeAdd(pts, delta)
    return pts

如果您还需要创建空白/静音视频/音频缓冲区的代码,请告诉我:)

【讨论】:

嘿,迈克,是的,很想看看创建空白/静音视频/音频缓冲区的代码。真的很挣扎 嘿克里斯蒂安。当然。我有一个包含其中一些实用程序的仓库。看看:github.com/MikeSoftZP/swift-utilities/blob/master/…

以上是关于使用 AVCaptureSession 和 AVAssetWriter 在翻转相机时无缝录制音频的主要内容,如果未能解决你的问题,请参考以下文章

AVCaptureSession 和 AudioQueue

如何在 AVCaptureSession 上设置音频采样率?

在使用 AVCaptureMovieFileOutput 保存之前修改 AVCaptureSession

如何记录使用 AVCaptureSession 捕获的第一个和最后一个电影帧的确切时间?

从同一个 AVCaptureSession 拍摄视频和照片时应用程序崩溃?

使用 AVCaptureSession 和 AVAssetWriter 在翻转相机时无缝录制音频