如何在ARView(Reality Kit)中仅录制ARView区域视频?
仅录制ARView区域的解决方案
RPScreenRecorder本身仅支持全屏录制,无法直接指定区域。要实现只录制ARView的内容,可以采用以下两种方案:
方案1:直接捕获ARView渲染帧录制(推荐)
这种方案跳过ReplayKit,直接从ARView的Metal渲染管线获取每一帧数据,写入视频文件,性能更优且精准控制录制区域。
实现步骤
- 初始化
AVAssetWriter和AVAssetWriterInputPixelBufferAdaptor,用于视频数据写入 - 通过
CADisplayLink定时捕获ARView的当前渲染帧 - 将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
相关产品推荐
相关产品推荐

