自定义相机录制视频时切换前后摄像头合并单文件问题求助
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.
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.
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.
- 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

