iOS アプリで動画撮影する際オートフォーカスモードを指定する Swift コード
昨日に引き続き、AVFoundation を利用して動画を撮影する iOS アプリの Swift コード片の拡充。
- 過去記事 : iOS アプリで 120fps・240fps のスローモーション動画を撮るための Swift 4 実装
- 過去記事 : 撮影した動画ファイルを iOS アプリ内に保存し、任意のタイミングでフォトライブラリに書き出す Swift コード
- 過去記事 : iOS アプリで動画撮影する際手ブレ補正を効かせる Swift コード
今回は動画撮影時のオートフォーカスモードを調整するためのコードを紹介する。
ViewController.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.focusMode
に AVCaptureDevice.FocusMode
のいずれかのモードを指定することで、オートフォーカスモードを指定できる。
- AVCaptureDevice.FocusMode
- AVCaptureDevice.FocusMode.locked : フォーカスを固定したままにする
- AVCaptureDevice.FocusMode.autoFocus : オートフォーカスを一度実行し、フォーカスが合ったら
locked
モードに切り替わる - AVCaptureDevice.FocusMode.continuousAutoFocus : 継続してオートフォーカスを行う
デフォルトだと continuousAutoFocus
(継続的な AF) が選択されていると思うので、動画を撮影すると頻繁にピント合わせが発生すると思う。画面がピンボケとフォーカスを繰り返して鬱陶しいので、 autoFocus
モードを指定して、オートフォーカスを1度だけ実行することにする。autoFocus
モードはピントが合い次第 locked
モードに移行する。locked
モードは文字どおり、オートフォーカスが行われないので、被写体との距離が変わったらピンボケした状態になる。
また、videoDevice.isSmoothAutoFocusEnabled
に true
を指定すると、オートフォーカスの速度をデフォルトよりも緩やかにしてくれる。シャッ、シャッと素早くピント合わせされる絵面は案外鬱陶しくて見づらいので、AF 速度を若干落とし、スムーズにピント合わせされるようにするワケだ。効き目のほどは劇的に変わるワケではないが、一応入れておく。
フォーカスモードを変更する際は videoDevice.lockForConfiguration()
でロックを取得する必要があることに留意。
【2】
: オートフォーカス切替用のタイマーを設定する
【1】
はビデオフォーマットを取得した後、録画を開始する前に autoFocus
モードを指定している。このまま動画の録画がスタートすると、1度だけオートフォーカスが行われ、以降はオートフォーカスが行われなくなる。
continuousAutoFocus
は頻繁に AF が動いて鬱陶しいが、全く AF が行われないのも扱いづらい。以下の参考文献では、時間や加速度を利用してイイカンジに autoFocus
と locked
モードを切り替えているが、AVFoundation で高機能な動画撮影アプリを作るにはこうした処理を自前で実装する必要が出てくる。
- AVFoundationを使ったカメラの実装 : 画面タップでフォーカス当てる実装 - Qiita
- ステートマシンを自前で用意し、ContinuousAuto・Auto・Locked モードをうまく切り替えるための実装
今回は面倒臭いので、一律で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 のロックを取得し、focusMode
に autoFocus
を指定するだけ。
/// 【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