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

3
我希望找到一种方法,在前后摄像头之间切换时保持无缝的音频跟踪。市场上有许多应用程序可以做到这一点,例如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即使你切换到前置摄像头,也会使用后置摄像头的音频。尝试继续使用后置摄像头的音频? - Joe Ginley
我想我试过了,但不能确定。好主意,谢谢。 - Andres Canella
值得一试,我知道我正在修复一个Siri不工作的iPhone,它使用前置麦克风。有趣的是,Snapchat仍然可以录制带有音频的前置视频。祝你好运,让我知道你想到了什么,我很感兴趣听听! - Joe Ginley
谢谢,我回来后会在这里发布并寻找一个可靠的解决方案。我最终只是重新对齐了音频,因此目前有一点间隔。Tinypop应用程序。 - Andres Canella
3个回答

4

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

抱歉,我的答案将使用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)
}

请注意 - 这个方法之所以有效是因为我一直保持帧率的稳定,因此请确保在整个过程中您对捕获设备的帧率有完全控制。
我有一个使用此逻辑的 repo,请查看此处

1
很好的简洁回答!谢谢分享。正如你所看到的,这是一个非常古老的帖子。我目前没有在处理这个问题。但如果解决方案可行,我相信它会帮助看到它的人。 - Andres Canella

1
这个问题最稳妥的解决方法是在切换源时暂停录制。同时,您也可以使用空白视频和静音音频帧来“填补”间隙。这就是我在项目中实现的方式。因此,在切换相机/麦克风时创建一个布尔值来阻止追加新的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
}

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

嘿,迈克。非常想看一下创建空白/无声视频/音频缓冲区的代码。我真的很苦恼这个问题。 - Christian Ayscue
1
嘿,Christian。当然可以。我有一个包含一些实用工具的存储库。看看这个链接:https://github.com/MikeSoftZP/swift-utilities/blob/master/swift-utilities/Helpers/CMSampleBuffer%2BUtilities.swift - MikeSoft

1

针对我在Woody Jean-louis方案中使用其repo时遇到的同步问题,我成功找到了一个中间解决方案。

结果类似于Instagram的效果,但似乎更好。基本上,我的做法是在切换相机时防止assetWriterAudioInput添加新样本。由于无法准确知道何时发生切换,因此我想出了一个方法,在切换前后captureOutput方法每0.02秒+-(最大0.04秒)发送视频样本。

知道这些后,我创建了一个self.lastVideoSampleDate,每次将视频样本附加到assetWriterInputPixelBufferAdator时更新它,只有当该日期小于0.05时,才允许将音频样本附加到assetWriterAudioInput

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接