LoginSignup
4
5

More than 5 years have passed since last update.

AVFoundation_OSXによる画面キャプチャ

Last updated at Posted at 2017-11-03

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.

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

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

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

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5