快速录制SCNView视频:局部录制、实时加音及同步预览
Hey there, let's break down how to solve your exact problem—recording a specific middle region of your SCNView with synchronized audio, while keeping the live view visible, using the renderer(_:didRenderScene:atTime:) delegate method. Here's a step-by-step implementation that covers all your requirements:
First, define the properties you'll need to handle video/audio capture, frame cropping, and session control:
import AVFoundation import SceneKit import UIKit class SceneRecorderViewController: UIViewController, SCNSceneRendererDelegate { @IBOutlet weak var scnView: SCNView! // Recording core components private var assetWriter: AVAssetWriter? private var videoInput: AVAssetWriterInput? private var audioInput: AVAssetWriterInput? private var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor? private var audioEngine: AVAudioEngine? // Recording state & target dimensions private var isRecording = false private let targetRecordingSize = CGSize(width: 375, height: 375) }
We'll use AVAssetWriter for flexible control over frame cropping and audio sync. Add this setup method:
private func prepareAssetWriter() -> Bool { let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let outputURL = docsDir.appendingPathComponent("scn_recording_\(Date().timeIntervalSince1970).mov") guard let writer = try? AVAssetWriter(outputURL: outputURL, fileType: .mov) else { print("Failed to create asset writer") return false } // Configure video input for target cropped size let videoSettings: [String: Any] = [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: targetRecordingSize.width, AVVideoHeightKey: targetRecordingSize.height, AVVideoCompressionPropertiesKey: [ AVVideoAverageBitRateKey: 5_000_000, // Adjust bitrate based on quality needs AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel ] ] let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) videoInput.expectsMediaDataInRealTime = true // Configure pixel buffer adapter for frame cropping let pixelBufferAttrs: [String: Any] = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA, kCVPixelBufferWidthKey as String: targetRecordingSize.width, kCVPixelBufferHeightKey as String: targetRecordingSize.height ] let pixelAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoInput, sourcePixelBufferAttributes: pixelBufferAttrs) // Configure audio input let audioSettings: [String: Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVNumberOfChannelsKey: 1, AVSampleRateKey: 44100, AVEncoderBitRateKey: 128000 ] let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings) audioInput.expectsMediaDataInRealTime = true // Add inputs to writer if writer.canAdd(videoInput) { writer.add(videoInput) } if writer.canAdd(audioInput) { writer.add(audioInput) } self.assetWriter = writer self.videoInput = videoInput self.audioInput = audioInput self.pixelBufferAdaptor = pixelAdaptor return true }
Implement the renderer(_:didRenderScene:atTime:) method to grab each rendered frame, crop it to your target middle region, and write it to the asset writer:
func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { guard isRecording, let videoInput = videoInput, videoInput.isReadyForMoreMediaData, let adaptor = pixelBufferAdaptor else { return } // Grab the current SCNView frame guard let fullFrame = scnView.snapshot().cgImage else { return } // Calculate crop rect for middle 375x375 area let scnViewBounds = scnView.bounds let cropYOrigin = (scnViewBounds.height - targetRecordingSize.height) / 2 let cropRect = CGRect(x: 0, y: cropYOrigin, width: targetRecordingSize.width, height: targetRecordingSize.height) // Convert cropped frame to pixel buffer and write if let pixelBuffer = createCroppedPixelBuffer(from: fullFrame, cropRect: cropRect) { let presentationTime = CMTimeMakeWithSeconds(time, preferredTimescale: 600) adaptor.append(pixelBuffer, withPresentationTime: presentationTime) } } // Helper: Crop CGImage to target rect and convert to CVPixelBuffer private func createCroppedPixelBuffer(from cgImage: CGImage, cropRect: CGRect) -> CVPixelBuffer? { var pixelBuffer: CVPixelBuffer? let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(cropRect.width), Int(cropRect.height), kCVPixelFormatType_32BGRA, pixelBufferAdaptor?.sourcePixelBufferAttributes as CFDictionary?, &pixelBuffer) guard status == kCVReturnSuccess, let buffer = pixelBuffer else { return nil } CVPixelBufferLockBaseAddress(buffer, []) defer { CVPixelBufferUnlockBaseAddress(buffer, []) } let context = CGContext(data: CVPixelBufferGetBaseAddress(buffer), width: Int(cropRect.width), height: Int(cropRect.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) // Draw the full image offset to crop to the middle region context?.draw(cgImage, in: CGRect(x: -cropRect.origin.x, y: -cropRect.origin.y, width: cgImage.width, height: cgImage.height)) return buffer }
Add a method to capture microphone audio and feed it into the asset writer in real-time:
private func startAudioCapture() { let audioEngine = AVAudioEngine() self.audioEngine = audioEngine do { let session = AVAudioSession.sharedInstance() try session.setCategory(.playAndRecord, mode: .default) try session.setActive(true) } catch { print("Audio session setup failed: \(error.localizedDescription)") return } let inputNode = audioEngine.inputNode let recordingFormat = inputNode.outputFormat(forBus: 0) inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] buffer, time in guard let self = self, self.isRecording, let audioInput = self.audioInput, audioInput.isReadyForMoreMediaData else { return } let presentationTime = CMTimeMakeWithSeconds(time.sampleTime / time.sampleRate, preferredTimescale: 600) audioInput.append(buffer) } do { try audioEngine.start() } catch { print("Failed to start audio engine: \(error.localizedDescription)") } }
Add action methods to trigger recording and clean up when done:
@IBAction func startRecording(_ sender: UIButton) { guard !isRecording else { return } guard prepareAssetWriter() else { print("Failed to prepare recording session") return } // Start writing assetWriter?.startWriting() assetWriter?.startSession(atSourceTime: CMTime.zero) isRecording = true startAudioCapture() scnView.delegate = self // Ensure delegate is set to capture frames } @IBAction func stopRecording(_ sender: UIButton) { guard isRecording else { return } isRecording = false // Mark inputs as finished videoInput?.markAsFinished() audioInput?.markAsFinished() // Finalize recording assetWriter?.finishWriting(completionHandler: { [weak self] in guard let self = self, let outputURL = self.assetWriter?.outputURL else { return } print("Recording saved to: \(outputURL.path)") // Save to photo library (optional) UISaveVideoAtPathToSavedPhotosAlbum(outputURL.path, nil, nil, nil) }) // Clean up audio audioEngine?.stop() do { try AVAudioSession.sharedInstance().setActive(false) } catch { print("Failed to deactivate audio session: \(error.localizedDescription)") } }
- Permissions: Don't forget to add
NSMicrophoneUsageDescriptionto yourInfo.plistto request microphone access. - Performance: For smoother recording, replace
scnView.snapshot()with direct rendering to a pixel buffer usingSCNSceneRenderer.render(to:viewport:commandQueue:pass:)—this avoids UIImage conversion overhead. - Dynamic Layouts: If your SCNView resizes, update the crop rect calculation in
viewDidLayoutSubviewsto keep the middle region accurate. - Sync: The time stamps used for video/audio are aligned to keep playback in sync, but test on your target device to adjust if needed.
内容的提问来源于stack exchange,提问作者Joker

