AVFoundation_OSXによる画面キャプチャ

## Macにカメラを繋いで録画する。
最初はopenframeworksの“ofxVideoRecorderExample”をベースにするつもりだったが、
出力動画に音ズレが発生した。
ライブラリの中身を見ると、以下の構成となっている。

```
* ffmpeg（コマンドツール）
* スレッド処理
* C++（OFだから当然だけど）
```

これらを短時間で扱うのは今の自分にはちょっと厳しい。
AppleのAVFoundation（Swift）で使うことにした。
（時間のあるときに、OFのほうもなんとかしたいが）
最終的には、iPadと連携させる必要もあったので、
両方ともSwiftで実装できるという利点もあった。

AVFoundationは色々やってはくれるのだけど、オブジェクトの使い方がややこしい。
AVCaptureDevice、AVCaptureSessionについては、
以下のリンクに掲載されている図がわかりやすい。
https://dev.classmethod.jp/smartphone/ios-avfoundatio-avcapturemoviefileoutput/


こちらも整理されていて親切だ。今回のソースで使っているのは、図の青い部分。
https://qiita.com/KUMAN/items/a2a1e903b26b062d2d79

#### 今回用いるAVFoundationのオブジェクト
1. __AVCaptureDevice__：デバイス（カメラ、マイク）を登録する。
2. __AVCaptureDeviceInput__：1で登録したデバイスをAVCaptureSessionの入力デバイスとして扱うためのオブジェクト。
3. __AVCaptureMovieFileOutput__：出力ファイルを扱う。
4. __AVCaptureSession__：今回の主役。キャプチャ処理を管理するオブジェクト。入力（2）と出力（3）のオブジェクトはここに追加する。録画開始や終了もこのオブジェクトが扱う。
5. __AVCaptureVideoPreviewLayer__：キャプチャ映像の表示に用いる。



```ViewController.swift

//
//  ViewController.swift
//
//  画面右下のボタンを押すと録画が始まり、もう一度押すと録画が終了。
//  録画ファイルは/Users/USER_NAME/Documents/temp.movに保存される。
//


import Cocoa
import AVFoundation
import AppKit


class ViewController: NSViewController, AVCaptureFileOutputRecordingDelegate {
    
    @IBOutlet weak var videoView: NSView!
    
    @IBAction func testBtn(_ sender: NSButton) {
        
        if self.isRecording {
            self.isRecording = false
            self.stopRecording()
        } else {
            self.isRecording = true
            self.startRecording()
        }
        
    }
    
    
    ////////////////////////////////////////////////////////////////////////////////
    // AVFoundation Objs
    ////////////////////////////////////////////////////////////////////////////////

    // ビデオデバイス←カメラ
    private var videoDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
    // オーディオバイス←マイク
    private var audioDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio)


    // 入力デバイスを指定してインスタンスを生成し、デバイスから得られるメディア（映像、音声）を
    // AVCaptureSessionのインスタンスに追加する
    private var videoInput:AVCaptureDeviceInput!
    private var audioInput:AVCaptureDeviceInput!


    // キャプチャしたメディア（映像、音声）を"QuickTime形式（.mov）"で記録する
    private let fileOutput = AVCaptureMovieFileOutput()


    // デバイス（カメラ、マイク）からキャプチャしたメディア（映像、音声）を管理するオブジェクト
    private var captureSession = AVCaptureSession()
    

    // デバイスからキャプチャした映像を表示するレイヤー（CALayerのサブクラス）
    private var videoLayer : AVCaptureVideoPreviewLayer!
    
    


    private var isRecording = false

    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    
    override func viewDidAppear() {
        super.viewDidAppear()
        
        
        ////////////////////////////////////////////////////////////////////////////////
        // captureSessionに各メディア（映像、音声）の入力デバイスと出力先を紐づける
        ////////////////////////////////////////////////////////////////////////////////
        
        // 入力カメラの指定
        do {
            // カメラを指定してアクセス
            videoInput = try AVCaptureDeviceInput(device: videoDevice) as AVCaptureDeviceInput
        } catch let error as NSError {
            print(error)
        }
        // 入力カメラをcaptureSessionに登録
        self.captureSession.addInput(videoInput)
        
        
        // 入力マイクの指定
        do {
            // マイクを指定してアクセス
            audioInput = try AVCaptureDeviceInput(device: audioDevice) as AVCaptureDeviceInput
        } catch let error as NSError {
            print(error)
        }
        // 入力マイクをcaptureSessionに登録
        self.captureSession.addInput(audioInput);
        
        // 出力先をcaptureSessionに登録
        self.captureSession.addOutput(self.fileOutput)
        
        
        // 画面表示用レイヤーに一連のセッションを紐づける
        self.videoLayer = AVCaptureVideoPreviewLayer(session: captureSession) as AVCaptureVideoPreviewLayer
        
        // 表示画面フレームの設定
        self.videoView.layer?.addSublayer(videoLayer)
        self.videoLayer.frame = self.videoView.frame

        
        // キャプチャセッション稼働開始
        self.captureSession.startRunning()
        
        
    }
    
    override func viewDidDisappear() {
        super.viewDidDisappear()
        
        // カメラの停止とメモリ解放
        self.captureSession.stopRunning()
        for output in self.captureSession.outputs {
            self.captureSession.removeOutput(output as! AVCaptureOutput)
        }
        for input in self.captureSession.inputs {
            self.captureSession.removeInput(input as! AVCaptureInput)
        }
    }
    
    
    private func startRecording() {
        // 出力先のディレクトリパス
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        let documentsDirectory = paths[0] as String
        // .movにしないとちゃんと動画が出力されない（生成された動画に音声が入ってなかったりする）
        let filePath : String? = "\(documentsDirectory)/temp.mov"
        let fileURL : NSURL = NSURL(fileURLWithPath: filePath!)
        
        // ファイルが存在している場合は削除
        if FileManager.default.fileExists(atPath: filePath!) {
            try! FileManager.default.removeItem(atPath: filePath!)
        }
        self.fileOutput.startRecording(toOutputFileURL: fileURL as URL!, recordingDelegate: self)
        
    }
    
    private func stopRecording() {
        self.fileOutput.stopRecording()
    }
    
    
    ///////////////////////////////////////////////////////////////////////////////////
    // AVCaptureFileOutputRecordingDelegate methods
    ///////////////////////////////////////////////////////////////////////////////////
    
    func capture(_ captureOutput: AVCaptureFileOutput!, didStartRecordingToOutputFileAt fileURL: URL!, fromConnections connections: [Any]!) {
        print("=================== didStartRecordingToOutputFileAt: \(fileURL.path)")
    }
    
    func capture(_ captureOutput: AVCaptureFileOutput!, willFinishRecordingToOutputFileAt fileURL: URL!, fromConnections connections: [Any]!, error: Error!) {
        print("=================== willFinishRecordingToOutputFileAt: \(fileURL.path)")
    }
    
    func capture(_ captureOutput: AVCaptureFileOutput!, didPauseRecordingToOutputFileAt fileURL: URL!, fromConnections connections: [Any]!) {
        print("=================== didPauseRecordingToOutputFileAt: \(fileURL.path)")
    }
    
    @available(OSX 10.7, *)
    func capture(_ captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAt outputFileURL: URL!, fromConnections connections: [Any]!, error: Error!) {
        print("=================== didFinishRecordingToOutputFileAt: \(outputFileURL.path)")
    }
}


```

最終的には録画動画を加工して.mp4にする必要があっため、出力ファイルを.mp4にしていた。Web上にもそういうサンプルがある。
しかし、音が記録されなくなり、OFでの音ズレ悪夢が一瞬（じゃなかったが）蘇る。

公式には以下のように記載されている。

```
A capture output that records video and audio to a QuickTime movie file.
```
https://developer.apple.com/documentation/avfoundation/avcapturemoviefileoutput

.movじゃないといけないようだ。

動画ファイルは内部的には画像と音声を組み合わせたもので、そのため音がずれたり弾かれたりということが発生しうる。また、ファイル形式（.mov、mp4、.aac）も、そこに絡んでくる。ffmpegの制御とともにこの辺のことはいずれマスターしたいものである。

ソースは以下に置いてあります。
https://github.com/moccow/AVF