【问题标题】:Take a Video with ARKIT使用 ARKIT 拍摄视频
【发布时间】:2017-07-26 11:56:37
【问题描述】:

你好社区

我尝试使用 Swift 4 和即将推出的伟大的 ARKit-Framework 构建一个应用程序,但我被卡住了。我需要使用框架或至少是 UIImage 序列拍摄视频,但我不知道如何。

这是我尝试过的

在 ARKit 中,您有一个跟踪您的世界的会话。此会话有一个 capturedImage 实例,您可以在其中获取当前图像。所以我创建了一个计时器,它每 0.1 秒将 capturedImage 附加到一个列表中。这对我有用,但如果我通过单击“开始”按钮启动计时器,相机开始滞后。我猜这与定时器无关,因为如果我通过单击“停止”按钮使定时器无效,相机就会再次流畅。

有没有办法解决滞后甚至更好的方法?

谢谢

【问题讨论】:

标签: swift list video-capture arkit


【解决方案1】:

我可以使用ReplayKit 来做到这一点。

看看 ReplayKit 是什么样的

在您的 iOS 设备上,转到设置 -> 控制中心 -> 自定义控件。将“屏幕录制”移动到“包含”部分,然后向上滑动以调出控制中心。您现在应该看到圆形屏幕录制图标,并且您会注意到当您按下它时,iOS 开始录制您的屏幕。点击蓝色条将结束录制并将视频保存到照片。

使用 ReplayKit,您可以让您的应用调用屏幕录像机并捕获您的 ARKit 内容。

操作方法

开始录制:

RPScreenRecorder.shared().startRecording { error in
    // Handle error, if any
}

要停止录制:

RPScreenRecorder.shared().stopRecording(handler: { (previewVc, error) in
    // Do things
})

录制完成后,.stopRecording 会为您提供可选的RPPreviewViewController,即

一个显示用户界面的对象,用户可以在其中预览和编辑使用 ReplayKit 创建的屏幕录像。

所以在我们的示例中,如果 previewVc 不为 nil,则可以显示它

RPScreenRecorder.shared().stopRecording(handler: { (previewVc, error) in
    if let previewVc = previewVc {
        previewVc.delegate = self
        self.present(previewVc, animated: true, completion: nil)
    }
})

您可以直接从 previewVc 编辑和保存视频,但您可能想将自己(或某人)设为 RPPreviewViewControllerDelegate,这样您就可以在完成后轻松关闭 previewVc。

extension MyViewController: RPPreviewViewControllerDelegate {
    func previewControllerDidFinish(_ previewController: RPPreviewViewController) {
        // Called when the preview vc is ready to be dismissed
    }
}

注意事项

您会注意到startRecording 将记录“应用程序显示”,因此如果您拥有的任何视图(按钮、标签等)也将被记录。 我发现在录制时隐藏控件并让我的用户知道点击屏幕会停止录制很有用,但我也读到其他人成功地将他们的基本控件放在单独的 UIWindow 上。

从记录中排除视图

单独的 UIWindow 技巧有效。我能够制作一个覆盖窗口,其中有一个记录按钮和一个计时器,而这些都没有被记录下来。

let overlayWindow = UIWindow(frame: view.frame)
let recordButton = UIButton( ... )
overlayWindow.backgroundColor = UIColor.clear

UIWindow 默认是隐藏的。所以当你想显示你的控件时,你必须将isHidden设置为false

祝你好运!

【讨论】:

    【解决方案2】:

    使用自定义渲染器。

    使用自定义渲染器渲染场景,然后从自定义渲染器获取纹理,最后将其转换为CVPixelBufferRef

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.rgbColorSpace = CGColorSpaceCreateDeviceRGB();
        self.bytesPerPixel = 4;
        self.bitsPerComponent = 8;
        self.bitsPerPixel = 32;
        self.textureSizeX = 640;
        self.textureSizeY = 960;
    
        // Set the view's delegate
        self.sceneView.delegate = self;
    
        // Show statistics such as fps and timing information
        self.sceneView.showsStatistics = YES;
    
        // Create a new scene
        SCNScene *scene = [SCNScene scene];//[SCNScene sceneNamed:@"art.scnassets/ship.scn"];
    
        // Set the scene to the view
        self.sceneView.scene = scene;
    
        self.sceneView.preferredFramesPerSecond = 30;
    
        [self setupMetal];
        [self setupTexture];
        self.renderer.scene = self.sceneView.scene;
    
    }
    
    - (void)setupMetal
    {
        if (self.sceneView.renderingAPI == SCNRenderingAPIMetal) {
            self.device = self.sceneView.device;
            self.commandQueue = [self.device newCommandQueue];
            self.renderer = [SCNRenderer rendererWithDevice:self.device options:nil];
        }
        else {
            NSAssert(nil, @"Only Support Metal");
        }
    }
    
    - (void)setupTexture
    {
        MTLTextureDescriptor *descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm_sRGB width:self.textureSizeX height:self.textureSizeY mipmapped:NO];
        descriptor.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget;
    
        id<MTLTexture> textureA = [self.device newTextureWithDescriptor:descriptor];
        self.offscreenTexture = textureA;
    }
    
    - (void)renderer:(id <SCNSceneRenderer>)renderer willRenderScene:(SCNScene *)scene atTime:(NSTimeInterval)time
    {
        [self doRender];
    }
    
    - (void)doRender
    {
        if (self.rendering) {
            return;
        }
        self.rendering = YES;
        CGRect viewport = CGRectMake(0, 0, self.textureSizeX, self.textureSizeY);
    
        id<MTLTexture> texture = self.offscreenTexture;
    
        MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor new];
        renderPassDescriptor.colorAttachments[0].texture = texture;
        renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
        renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 1, 0, 1.0);
        renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
    
        id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
    
        self.renderer.pointOfView = self.sceneView.pointOfView;
    
        [self.renderer renderAtTime:0 viewport:viewport commandBuffer:commandBuffer passDescriptor:renderPassDescriptor];
    
        [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull bf) {
            [self.recorder writeFrameForTexture:texture];
            self.rendering = NO;
        }];
    
        [commandBuffer commit];
    }
    

    然后在记录器中,将AVAssetWriterInputPixelBufferAdaptor 设置为AVAssetWriter。并将纹理转换为CVPixelBufferRef

    - (void)writeFrameForTexture:(id<MTLTexture>)texture {
        CVPixelBufferPoolRef pixelBufferPool = self.assetWriterPixelBufferInput.pixelBufferPool;
        CVPixelBufferRef pixelBuffer;
        CVReturn status = CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &pixelBuffer);
        CVPixelBufferLockBaseAddress(pixelBuffer, 0);
        void *pixelBufferBytes = CVPixelBufferGetBaseAddress(pixelBuffer);
        size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
        MTLRegion region = MTLRegionMake2D(0, 0, texture.width, texture.height);
        [texture getBytes:pixelBufferBytes bytesPerRow:bytesPerRow fromRegion:region mipmapLevel:0];
    
        [self.assetWriterPixelBufferInput appendPixelBuffer:pixelBuffer withPresentationTime:presentationTime];
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
        CVPixelBufferRelease(pixelBuffer);
    }
    

    确保自定义渲染器和适配器共享相同的像素编码。

    我测试了默认的 ship.scn,它只消耗 30% 的 CPU,而与每帧使用 snapshot 方法相比,它几乎消耗了 90%。这不会弹出权限对话框。

    【讨论】:

      【解决方案3】:

      我已经发布了一个开源框架来解决这个问题。 https://github.com/svtek/SceneKitVideoRecorder

      它通过从场景视图金属层获取可绘制对象来工作。

      您可以附加一个显示链接以在屏幕刷新时调用您的渲染器:

      displayLink = CADisplayLink(target: self, selector: #selector(updateDisplayLink))
      displayLink?.add(to: .main, forMode: .commonModes)
      

      然后通过以下方式从金属层抓取可绘制对象:

      let metalLayer = sceneView.layer as! CAMetalLayer
      let nextDrawable = metalLayer.nextDrawable()
      

      注意nextDrawable() 调用会消耗可绘制对象。您应该尽可能少地调用它,并在 autoreleasepool{} 中这样做,以便可绘制对象被正确释放并替换为新对象。

      然后您应该将 MTLTexture 从可绘制对象读取到像素缓冲区,您可以将其附加到 AVAssetWriter 以创建视频。

      let destinationTexture = currentDrawable.texture
      destinationTexture.getBytes(...)
      

      考虑到这些,剩下的就是在 iOS/Cocoa 上进行非常简单的视频录制了。

      你可以在我上面分享的 repo 中找到所有这些实现。

      【讨论】:

      • Ömer,SceneKitVideoRenderer 看起来很酷,但是物理和动画以双倍速度运行,大概是由于渲染每一帧两次 - 一次用于显示,一次用于记录。是对的吗?您(或任何人)是否知道任何解决方法?
      • 关于这个有一个未解决的问题。我们仍然要提出一个好的解决方案。当您说动画时,您是指 SCNAnimations 吗?因为模型动画看起来很正常。
      【解决方案4】:

      我也有类似的需求,想在内部录制应用程序中的 ARSceneView,并且不使用 ReplayKit,以便我可以操作从录制中生成的视频。我最终使用了这个项目:https://github.com/lacyrhoades/SceneKit2Video。该项目用于将 SceneView 渲染到视频,但您可以将其配置为接受 ARSceneViews。它工作得很好,如果您愿意,您可以选择使用委托功能获取图像馈送而不是视频。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-12-03
        • 1970-01-01
        • 2019-04-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多