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

如何在ARView(Reality Kit)中仅录制ARView区域视频?

仅录制ARView区域的解决方案

RPScreenRecorder本身仅支持全屏录制,无法直接指定区域。要实现只录制ARView的内容,可以采用以下两种方案:

方案1:直接捕获ARView渲染帧录制(推荐)

这种方案跳过ReplayKit,直接从ARView的Metal渲染管线获取每一帧数据,写入视频文件,性能更优且精准控制录制区域。

实现步骤

  1. 初始化AVAssetWriter和AVAssetWriterInputPixelBufferAdaptor,用于视频数据写入
  2. 通过CADisplayLink定时捕获ARView的当前渲染帧
  3. 将Metal纹理转换为CVPixelBuffer,再写入视频文件

代码示例

import ARKit
import AVFoundation
import QuartzCore

class ARViewRecorder: NSObject {
    private var assetWriter: AVAssetWriter?
    private var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor?
    private var displayLink: CADisplayLink?
    private let processingQueue = DispatchQueue(label: "com.yourapp.arview.recorder")
    
    // 开始录制ARView
    func startRecording(_ arView: ARView, outputURL: URL) throws {
        // 初始化AVAssetWriter
        assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: .mov)
        
        // 配置视频输入参数,匹配ARView尺寸
        let videoSettings: [String: Any] = [
            AVVideoCodecKey: AVVideoCodecType.h264,
            AVVideoWidthKey: arView.bounds.width,
            AVVideoHeightKey: arView.bounds.height
        ]
        let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
        videoInput.expectsMediaDataInRealTime = true
        
        // 创建PixelBuffer适配器
        let pixelBufferAttrs: [String: Any] = [
            kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
            kCVPixelBufferWidthKey as String: arView.bounds.width,
            kCVPixelBufferHeightKey as String: arView.bounds.height
        ]
        pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoInput, sourcePixelBufferAttributes: pixelBufferAttrs)
        
        if assetWriter?.canAdd(videoInput) == true {
            assetWriter?.add(videoInput)
        }
        
        assetWriter?.startWriting()
        assetWriter?.startSession(atSourceTime: .zero)
        
        // 启动DisplayLink捕获帧
        displayLink = CADisplayLink(target: self, selector: #selector(captureARViewFrame))
        displayLink?.add(to: .main, forMode: .common)
    }
    
    // 捕获ARView帧
    @objc private func captureARViewFrame() {
        guard let arView = ARView.current,
              let adaptor = pixelBufferAdaptor,
              let input = adaptor.assetWriterInput,
              input.isReadyForMoreMediaData else { return }
        
        let timestamp = CMTimeMakeWithSeconds(CACurrentMediaTime(), preferredTimescale: 600)
        
        // 获取当前Metal纹理并转为PixelBuffer
        guard let drawable = arView.currentDrawable,
              let pixelBuffer = drawable.texture.toCVPixelBuffer() else { return }
        
        adaptor.append(pixelBuffer, withPresentationTime: timestamp)
    }
    
    // 停止录制
    func stopRecording(completion: @escaping () -> Void) {
        displayLink?.invalidate()
        displayLink = nil
        
        assetWriter?.finishWriting { [weak self] in
            self?.assetWriter = nil
            self?.pixelBufferAdaptor = nil
            completion()
        }
    }
}

// Metal纹理转CVPixelBuffer扩展
extension MTLTexture {
    func toCVPixelBuffer() -> CVPixelBuffer? {
        let attrs: [String: Any] = [
            kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
            kCVPixelBufferWidthKey as String: width,
            kCVPixelBufferHeightKey as String: height,
            kCVPixelBufferIOSurfacePropertiesKey as String: [:]
        ]
        
        var pixelBuffer: CVPixelBuffer?
        let status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, attrs as CFDictionary, &pixelBuffer)
        
        guard status == kCVReturnSuccess, let buffer = pixelBuffer else { return nil }
        
        CVPixelBufferLockBaseAddress(buffer, .readWrite)
        defer { CVPixelBufferUnlockBaseAddress(buffer, .readWrite) }
        
        guard let baseAddr = CVPixelBufferGetBaseAddress(buffer) else { return nil }
        let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
        let region = MTLRegionMake2D(0, 0, width, height)
        
        getBytes(baseAddr, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0)
        return buffer
    }
}

方案2:录制全屏后裁剪(不推荐,适合非实时场景)

如果依赖ReplayKit的其他特性,可以先录制全屏视频,再通过AVFoundation裁剪出ARView对应的区域。

代码示例

import ReplayKit
import AVFoundation

func cropFullScreenVideo(fullScreenURL: URL, arViewFrame: CGRect, outputURL: URL) {
    let asset = AVAsset(url: fullScreenURL)
    let composition = AVMutableComposition()
    
    guard let videoTrack = asset.tracks(withMediaType: .video).first else { return }
    let compTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!
    
    do {
        try compTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration), of: videoTrack, at: .zero)
    } catch {
        print("插入轨道失败: \(error)")
        return
    }
    
    // 计算裁剪区域(适配视频分辨率)
    let videoSize = videoTrack.naturalSize
    let scaleX = videoSize.width / UIScreen.main.bounds.width
    let scaleY = videoSize.height / UIScreen.main.bounds.height
    let cropRect = CGRect(
        x: arViewFrame.origin.x * scaleX,
        y: arViewFrame.origin.y * scaleY,
        width: arViewFrame.width * scaleX,
        height: arViewFrame.height * scaleY
    )
    
    // 配置裁剪指令
    let instruction = AVMutableVideoCompositionInstruction()
    instruction.timeRange = CMTimeRangeMake(start: .zero, duration: asset.duration)
    
    let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compTrack)
    layerInstruction.setCropRectangleRamp(fromStartCropRectangle: cropRect, toEndCropRectangle: cropRect, timeRange: instruction.timeRange)
    layerInstruction.setTransform(videoTrack.preferredTransform, at: .zero)
    instruction.layerInstructions = [layerInstruction]
    
    let videoComp = AVMutableVideoComposition()
    videoComp.instructions = [instruction]
    videoComp.frameDuration = videoTrack.minFrameDuration
    videoComp.renderSize = cropRect.size
    
    // 导出裁剪后的视频
    let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)!
    exporter.outputURL = outputURL
    exporter.outputFileType = .mov
    exporter.videoComposition = videoComp
    
    exporter.exportAsynchronously {
        switch exporter.status {
        case .completed:
            print("裁剪完成")
        case .failed:
            print("裁剪失败: \(exporter.error?.localizedDescription ?? "未知错误")")
        default: break
        }
    }
}

注意事项

  • 方案1需要确保ARView的Metal纹理格式与CVPixelBuffer兼容,部分设备可能需要调整像素格式
  • 录制前需申请相机权限(ARKit必需)和相册访问权限(保存视频)
  • 方案2存在二次编码损耗,实时录制场景优先选方案1

内容的提问来源于stack exchange,提问作者Deepak Singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 18:01:08