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

快速录制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:

1. Setup Core Recording Objects

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)
}
2. Initialize the Asset Writer for Custom Recording

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
}
3. Capture & Crop SCNView Frames via Renderer Delegate

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
}
4. Sync Audio Recording

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)")
    }
}
5. Control Recording Start/Stop

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)")
    }
}
Key Notes for Success
  • Permissions: Don't forget to add NSMicrophoneUsageDescription to your Info.plist to request microphone access.
  • Performance: For smoother recording, replace scnView.snapshot() with direct rendering to a pixel buffer using SCNSceneRenderer.render(to:viewport:commandQueue:pass:)—this avoids UIImage conversion overhead.
  • Dynamic Layouts: If your SCNView resizes, update the crop rect calculation in viewDidLayoutSubviews to 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 04:04:24