iOS アプリで 120fps・240fps のスローモーション動画を撮るための Swift 4 実装

ふと「iOS のカメラアプリでスローモーションが撮れるヤツって極端に少ないな?」と思い、Swift での実装の仕方を調べてみた。

iOS アプリにおけるスローモーション動画撮影に関して解説されている日本語文献は、以下の @shu223 さんの文献ぐらいしか見当たらなかった。

コチラは Objective-C で書かれているようだったので、同様の処理を Swift 4 に移植してみようと思う。

目次

環境情報

元のコードの仕組み

今回参考とする元コードは以下。

バックカメラを使うビデオ用の AVCaptureDevice を作り、その中から利用できるフォーマット情報 AVCaptureDeviceFormat の一覧を取り出している。フォーマット情報をループで1つずつ見ていって、指定のフレームレート (FPS) で解像度が一番大きいフォーマットを拾い上げている。期待するフォーマットが取得できたら、最初に生成した AVCaptureDeviceactiveFormat に設定し、フレームレートを設定して完了、という流れだ。

参考までに、iPhone7 Plus で利用できるフォーマット情報を取得した結果は以下のとおり。

<AVCaptureDeviceFormat: 0x281590660 'vide'/'420v'  192x 144, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @21.00), AF System:2, ISO:22.0-1408.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x281590650 'vide'/'420f'  192x 144, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @21.00), AF System:2, ISO:22.0-1408.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x281590640 'vide'/'420v'  352x 288, { 3- 30 fps}, HRSI:3696x3024, fov:54.070, max zoom:189.00 (upscales @10.50), AF System:2, ISO:22.0-1408.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x281590630 'vide'/'420f'  352x 288, { 3- 30 fps}, HRSI:3696x3024, fov:54.070, max zoom:189.00 (upscales @10.50), AF System:2, ISO:22.0-1408.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x281590620 'vide'/'420v'  480x 360, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @8.40), AF System:2, ISO:22.0-1408.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x281590610 'vide'/'420f'  480x 360, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @8.40), AF System:2, ISO:22.0-1408.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x281590600 'vide'/'420v'  640x 480, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @6.30), AF System:2, ISO:22.0-1408.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x2815905f0 'vide'/'420f'  640x 480, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @6.30), AF System:2, ISO:22.0-1408.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x2815905e0 'vide'/'420v'  640x 480, { 3- 60 fps}, HRSI:2048x1536, fov:59.680, binned, max zoom:96.00 (upscales @3.20), AF System:1, ISO:22.0-1408.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x2815905d0 'vide'/'420f'  640x 480, { 3- 60 fps}, HRSI:2048x1536, fov:59.680, binned, max zoom:96.00 (upscales @3.20), AF System:1, ISO:22.0-1408.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x2815905c0 'vide'/'420v'  960x 540, { 3- 60 fps}, HRSI:4096x2304, fov:59.680, supports vis, max zoom:130.88 (upscales @3.88), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x2815905b0 'vide'/'420f'  960x 540, { 3- 60 fps}, HRSI:4096x2304, fov:59.680, supports vis, max zoom:130.88 (upscales @3.88), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x2815905a0 'vide'/'420v' 1024x 768, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @3.94), AF System:2, ISO:22.0-1408.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x281590590 'vide'/'420f' 1024x 768, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @3.94), AF System:2, ISO:22.0-1408.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x281590580 'vide'/'420v' 1280x 720, { 3- 30 fps}, HRSI:4096x2304, fov:59.680, supports vis, max zoom:120.00 (upscales @2.91), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x281590570 'vide'/'420f' 1280x 720, { 3- 30 fps}, HRSI:4096x2304, fov:59.680, supports vis, max zoom:120.00 (upscales @2.91), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x281590560 'vide'/'420v' 1280x 720, { 3- 60 fps}, HRSI:4096x2304, fov:59.680, supports vis, max zoom:120.00 (upscales @2.91), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x281590550 'vide'/'420f' 1280x 720, { 3- 60 fps}, HRSI:4096x2304, fov:59.680, supports vis, max zoom:120.00 (upscales @2.91), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x281590540 'vide'/'420v' 1280x 720, { 6- 60 fps}, fov:59.680, binned, supports vis, max zoom:60.00 (upscales @1.45), AF System:1, ISO:22.0-704.0, SS:0.000002-0.166667>
<AVCaptureDeviceFormat: 0x281590530 'vide'/'420f' 1280x 720, { 6- 60 fps}, fov:59.680, binned, supports vis, max zoom:60.00 (upscales @1.45), AF System:1, ISO:22.0-704.0, SS:0.000002-0.166667, supports wide color>
<AVCaptureDeviceFormat: 0x281590520 'vide'/'420v' 1280x 720, { 6-240 fps}, fov:59.680, binned, supports vis, max zoom:65.50 (upscales @1.45), AF System:1, ISO:22.0-704.0, SS:0.000002-0.166667>
<AVCaptureDeviceFormat: 0x281590510 'vide'/'420f' 1280x 720, { 6-240 fps}, fov:59.680, binned, supports vis, max zoom:65.50 (upscales @1.45), AF System:1, ISO:22.0-704.0, SS:0.000002-0.166667, supports wide color>
<AVCaptureDeviceFormat: 0x281590500 'vide'/'420v' 1440x1080, { 3- 60 fps}, HRSI:2048x1536, fov:59.680, binned, max zoom:96.00 (upscales @1.42), AF System:1, ISO:22.0-1408.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x2815904f0 'vide'/'420f' 1440x1080, { 3- 60 fps}, HRSI:2048x1536, fov:59.680, binned, max zoom:96.00 (upscales @1.42), AF System:1, ISO:22.0-1408.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x2815904e0 'vide'/'420v' 1920x1080, { 3- 30 fps}, HRSI:4096x2304, fov:59.680, supports vis, max zoom:16.00 (upscales @1.94), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x2815904d0 'vide'/'420f' 1920x1080, { 3- 30 fps}, HRSI:4096x2304, fov:59.680, supports vis, max zoom:16.00 (upscales @1.94), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x2815904c0 'vide'/'420v' 1920x1080, { 3- 60 fps}, HRSI:4096x2304, fov:59.680, supports vis, max zoom:16.00 (upscales @1.94), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x2815904b0 'vide'/'420f' 1920x1080, { 3- 60 fps}, HRSI:4096x2304, fov:59.680, supports vis, max zoom:16.00 (upscales @1.94), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x2815904a0 'vide'/'420v' 1920x1080, { 6-120 fps}, fov:59.680, binned, supports vis, max zoom:65.50 (upscales @1.00), AF System:1, ISO:22.0-704.0, SS:0.000002-0.166667>
<AVCaptureDeviceFormat: 0x281590490 'vide'/'420f' 1920x1080, { 6-120 fps}, fov:59.680, binned, supports vis, max zoom:65.50 (upscales @1.00), AF System:1, ISO:22.0-704.0, SS:0.000002-0.166667, supports wide color>
<AVCaptureDeviceFormat: 0x281590480 'vide'/'420v' 1920x1440, { 3- 60 fps}, HRSI:2048x1536, fov:59.680, binned, max zoom:96.00 (upscales @1.07), AF System:1, ISO:22.0-1408.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x281590460 'vide'/'420f' 1920x1440, { 3- 60 fps}, HRSI:2048x1536, fov:59.680, binned, max zoom:96.00 (upscales @1.07), AF System:1, ISO:22.0-1408.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x281590410 'vide'/'420v' 2592x1936, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @1.56), AF System:2, ISO:22.0-1760.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x2815903f0 'vide'/'420f' 2592x1936, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @1.56), AF System:2, ISO:22.0-1760.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x281590360 'vide'/'420v' 3264x2448, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @1.24), AF System:2, ISO:22.0-1760.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x2815903d0 'vide'/'420f' 3264x2448, { 3- 30 fps}, HRSI:4032x3024, fov:58.986, max zoom:189.00 (upscales @1.24), AF System:2, ISO:22.0-1760.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x281590760 'vide'/'420v' 3840x2160, { 3- 30 fps}, fov:59.680, supports vis, max zoom:130.88 (upscales @1.00), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x281590770 'vide'/'420f' 3840x2160, { 3- 30 fps}, fov:59.680, supports vis, max zoom:130.88 (upscales @1.00), AF System:2, ISO:22.0-704.0, SS:0.000005-0.333333, supports wide color>
<AVCaptureDeviceFormat: 0x281590780 'vide'/'420v' 4032x3024, { 3- 30 fps}, fov:58.986, max zoom:189.00 (upscales @1.00), AF System:2, ISO:22.0-1760.0, SS:0.000005-0.333333>
<AVCaptureDeviceFormat: 0x281590790 'vide'/'420f' 4032x3024, { 3- 30 fps}, fov:58.986, max zoom:189.00 (upscales @1.00), AF System:2, ISO:22.0-1760.0, SS:0.000005-0.333333, supports wide color>

移植したコード

この処理を Swift 4・iOS 11 向けに実装し直したコードは以下。

// 適当な ViewController で実装してみた

import UIKit
import AVFoundation
import Photos

class ViewController: UIViewController, AVCaptureFileOutputRecordingDelegate {
  // セッション
  var session: AVCaptureSession!
  // ビデオデバイス
  var videoDevice: AVCaptureDevice!
  // オーディオデバイス
  var audioDevice: AVCaptureDevice!
  // ファイル出力
  var fileOutput: AVCaptureMovieFileOutput!
  
  // 初期処理
  func initSession() {
    // セッション生成
    session = AVCaptureSession()
    
    // 入力 : 背面カメラ
    videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
    let videoInput = try! AVCaptureDeviceInput.init(device: videoDevice)
    session.addInput(videoInput)
    
    // ↓ココ重要!!!!!
    // 120fps のフォーマットを探索して設定する
    switchFormat(desiredFps: 120.0)
    
    // 入力 : マイク
    audioDevice = AVCaptureDevice.default(for: .audio)
    let audioInput = try! AVCaptureDeviceInput.init(device: audioDevice)
    session.addInput(audioInput)
    
    // ファイル出力設定
    fileOutput = AVCaptureMovieFileOutput()
    session.addOutput(fileOutput)
    
    // セッションを開始する (録画開始とは別)
    session.startRunning()
  }
  
  // 指定の FPS のフォーマットに切り替える (その FPS で最大解像度のフォーマットを選ぶ)
  // 
  // - Parameters:
  //   - desiredFps: 切り替えたい FPS (AVFrameRateRange.maxFrameRate が Double なので合わせる)
  func switchFormat(desiredFps: Double) {
    // セッションが始動しているかどうか
    let isRunning = session.isRunning
    
    // セッションが始動中なら止める
    if isRunning {
      session.stopRunning()
    }
    
    // 取得したフォーマットを格納する変数
    var selectedFormat: AVCaptureDevice.Format! = nil
    // そのフレームレートの中で一番大きい解像度を取得する
    var maxWidth: 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                                         // 幅
        print("フォーマット情報 : \(description)")
        
        // 指定のフレームレートで一番大きな解像度を得る
        if desiredFps == range.maxFrameRate && width >= maxWidth {
          print("このフォーマットを候補にする")
          selectedFormat = format
          maxWidth = width
        }
      }
    }
    
    // フォーマットが取得できていれば設定する
    if selectedFormat != nil {
      do {
        try videoDevice.lockForConfiguration()
        videoDevice.activeFormat = selectedFormat
        videoDevice.activeVideoMinFrameDuration = CMTimeMake(1, Int32(desiredFps))
        videoDevice.activeVideoMaxFrameDuration = CMTimeMake(1, Int32(desiredFps))
        videoDevice.unlockForConfiguration()
        print("フォーマット・フレームレートを設定 : \(desiredFps) fps・\(maxWidth) px")
      }
      catch {
        print("フォーマット・フレームレートが指定できなかった")
      }
    }
    else {
      print("指定のフォーマットが取得できなかった")
    }
    
    // セッションが始動中だったら再開する
    if isRunning {
      session.startRunning()
    }
  }
}

本質は switchFormat() 関数。switchFormat(desiredFps: 120.0) と叩けば 120FPS、switchFormat(desiredFps: 240.0) と叩けば 240FPS のフォーマットを探し出して設定してくれる。60.0 で 60FPS とかも可能。

僕が移植したコードは、フォーマット判定の条件文をちょっと変えてある。手元の iPhoneSE と iPhone7Plus ではコレで動いたが、機種によってもし上手く挙動しない場合は、適宜調整して欲しい。

詳細

Swift に移植するにあたって勉強した細々とした情報を書いていこうと思う。Objective-C の癖を Swift に直し、かつ古い API を最新のモノに書き直すのに苦労した…。

バックカメラを選択するやり方が変わっていた

巷の文献を見ていると、VideoDevice の取得方法が違って、手元で書いてみると非推奨だの何だのとエラーが出てしまった。どうも AVCaptureDevice.devices() が非推奨になって、.default() でいきなり取得できるようになったようだ。

videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
print("\(videoDevice!.localizedName) found!")
for d in AVCaptureDevice.devices() {
  if (d as AnyObject).position == AVCaptureDevice.Position.back {
    videoDevice = d as AVCaptureDevice
    print("\(device!.localizedName) found.")
  }
}

入力の初期化処理で例外チェック

マイク入力などを準備する際に例外チェックする実装もできる。

audioDevice = AVCaptureDevice.default(for: .audio)
let audioInput = try! AVCaptureDeviceInput.init(device: audioDevice)
session.addInput(audioInput)
do {
  let audioInput = try AVCaptureDeviceInput.init(device: audioDevice)
  session.addInput(audioInput)
}
catch {
  print("音声録音開始できず")
}

フレームレート設定をするには AVCaptureSession に AVCaptureDeviceInput を追加しておいてから

コレ重要。session.addInput(videoInput) した後でないと、今回の switchFormat() 関数によるフレームレート設定が有効にならない。

lockForConfiguration の仕様が変わった

これまでは videoDevice.lockForConfiguration() は、ロックできたら (できていたら) true を返す、という API だったが、最近はロックできなかった時に例外を投げるという API に変わったようだ。

try videoDevice.lockForConfiguration()
videoDevice.activeFormat = selectedFormat
if videoDevice.lockForConfiguration() {
  videoDevice.activeFormat = selectedFormat
}

CMFormatDescriptionvide って何?

上述のコードでいう変数 descriptionCMFormatDescription を見てみると、vide という項目があり、420v とか 420f とかいう値が入っている。コレは何かというと、ビデオフレームのデータの並び順のことらしい。

420v は値域がビデオレンジ。420f はフルレンジ。フルレンジである 420f の方が画質としては綺麗なのだが、そこまで大差はない様子。

両者の区別をしたいが上手くできなかった。for ... in ループの中で 420f の方が後に来ていたので、selectedFormat は2回更新されて 420f の方のフォーマットが設定されるので、このままほっといていいかな。

CMFormatDescriptionbinned って何?ビニングの話

CMFormatDescription の中に、binned という項目が遭った。コレはビニングという仕組みを利用したフォーマットか否かを示している。

ビニングとは、隣接するピクセルをひとまとめにすることで暗所で画質の劣化をおさえたりする技術のこと。60fps までは、同一の解像度とフレームレートで、ビニングするフォーマットとビニングしないフォーマットが用意されており、120fps 以上はビニングするフォーマットしかない。

format.isVideoBinned でも拾える。

セッションに出力を追加する時、追加できるか確認する

セッションに出力を設定できるか確認するための canAddOutput() というメソッドがあったりする。

fileOutput = AVCaptureMovieFileOutput()
session.addOutput(fileOutput)
fileOutput = AVCaptureMovieFileOutput()
if session.canAddOutput(fileOutput) {
  session.addOutput(fileOutput)
}

以上

コレで、Swift アプリに手軽にスーパースロー動画撮影の仕組みを組み込めるようになったかと思う。

カメラアプリとしての機能拡充は皆様に別途やっていただくとしよう…。