如何从ARKit录制视频?

15

现在我正在测试 ARKit/SceneKit 的实现。基本的屏幕渲染已经有了,接下来我想尝试将屏幕上看到的内容记录为视频。

关于 Scene Kit 的录制,我找到了 这个代码片段

//
//  ViewController.swift
//  SceneKitToVideo
//
//  Created by Lacy Rhoades on 11/29/16.
//  Copyright © 2016 Lacy Rhoades. All rights reserved.
//

import SceneKit
import GPUImage
import Photos

class ViewController: UIViewController {

    // Renders a scene (and shows it on the screen)
    var scnView: SCNView!

    // Another renderer
    var secondaryRenderer: SCNRenderer?

    // Abducts image data via an OpenGL texture
    var textureInput: GPUImageTextureInput?

    // Recieves image data from textureInput, shows it on screen
    var gpuImageView: GPUImageView!

    // Recieves image data from the textureInput, writes to a file
    var movieWriter: GPUImageMovieWriter?

    // Where to write the output file
    let path = NSTemporaryDirectory().appending("tmp.mp4")

    // Output file dimensions
    let videoSize = CGSize(width: 800.0, height: 600.0)

    // EAGLContext in the sharegroup with GPUImage
    var eaglContext: EAGLContext!

    override func viewDidLoad() {
        super.viewDidLoad()

        let group = GPUImageContext.sharedImageProcessing().context.sharegroup
        self.eaglContext = EAGLContext(api: .openGLES2, sharegroup: group )
        let options = ["preferredRenderingAPI": SCNRenderingAPI.openGLES2]

        // Main view with 3d in it
        self.scnView = SCNView(frame: CGRect.zero, options: options)
        self.scnView.preferredFramesPerSecond = 60
        self.scnView.eaglContext = eaglContext
        self.scnView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(self.scnView)

        // Secondary renderer for rendering to an OpenGL framebuffer
        self.secondaryRenderer = SCNRenderer(context: eaglContext, options: options)

        // Output of the GPUImage pipeline
        self.gpuImageView = GPUImageView()
        self.gpuImageView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(self.gpuImageView)

        self.setupConstraints()

        self.setupScene()

        self.setupMovieWriter()

        DispatchQueue.main.async {
            self.setupOpenGL()
        }
    }

    func setupConstraints() {
        let relativeWidth: CGFloat = 0.8

        self.view.addConstraint(NSLayoutConstraint(item: self.scnView, attribute: .width, relatedBy: .equal, toItem: self.view, attribute: .width, multiplier: relativeWidth, constant: 0))
        self.view.addConstraint(NSLayoutConstraint(item: self.scnView, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0))

        self.view.addConstraint(NSLayoutConstraint(item: self.gpuImageView, attribute: .width, relatedBy: .equal, toItem: self.view, attribute: .width, multiplier: relativeWidth, constant: 0))
        self.view.addConstraint(NSLayoutConstraint(item: self.gpuImageView, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0))

        self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-(==30.0)-[scnView]-(==30.0)-[gpuImageView]", options: [], metrics: [:], views: ["gpuImageView": gpuImageView, "scnView": scnView]))

        let videoRatio = self.videoSize.width / self.videoSize.height
        self.view.addConstraint(NSLayoutConstraint(item: self.scnView, attribute: .width, relatedBy: .equal, toItem: self.scnView, attribute: .height, multiplier: videoRatio, constant: 1))
        self.view.addConstraint(NSLayoutConstraint(item: self.gpuImageView, attribute: .width, relatedBy: .equal, toItem: self.gpuImageView, attribute: .height, multiplier: videoRatio, constant: 1))
    }

    override func viewDidAppear(_ animated: Bool) {
        self.cameraBoxNode.runAction(
            SCNAction.repeatForever(
                SCNAction.rotateBy(x: 0.0, y: -2 * CGFloat.pi, z: 0.0, duration: 8.0)
            )
        )

        self.scnView.isPlaying = true

        Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: {
            timer in
            self.startRecording()
        })
    }

    var scene: SCNScene!
    var geometryNode: SCNNode!
    var cameraNode: SCNNode!
    var cameraBoxNode: SCNNode!
    var imageMaterial: SCNMaterial!
    func setupScene() {
        self.imageMaterial = SCNMaterial()
        self.imageMaterial.isDoubleSided = true
        self.imageMaterial.diffuse.contentsTransform = SCNMatrix4MakeScale(-1, 1, 1)
        self.imageMaterial.diffuse.wrapS = .repeat
        self.imageMaterial.diffuse.contents = UIImage(named: "pano_01")

        self.scene = SCNScene()

        let sphere = SCNSphere(radius: 100.0)
        sphere.materials = [imageMaterial!]
        self.geometryNode = SCNNode(geometry: sphere)
        self.geometryNode.position = SCNVector3Make(0.0, 0.0, 0.0)
        scene.rootNode.addChildNode(self.geometryNode)

        self.cameraNode = SCNNode()
        self.cameraNode.camera = SCNCamera()
        self.cameraNode.camera?.yFov = 72.0
        self.cameraNode.position = SCNVector3Make(0, 0, 0)
        self.cameraNode.eulerAngles = SCNVector3Make(0.0, 0.0, 0.0)

        self.cameraBoxNode = SCNNode()
        self.cameraBoxNode.addChildNode(self.cameraNode)
        scene.rootNode.addChildNode(self.cameraBoxNode)

        self.scnView.scene = scene
        self.scnView.backgroundColor = UIColor.darkGray
        self.scnView.autoenablesDefaultLighting = true
    }

    func setupMovieWriter() {
        let _ = FileUtil.mkdirUsingFile(path)
        let _ = FileUtil.unlink(path)
        let url = URL(fileURLWithPath: path)
        self.movieWriter = GPUImageMovieWriter(movieURL: url, size: self.videoSize)
    }

    let glRenderQueue = GPUImageContext.sharedContextQueue()!
    var outputTexture: GLuint = 0
    var outputFramebuffer: GLuint = 0
    func setupOpenGL() {
        self.glRenderQueue.sync {
            let context = EAGLContext.current()
            if context != self.eaglContext {
                EAGLContext.setCurrent(self.eaglContext)
            }

            glGenFramebuffers(1, &self.outputFramebuffer)
            glBindFramebuffer(GLenum(GL_FRAMEBUFFER), self.outputFramebuffer)

            glGenTextures(1, &self.outputTexture)
            glBindTexture(GLenum(GL_TEXTURE_2D), self.outputTexture)
        }

        // Pipe the texture into GPUImage-land
        self.textureInput = GPUImageTextureInput(texture: self.outputTexture, size: self.videoSize)

        let rotate = GPUImageFilter()
        rotate.setInputRotation(kGPUImageFlipVertical, at: 0)
        self.textureInput?.addTarget(rotate)
        rotate.addTarget(self.gpuImageView)

        if let writer = self.movieWriter {
            rotate.addTarget(writer)
        }

        // Call me back on every render
        self.scnView.delegate = self
    }

    func renderToFramebuffer(atTime time: TimeInterval) {
        self.glRenderQueue.sync {
            let context = EAGLContext.current()
            if context != self.eaglContext {
                EAGLContext.setCurrent(self.eaglContext)
            }

            objc_sync_enter(self.eaglContext)

            let width = GLsizei(self.videoSize.width)
            let height = GLsizei(self.videoSize.height)

            glBindFramebuffer(GLenum(GL_FRAMEBUFFER), self.outputFramebuffer)
            glBindTexture(GLenum(GL_TEXTURE_2D), self.outputTexture)

            glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, width, height, 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), nil)

            glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
            glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
            glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
            glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)

            glFramebufferTexture2D(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_TEXTURE_2D), self.outputTexture, 0)

            glViewport(0, 0, width, height)

            glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))

            self.secondaryRenderer?.render(atTime: time)

            self.videoBuildingQueue.sync {
                self.textureInput?.processTexture(withFrameTime: CMTime(seconds: time, preferredTimescale: 100000))
            }

            objc_sync_exit(self.eaglContext)
        }

    }

    func startRecording() {
        self.startRecord()
        Timer.scheduledTimer(withTimeInterval: 24.0, repeats: false, block: {
            timer in
            self.stopRecord()
        })
    }

    let videoBuildingQueue = DispatchQueue.global(qos: .default)

    func startRecord() {
        self.videoBuildingQueue.sync {
            //inOrientation: CGAffineTransform(scaleX: 1.0, y: -1.0)
            self.movieWriter?.startRecording()
        }
    }

    var renderStartTime: TimeInterval = 0

    func stopRecord() {
        self.videoBuildingQueue.sync {
            self.movieWriter?.finishRecording(completionHandler: {
                self.saveFileToCameraRoll()
            })
        }
    }

    func saveFileToCameraRoll() {
        assert(FileUtil.fileExists(self.path), "Check for file output")

        DispatchQueue.global(qos: .utility).async {
            PHPhotoLibrary.shared().performChanges({
                PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: self.path))
            }) { (done, err) in
                if err != nil {
                    print("Error creating video file in library")
                    print(err.debugDescription)
                } else {
                    print("Done writing asset to the user's photo library")
                }
            }
        }
    }

}

extension ViewController: SCNSceneRendererDelegate {
    func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
        self.secondaryRenderer?.scene = scene
        self.secondaryRenderer?.pointOfView = (renderer as! SCNView).pointOfView
        self.renderToFramebuffer(atTime: time)
    }
}

但是这并不能呈现设备相机中的图像。

因此,我开始寻找方法来实现它。 到目前为止,我找到了通过访问ARFrame将捕获的图像作为CVImageBufferRef获取的方法。 而苹果的GLCameraRipple示例似乎可以帮助我从中获取OpenGL纹理。

但我的问题是如何在渲染循环中绘制它。 这可能对于有OpenGL经验的人来说很明显,但是我只有非常少关于OpenGL的知识,所以我无法弄清楚如何将其添加到上面的代码中。


有点相关,但是你可以在iOS 11上本地录制屏幕。从控制中心中访问该选项。你可以从设置/控制中心中添加快捷方式。 - Tomislav Markovski
谢谢提供这些信息。但是我需要控制录音行为,而“控制中心”并没有给我们这种控制权。 - Takeshi Yokemura
6个回答

5
您可以使用ReplayKit、ARKit和SceneKit内容来记录屏幕上看到的所有内容(或将其实时流式传输到像Twitch这样的服务中)。 (正如苹果在WWDC上指出的那样,ReplayKit实际上是iOS 11控制中心屏幕录制功能的基础。)

我再次考虑了这个想法,发现抓取屏幕上的所有内容并不是一个好主意,因为它包括其他 UI 元素,如录制按钮和进度条,这些元素对于用户体验仍然很重要。此外,如果将来我想让它非全屏显示,那么更改将会非常困难。因此,我仍然希望采用直接的编程方式进行渲染。 - Takeshi Yokemura
@TakeshiYokemura 我发现ScreenRecord是一个不错的解决方案,而且这篇文章也很值得一读。就像在那些参考作品中实现的那样,您可以尝试将您的录制按钮和进度条放在一个单独的UIWindow中,因为那个窗口不会被记录。 - Tulio Troncoso
谢谢你的评论。你尝试过与ARKit结合使用吗?看起来很有可能会出现我上面提到的同样错误。如果你不确定,我可以试一下。 - Takeshi Yokemura
1
我还发现,似乎可以通过将特定的UI元素放在单独的UIWindow中来避免将它们包含在录制中。https://code.tutsplus.com/tutorials/ios-9-an-introduction-to-replaykit--cms-25458 - Takeshi Yokemura
2
我不喜欢使用ReplayKit的想法 - 每次用户都会被呈现出一个丑陋的权限对话框来录制屏幕。将CVPixelBuffer编码为MP4容器的方法将是一个更好的解决方案(...我仍在寻找一个好的例子)。 - JCutting8
显示剩余7条评论

2

Swift 5

您可以使用此ARCapture框架记录来自ARKit视图的视频。

private var capture: ARCapture?
...

override func viewDidLoad() {
    super.viewDidLoad()

    // Create a new scene
    let scene = SCNScene()
    ...
    // TODO Setup ARSCNView with the scene
    // sceneView.scene = scene
    
    // Setup ARCapture
    capture = ARCapture(view: sceneView)

}

/// "Record" button action handler
@IBAction func recordAction(_ sender: UIButton) {
    capture?.start()
}

/// "Stop" button action handler
@IBAction func stopAction(_ sender: UIButton) {
    capture?.stop({ (status) in
        print("Video exported: \(status)")
    })
}

当您调用 ARCapture.stop 后,视频将会在“照片”应用程序中呈现。


有没有自动查找已保存视频的URL的方法? - Saul Aryeh Kohn
1
很遗憾,目前还没有,但您可以发布一个问题。也许有一天会更新。 - Alexander Volkov

1

不知道你现在是否已经回答了这个问题,但是编写你引用的类的lacyrhoades发布了另一个项目在github上,似乎可以做到你所要求的。我已经使用过它,它可以记录具有AR对象的SceneView以及相机输入。你可以通过此链接找到它:

https://github.com/lacyrhoades/SCNKit2Video

如果您想要与AR一起使用它,您需要将ARSceneView配置到他制作的项目中,因为他的项目只运行一个SceneView,而不是带有AR的SceneView。
希望这可以帮助您。

我尝试了ghitub项目,并成功地记录了一个静态物体的视频。你是否像你说的那样“配置ARSceneView”了呢? - Marie Dm
1
@MarieDm,请查看我下面的答案。希望它能帮助你完成你的神奇AR项目 :) - Ömer Karışman
谢谢@ÖmerKarışman。我从启发你的项目中找到了一个解决方案。尽管如此,我还是试了一下。我可能配置不正确,但是没有声音,并且图像存在问题,这真的很奇怪。 - Marie Dm
如果您能打开一个问题并发送一些详细信息,我会很高兴。 - Ömer Karışman
1
@ÖmerKarışman,我刚刚在已打开的问题#3上进行了操作https://github.com/svtek/SceneKitVideoRecorder/issues/3 - Marie Dm
这里有一个关于如何实现它的教程 - 代码是开源的:https://medium.com/ar-tips-and-tricks/arkit-how-to-make-your-own-audio-video-recorder-3d72016d1087?fbclid=IwAR2SiKIRxt3np5Wnn-Vi90XheEsGksSO5j0kj-N9emmQoFIoL80aqRGxndY - odlh

1
我刚发现了一个叫做ARVideoKit的框架,它似乎很容易实现,并且有更多功能,例如捕捉GIF和Live Photos。
该框架的官方库是:https://github.com/AFathi/ARVideoKit/ 要安装它,您需要克隆该库并将.framework文件拖入您项目的嵌入式二进制文件中。
然后实现非常简单:
  1. 在你的UIViewController类中导入ARVideoKit

  2. 创建一个RecordAR?变量

    var videoRec:RecordAR?

  3. viewDidLoad中初始化你的变量

    videoRec = RecordAR(ARSpriteKit:sceneView)

  4. viewWillAppear中准备RecordAR

    videoRec.prepare(configuration)

  5. 开始录制视频

    videoRec.record()

  6. 停止并导出到相机胶卷!

    videoRec.stopAndExport()

查看框架文档,它支持更多功能的使用!

你可以在这里找到他们的文档:https://github.com/AFathi/ARVideoKit/wiki

希望有所帮助!


1
请考虑扩展您的答案,包括框架如何解决提问者的问题。不鼓励仅提供链接的答案。 - Tom Aranda
1
很遗憾,ARVideoKit不支持RealityKit(即ARKit3身体跟踪等)。 - JCutting8

1
ReplayKit不是一个好的解决方案,因为用户会看到一个丑陋的权限对话框,而且你还需要绕过它来记录UI元素。此外,你对视频分辨率的控制也更少。相反,你应该使用ARKit返回的捕获帧CVPixelBuffer,并像处理从相机捕获的帧一样处理它。假设你需要处理视频帧,你可能还需要使用像Metal这样的框架来处理绘图。这并不简单。请参见这里提供的答案:如何在RealityKit中录制视频?

-6
如果您能够将设备连接到Mac,那么使用QuickTime Player从iOS设备录制屏幕(和声音)非常容易。
在QuickTime中,在文件菜单中选择新的电影录制,然后在记录对话框中,接近大红色记录按钮的下拉箭头中,您可以选择音频输入和视频输入。在那里选择您的i设备,然后就可以开始了。

1
感谢您的评论。我们的目的不仅仅是为了让我自己录制视频,而是提供一种在我的应用程序中轻松录制并让用户使用的方式。也许只需告诉用户“嘿,点击iOS屏幕录制按钮!”这可能是一种方式,但这不是我们想要向用户提供的体验。理想的行为方式是在我的应用程序中拥有一个录制按钮,通过点击它来开始记录AR场景。 - Takeshi Yokemura

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接