iOS アプリで動画撮影する際オートフォーカスモードを指定する Swift コード

昨日に引き続き、AVFoundation を利用して動画を撮影する iOS アプリの Swift コード片の拡充。

今回は動画撮影時のオートフォーカスモードを調整するためのコードを紹介する。

import UIKit
import AVFoundation
import Photos

class ViewController: UIViewController, AVCaptureFileOutputRecordingDelegate {
  /// セッション
  var session: AVCaptureSession!
  /// ビデオデバイス
  var videoDevice: AVCaptureDevice!
  /// オーディオデバイス
  var audioDevice: AVCaptureDevice!
  /// ファイル出力
  var fileOutput: AVCaptureMovieFileOutput!
  
  // オートフォーカスを再設定するためのタイマー
  var focusTimer: Timer?
  
  // …… (中略) ……
  
  /// 指定の FPS のフォーマットに切り替える (その FPS で最大解像度のフォーマットを選ぶ)
  ///
  /// - parameter desiredFps: 切り替えたい FPS (AVFrameRateRange.maxFrameRate が Double なので合わせる)
  private func switchFormat(desiredFps: Double) {
    let isRunning = session.isRunning
    if isRunning { session.stopRunning() }  // セッションが始動中なら一時的に停止しておく
    
    // 取得したフォーマットを格納する変数
    var selectedFormat: AVCaptureDevice.Format! = nil
    // そのフレームレートの中で一番大きい解像度を取得する
    var currentMaxWidth: Int32 = 0
    
    // フォーマットを探る
    for format in videoDevice.formats {
      // フォーマット内の情報を抜き出す (for in と書いているが1つの format につき1つの range しかない)
      for range: AVFrameRateRange in format.videoSupportedFrameRateRanges {
        let description = format.formatDescription as CMFormatDescription  // フォーマットの説明
        let dimensions = CMVideoFormatDescriptionGetDimensions(description)  // 幅・高さ情報を抜き出す
        let width = dimensions.width  // 幅
        
        // 指定のフレームレートで一番大きな解像度を得る (1920px 以上は選ばない)
        if desiredFps == range.maxFrameRate && currentMaxWidth <= width && width <= 1920 {
          selectedFormat = format
          currentMaxWidth = width
        }
      }
    }
    
    // フォーマットが取得できていれば設定する
    if selectedFormat != nil {
      do {
        try videoDevice.lockForConfiguration()  // ロックできなければ例外を投げる
        videoDevice.activeFormat = selectedFormat
        videoDevice.activeVideoMinFrameDuration = CMTimeMake(value: 1, timescale: Int32(desiredFps))  // Swift 4.2.1 になって
        videoDevice.activeVideoMaxFrameDuration = CMTimeMake(value: 1, timescale: Int32(desiredFps))  // value と timescale の引数名を書かないといけなくなった
        
        // 【1】 以下2行を追加 : オートフォーカス設定
        videoDevice.focusMode = AVCaptureDevice.FocusMode.autoFocus  // オートフォーカスのモードを指定する
        videoDevice.isSmoothAutoFocusEnabled = true  // オートフォーカスの速度を滑らかにする
        
        videoDevice.unlockForConfiguration()
        if isRunning { session.startRunning() }  // セッションが始動中だった場合は一時停止していたものを再開する
      }
      catch {
        print("フォーマット・フレームレートが指定できなかった : \(desiredFps) fps")
      }
    }
    else {
      print("フォーマットが取得できなかった : \(desiredFps) fps")
    }
  }
  
  
  /// 録画を開始する
  private func startRecording() {
    // 録画開始処理……省略……
    fileOutput?.startRecording(to: fileURL as URL, recordingDelegate: self)
    
    // 【2】 タイマーを設定する
    if(focusTimer != nil) {
      focusTimer?.invalidate()  // タイマーが設定済だったら停止・破棄しておく (2回目以降の開始時は必ず通るようだが問題なし)
    }
    self.focusTimer = Timer.scheduledTimer(timeInterval: 5,  // 実行間隔 (秒) … 5秒おきにフォーカスモードを autoFocus に戻す
                                           target: self,     // 実行するメソッドを持つオブジェクトを指定する
                                           selector: #selector(ViewController.onFocusTimer),  // 実行するメソッド
                                           userInfo: nil,  // オブジェクトに付けて送信する値
                                           repeats: true)  // 繰り返し実行するかどうか
  }
  
  /// 【3】 一定期間ごとにオートフォーカスを行う
  @objc func onFocusTimer() {
    do {
      try videoDevice.lockForConfiguration()  // ロックできなければ例外を投げる
      videoDevice.focusMode = AVCaptureDevice.FocusMode.autoFocus  // 再度オートフォーカスにする
      videoDevice.unlockForConfiguration()
    }
    catch {
      print("onFocusTimer : エラー")
    }
  }
  
  /// 録画を停止する
  private func stopRecording() {
    fileOutput?.stopRecording()
    
    // 【4】 タイマーを停止・破棄する
    if(focusTimer != nil) {
      focusTimer?.invalidate()
    }
  }
}

コード全量は前回までの記事をそれぞれ参考にしてほしい。コード中の 【1】【4】 の4箇所、オートフォーカス設定に関するコードを追加した。

【1】 : フォーカスモードの設定

videoDevice.focusModeAVCaptureDevice.FocusMode のいずれかのモードを指定することで、オートフォーカスモードを指定できる。

デフォルトだと continuousAutoFocus (継続的な AF) が選択されていると思うので、動画を撮影すると頻繁にピント合わせが発生すると思う。画面がピンボケとフォーカスを繰り返して鬱陶しいので、 autoFocus モードを指定して、オートフォーカスを1度だけ実行することにする。autoFocus モードはピントが合い次第 locked モードに移行する。locked モードは文字どおり、オートフォーカスが行われないので、被写体との距離が変わったらピンボケした状態になる。

また、videoDevice.isSmoothAutoFocusEnabledtrue を指定すると、オートフォーカスの速度をデフォルトよりも緩やかにしてくれる。シャッ、シャッと素早くピント合わせされる絵面は案外鬱陶しくて見づらいので、AF 速度を若干落とし、スムーズにピント合わせされるようにするワケだ。効き目のほどは劇的に変わるワケではないが、一応入れておく。

フォーカスモードを変更する際は videoDevice.lockForConfiguration() でロックを取得する必要があることに留意。

【2】 : オートフォーカス切替用のタイマーを設定する

【1】 はビデオフォーマットを取得した後、録画を開始する前に autoFocus モードを指定している。このまま動画の録画がスタートすると、1度だけオートフォーカスが行われ、以降はオートフォーカスが行われなくなる。

continuousAutoFocus は頻繁に AF が動いて鬱陶しいが、全く AF が行われないのも扱いづらい。以下の参考文献では、時間や加速度を利用してイイカンジに autoFocuslocked モードを切り替えているが、AVFoundation で高機能な動画撮影アプリを作るにはこうした処理を自前で実装する必要が出てくる。

今回は面倒臭いので、一律で5秒おきに autoFocus モードに戻す (autoFocus モードで AF が完了したら自動的に locked になる) というタイマーを設定してみようと思う。それが以下のタイマー宣言部分だ。

self.focusTimer = Timer.scheduledTimer(timeInterval: 5,  // 実行間隔 (秒) … 5秒おきにフォーカスモードを autoFocus に戻す
                                       target: self,     // 実行するメソッドを持つオブジェクトを指定する
                                       selector: #selector(ViewController.onFocusTimer),  // 実行するメソッド
                                       userInfo: nil,  // オブジェクトに付けて送信する値
                                       repeats: true)  // 繰り返し実行するかどうか

【3】 : 一定期間ごとに行う処理

タイマー処理で呼び出している関数が以下。VideoDevice のロックを取得し、focusModeautoFocus を指定するだけ。

/// 【3】 一定期間ごとにオートフォーカスを行う
@objc func onFocusTimer() {
  do {
    try videoDevice.lockForConfiguration()  // ロックできなければ例外を投げる
    videoDevice.focusMode = AVCaptureDevice.FocusMode.autoFocus  // 再度オートフォーカスにする
    videoDevice.unlockForConfiguration()
  }
  catch {
    print("onFocusTimer : エラー")
  }
}

JavaScript でいうと、function onFocusTimer() {} を宣言しておいて、const focusTimer = setInterval(onFocusTimer, 5000) と繰り返し処理を設定した感じ。

【4】 : 動画停止時にタイマーも止める

動画を停止する際、同時にインスタンス変数 focusTimer に対して、タイマー停止処理を呼び出しておく。invalidate() メソッドがそれだ。

/// 録画を停止する
private func stopRecording() {
  fileOutput?.stopRecording()
  
  // 【4】 タイマーを停止・破棄する
  if(focusTimer != nil) {
    focusTimer?.invalidate()
  }
}

かなりタイマー処理が雑だが、コレでも一応狙ったとおり、5秒おきにオートフォーカスをやり直す、という処理は実現できた。タップした画面の位置にフォーカスを合わせるだとか、長押しでフォーカスロックモードに切り替えるとか、そういった処理を実装しようとなると、かなり頑張ってコードを書く必要があり、面倒なので諦める。w