You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义相机录制视频时切换前后摄像头合并单文件问题求助

Hey there! Let's tackle this problem—switching cameras mid-recording without splitting the video into separate files is totally doable, but the key is to avoid stopping and restarting the recording process. Here's what's happening in your current setup: when you switch cameras, you're probably stopping the active recording, reconfiguring the capture session, then starting a new recording, which creates two distinct files.

Below are two solid solutions, starting with the simpler one that leverages Apple's built-in APIs, followed by a more flexible approach if you need full control over the recording pipeline.

Solution 1: Dynamic Camera Switching with AVCaptureMovieFileOutput (Simpler)

This approach works by pre-configuring both front and back camera inputs, then swapping them without stopping the recording. The AVCaptureMovieFileOutput will keep writing to the same file as long as the capture session remains active.

First, set up your capture session and pre-prepare both camera inputs (do this early, like in viewDidLoad):

import UIKit
import AVFoundation
import CoreMedia

class VideoViewController: UIViewController, AVCaptureFileOutputRecordingDelegate {
    // Pre-initialized camera inputs
    private var frontCameraInput: AVCaptureDeviceInput?
    private var backCameraInput: AVCaptureDeviceInput?
    private var currentCameraInput: AVCaptureDeviceInput?
    
    private let captureSession = AVCaptureSession()
    private let movieFileOutput = AVCaptureMovieFileOutput()
    private var previewLayer: AVCaptureVideoPreviewLayer!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCameraInputs()
        setupMovieOutput()
        setupPreviewLayer()
        startCaptureSession()
    }
    
    // MARK: - Camera Setup
    private func setupCameraInputs() {
        // Get front and back cameras
        guard let frontCam = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front),
              let backCam = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
            print("Could not access cameras")
            return
        }
        
        // Create inputs for both cameras
        do {
            frontCameraInput = try AVCaptureDeviceInput(device: frontCam)
            backCameraInput = try AVCaptureDeviceInput(device: backCam)
        } catch {
            print("Failed to create camera input: \(error.localizedDescription)")
            return
        }
        
        // Add back camera as default input
        if captureSession.canAddInput(backCameraInput!) {
            captureSession.addInput(backCameraInput!)
            currentCameraInput = backCameraInput
        }
    }
    
    private func setupMovieOutput() {
        guard captureSession.canAddOutput(movieFileOutput) else {
            print("Could not add movie output")
            return
        }
        captureSession.addOutput(movieFileOutput)
        
        // Optional: Enable video stabilization if supported
        if let videoConnection = movieFileOutput.connection(with: .video),
           videoConnection.isVideoStabilizationSupported {
            videoConnection.preferredVideoStabilizationMode = .auto
        }
    }
    
    private func setupPreviewLayer() {
        previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.frame = view.bounds
        previewLayer.videoGravity = .resizeAspectFill
        view.layer.addSublayer(previewLayer)
    }
    
    private func startCaptureSession() {
        DispatchQueue.global(qos: .background).async {
            self.captureSession.startRunning()
        }
    }
    
    // MARK: - Recording Controls
    func startRecording() {
        guard !movieFileOutput.isRecording else { return }
        
        let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let outputURL = docsDir.appendingPathComponent("recording_\(Date().timeIntervalSince1970).mov")
        
        movieFileOutput.startRecording(to: outputURL, recordingDelegate: self)
        print("Started recording to \(outputURL)")
    }
    
    func stopRecording() {
        guard movieFileOutput.isRecording else { return }
        movieFileOutput.stopRecording()
    }
    
    // MARK: - Camera Toggle (The Magic Part!)
    func toggleCamera() {
        guard let currentInput = currentCameraInput else { return }
        
        // Determine which camera to switch to
        let newInput = currentInput.device.position == .back ? frontCameraInput : backCameraInput
        
        guard let validNewInput = newInput else {
            print("Could not access target camera")
            return
        }
        
        // Lock the session to modify inputs (critical!)
        captureSession.beginConfiguration()
        
        // Remove current input and add the new one
        captureSession.removeInput(currentInput)
        if captureSession.canAddInput(validNewInput) {
            captureSession.addInput(validNewInput)
            currentCameraInput = validNewInput
        } else {
            // Fallback: re-add the original input if the new one fails
            captureSession.addInput(currentInput)
        }
        
        // Unlock the session to apply changes
        captureSession.commitConfiguration()
        
        // 🎉 No need to stop/restart recording—movieFileOutput keeps writing to the same file!
    }
    
    // MARK: - AVCaptureFileOutputRecordingDelegate
    func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
        if let error = error {
            print("Recording failed: \(error.localizedDescription)")
            return
        }
        print("Recording completed! File at \(outputFileURL)")
        // Handle the finished video file here (e.g., save to photo library)
    }
}

Key Notes for Solution 1:

  • No Recording Interruption: By modifying the capture session's inputs while it's running (using beginConfiguration()/commitConfiguration()), the recording continues seamlessly.
  • Compatibility: Works on most modern iOS devices (iOS 10+). Older devices might have issues, but this is the standard approach.
  • Permissions: Don't forget to request camera, microphone, and photo library permissions in your Info.plist.

Solution 2: Manual Frame Writing with AVAssetWriter (Full Control)

If Solution 1 doesn't work for your use case (e.g., older devices or custom processing needs), you can manually capture video/audio frames and write them to a single file using AVAssetWriter. This gives you complete control over the recording pipeline.

Here's the core implementation:

extension VideoViewController: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
    private var videoDataOutput: AVCaptureVideoDataOutput!
    private var audioDataOutput: AVCaptureAudioDataOutput!
    private var assetWriter: AVAssetWriter?
    private var videoAssetInput: AVAssetWriterInput?
    private var audioAssetInput: AVAssetWriterInput?
    
    private func setupDataOutputs() {
        // Video data output (for capturing frames)
        videoDataOutput = AVCaptureVideoDataOutput()
        videoDataOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        
        // Audio data output (for capturing audio)
        audioDataOutput = AVCaptureAudioDataOutput()
        audioDataOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "audioQueue"))
        
        // Add outputs to session
        if captureSession.canAddOutput(videoDataOutput) {
            captureSession.addOutput(videoDataOutput)
        }
        if captureSession.canAddOutput(audioDataOutput) {
            captureSession.addOutput(audioDataOutput)
        }
    }
    
    private func setupAssetWriter() {
        let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let outputURL = docsDir.appendingPathComponent("custom_recording_\(Date().timeIntervalSince1970).mov")
        
        do {
            assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: .mov)
            
            // Configure video input track
            let videoSettings: [String: Any] = [
                AVVideoCodecKey: AVVideoCodecType.h264,
                AVVideoWidthKey: UIScreen.main.bounds.width,
                AVVideoHeightKey: UIScreen.main.bounds.height
            ]
            videoAssetInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
            videoAssetInput?.expectsMediaDataInRealTime = true
            if assetWriter?.canAdd(videoAssetInput!) == true {
                assetWriter?.add(videoAssetInput!)
            }
            
            // Configure audio input track
            let audioSettings: [String: Any] = [
                AVFormatIDKey: kAudioFormatMPEG4AAC,
                AVNumberOfChannelsKey: 2,
                AVSampleRateKey: 44100.0,
                AVEncoderBitRateKey: 128000
            ]
            audioAssetInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings)
            audioAssetInput?.expectsMediaDataInRealTime = true
            if assetWriter?.canAdd(audioAssetInput!) == true {
                assetWriter?.add(audioAssetInput!)
            }
            
        } catch {
            print("Failed to create asset writer: \(error.localizedDescription)")
            assetWriter = nil
        }
    }
    
    func startCustomRecording() {
        guard let writer = assetWriter, writer.status == .unknown else { return }
        writer.startWriting()
        writer.startSession(atSourceTime: CMTime.zero)
    }
    
    func stopCustomRecording() {
        videoAssetInput?.markAsFinished()
        audioAssetInput?.markAsFinished()
        assetWriter?.finishWriting(completionHandler: {
            switch self.assetWriter?.status {
            case .completed:
                print("Custom recording finished successfully!")
            case .failed:
                print("Custom recording failed: \(self.assetWriter?.error?.localizedDescription ?? "Unknown error")")
            default:
                break
            }
        })
    }
    
    // MARK: - Sample Buffer Delegates
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let writer = assetWriter else { return }
        
        if output == videoDataOutput {
            if videoAssetInput?.isReadyForMoreMediaData == true {
                videoAssetInput?.append(sampleBuffer)
            }
        } else if output == audioDataOutput {
            if audioAssetInput?.isReadyForMoreMediaData == true {
                audioAssetInput?.append(sampleBuffer)
            }
        }
    }
}

Key Notes for Solution 2:

  • Full Control: You can modify frames before writing (e.g., apply filters) and handle edge cases more granularly.
  • Complexity: Requires more code to manage frame timing, error handling, and session state.
  • Camera Toggle: Use the same toggleCamera() method from Solution 1—no changes needed here, since we're just swapping inputs while capturing frames.

Final Tips
  • Test on Real Devices: Simulator doesn't support camera switching or full recording functionality, so always test on a physical device.
  • Handle Errors: Add error handling for session configuration failures, permission issues, and recording errors to make your app robust.
  • Performance: For Solution 2, use background queues for frame processing to avoid blocking the main thread.

内容的提问来源于stack exchange,提问作者Wagner Filho

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 08:42:43