Как записать видео с ARKit?

Сейчас я тестирую реализацию ARKit/SceneKit. Основной рендеринг на экране вроде работает, поэтому я хочу попробовать записать то, что я вижу на экране, в видео.

Просто для записи Scene Kit я нашел этот Gist:

//
//  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)
    }
}

но это не делает изображение с камеры устройства.

Так что я тоже начал искать способ сделать это. До сих пор я нашел способ захватить захваченное изображение как CVImageBufferRef путем доступа к ARFrame. И пример Apple GLCameraRipple, похоже, помогает мне извлечь из него текстуру OpenGL.

Но мой вопрос, как нарисовать его в цикле рендеринга. Это может быть очевидно для опытных OpenGL, но у меня очень мало знаний об OpenGL, поэтому я не могу понять, как добавить это в приведенный выше код.

7 ответов

Вы можете записывать все, что видно на экране (или транслировать его в прямом эфире на такие сервисы, как Twitch) с использованием содержимого ReplayKit, ARKit и SceneKit.

(Как указала Apple на WWDC, ReplayKit фактически является основой для функции записи экрана Центра управления в iOS 11).

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 видео будет представлено в приложении «Фото».

ReplayKit не является хорошим решением, потому что вашим пользователям предоставляется уродливое диалоговое окно с разрешениями, и вам также нужно обойти его, записывая элементы пользовательского интерфейса. У вас также меньше возможностей контролировать разрешение видео.

Вместо этого вы должны использовать захваченный кадр CVPixelBufferвозвращенный ARKit, и обрабатывайте его, как если бы вы записывали кадры, снятые с камеры. Предполагая, что вам нужно обработать кадры видео, вам также может потребоваться использовать фреймворк, например Metal, для обработки чертежа. Это не просто. См. Ответ здесь: Как записать видео в RealityKit?

Не знаю, удалось ли вам ответить на этот вопрос сейчас или нет, но lacyrhoades, человек, который написал класс, на который вы ссылались, выпустил еще один проект на github, который, кажется, выполняет то, что вы просите. Я использовал его, и ему удается записать SceneView с объектами AR, а также с камеры. Вы можете найти его по этой ссылке:

https://github.com/lacyrhoades/SCNKit2Video

Если вы хотите использовать его с AR, вам нужно настроить ARSceneView для проекта, который он делает, так как его один просто запускает SceneView, а не один с AR.

Надеюсь, поможет.

Я только что нашел этот фреймворк, называемый ARVideoKit, и, кажется, его легко реализовать, плюс у него есть больше функций, таких как захват GIF-файлов и Live Photos.

Официальный репозиторий фреймворка: https://github.com/AFathi/ARVideoKit/

Чтобы установить его, вам нужно будет клонировать репозиторий и перетащить файл.framework во встроенный бинарный файл вашего проекта.

Тогда реализация довольно проста:

  1. import ARVideoKit в вашем UIViewController учебный класс

  2. Создать RecordAR? переменная

    var videoRec:RecordAR?

  3. Инициализируйте вашу переменную в viewDidLoad

    videoRec = RecordAR(ARSpriteKit:sceneView)

  4. Подготовить RecordAR в viewWillAppear

    videoRec.prepare(configuration)

  5. Начните запись видео

    videoRec.record()

  6. Остановись и экспортируй в фотопленку!

    videoRec.stopAndExport()

Взгляните на документацию фреймворка, он поддерживает больше возможностей для использования!

Вы можете найти их документацию здесь: https://github.com/AFathi/ARVideoKit/wiki

Надеюсь, что это помогло!

Если вы можете сохранить ваше устройство подключенным к вашему Mac, очень просто использовать QuickTime Player для записи экрана (и звука) с вашего устройства iOS.

В QuickTime выберите новую запись фильма в меню "Файл", затем в диалоговом окне записи рядом с большой красной кнопкой записи есть маленькая выпадающая стрелка, где вы можете выбрать аудиовход и видеовход. Выберите ваше i-устройство там, и вы готовы к работе.

Если вы используете ARKit, то вы используете iOS 11.

iOS 11 имеет встроенную запись экрана (с поддержкой микрофона).

Тем не менее, в iOS 11 Beta 2 он немного глючит - но работает.

Чтобы подвести итог:

  • В меню "Настройки", "Центр управления", "Настройка" добавьте виджет " Запись экрана".
  • В Центре управления (дважды нажмите кнопку "Домой" или сдвиньте ее вверх от нижней части экрана), нажмите на кнопку " Запись экрана" принудительное нажатие (или длительное нажатие), чтобы настроить, хотите ли вы запись с микрофона
  • Нажмите кнопку записи экрана, чтобы начать запись
  • Запустите ваше приложение
  • Вернитесь в Центр управления и нажмите Запись экрана, чтобы остановить запись.
  • Видео есть в вашей фотопленке.

Глючная часть в том, что он не работает с некоторыми ландшафтными приложениями и не всегда записывает.

Вот учебник, который я написал об использовании его более подробно:

http://talesfromtherift.com/how-to-screen-record-arkit/

Другие вопросы по тегам