【MacOS】スクリーンレコーディング 【Swift】

iOS11から本体機能にスクリーンレコーディングがついたということで、ここではあえてMacOSでスクリーンレコーディングする方法を紹介してみたいと思います。

MacOSの場合標準の`AVFoundation`を使用することでカメラやマイク、画面のレコーディングを行うことができます。

基本的には

`セッションの作成` -> `入力元の指定` -> `出力先の指定` -> `開始`
のステップとなっており、それぞれ`入力元`と`出力先`をカスタマイズすることで任意のデバイス(カメラ、マイク・画面等)から好きな形式で出力することができます。

では、実際にMacのメインディスプレイをレコーディングしてMP4形式で出力する方法を紹介したいと思います。

```swift
/*** 画面を10秒録画して.mp4で出力 ***/
import Cocoa

final class ViewController: NSViewController {

    private let recorder = ScreenRecorder()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //　録画開始
        recorder.start()
        
        // 10秒後に録画停止
        DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
            self?.recorder.stop()
        }
    }
}
```

```swift
/*** 画面を10秒録画して.mp4で出力 ***/
import Foundation
import AVFoundation
import Cocoa

final class ScreenRecorder: NSObject, AVCaptureFileOutputRecordingDelegate {

    private let session = AVCaptureSession()
    
    // 出力形式をファイル保存にする
    private let output = AVCaptureMovieFileOutput()
    
    // 保存ファイル名
    private let fileName = "sample.mp4"
    
    // 保存先
    private var savePath: URL {
        return URL(fileURLWithPath: "/Users/hogehoge/Desktop/" + fileName)
    }
 
    override init() {
        super.init()
        
        // 録画の画質を指定
        session.sessionPreset = AVCaptureSessionPresetHigh

        // メインディスプレイのIDを取得
        let displayID = CGDirectDisplayID(CGMainDisplayID())

        // 入力ソースをメインディスプレイに設定
        let input: AVCaptureScreenInput = AVCaptureScreenInput(displayID: displayID)

        // カーソルは録画対象から外す
        input.capturesCursor = false

        // クリックは録画する
        input.capturesMouseClicks = true
        
        // セッションにInputソースを渡す
        if session.canAddInput(input) {
            session.addInput(input)
        }
        
        // 出力ソースを設定
        if session.canAddOutput(output) {
            session.addOutput(output)
        }
    }
    
    // 録画開始
    func start() {
        session.startRunning()
        output.startRecording(toOutputFileURL: savePath, recordingDelegate: self)
    }
    
    // 録画停止
    func stop() {
        output.stopRecording()
    }
    
    func capture(_ captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAt outputFileURL: URL!, fromConnections connections: [Any]!, error: Error!) {
        session.stopRunning()
    }
}
```

また`AVCaptureSession`を利用してカメラの動画や画面から取得されるサンプルバッファを取得したい場合は次のように書くことできます。

```swift
/*** 入力ソースからの画素情報にアクセスしたい場合 ***/
import Foundation
import AVFoundation

// AVCaptureFileOutputRecordingDelegateに準拠
final class ScreenRecorder: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {

    private let session = AVCaptureSession()

    // ビデオアウトプットを指定
    private let output = AVCaptureVideoDataOutput()
    private var callbackQueue: DispatchQueue!
 
    override init() {
        super.init()
        
        session.sessionPreset = AVCaptureSessionPresetHigh

        let displayID = CGDirectDisplayID(CGMainDisplayID())

        let input: AVCaptureScreenInput = AVCaptureScreenInput(displayID: displayID)

        // 60FPSに設定
        input.minFrameDuration = CMTime(seconds: 1, preferredTimescale: 60)
        
        if session.canAddInput(input) {
            session.addInput(input)
        }
        
        // サンプルバッファが蓄積されるQueue. 必ずSerialなQueueを指定してください
        callbackQueue = DispatchQueue(label: "jp.rinov.screenRecord_example")
        
        // 処理の遅れたフレームは無視する
        output.alwaysDiscardsLateVideoFrames = true
        
        // DelegateとcallbackQueueを指定
        output.setSampleBufferDelegate(self, queue: callbackQueue)

        if session.canAddOutput(output) {
            session.addOutput(output)
        }
    }
    
    func start() {
        session.startRunning()
    }
    
    func stop() {
        session.stopRunning()
    }
    
    // MARK: - AVCaptureVideoDataOutputSampleBufferDelegate
    
    public func captureOutput(_ captureOutput: AVCaptureOutput!, didDrop sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
        // 処理落ちによりバッファが取得できない場合
    }
    public func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
        // SampleBufferが更新された場合
    }
}

```

上記サンプルではバッファが更新されるごとにデリゲートメソッドに通知が来ますが、`minFrameDuration`を60FPS相当にしていも実際にデリゲートメソッドが呼ばれる感覚は5FPS相当です。つまりリアルタイムに画素に対する処理を行いたい場合にはほとんど使用できません。動画編集などを行いたい場合などは一度`AVCaptureMovieFileOutput`でファイル出力後に当該ファイルを読み込んで処理を行うほうが良さそうです。

参考:
https://developer.apple.com/documentation/avfoundation/avcapturemoviefileoutput
https://developer.apple.com/documentation/avfoundation/avcapturescreeninput