iOS アプリで 120fps・240fps のスローモーション動画を撮るための Swift 4 実装
ふと「iOS のカメラアプリでスローモーションが撮れるヤツって極端に少ないな?」と思い、Swift での実装の仕方を調べてみた。
iOS アプリにおけるスローモーション動画撮影に関して解説されている日本語文献は、以下の @shu223
さんの文献ぐらいしか見当たらなかった。
- 参考 : AV Foundationで240fpsスローモーション動画撮影を実装する
- 参考 : AV Foundationで120fpsスローモーション動画撮影を実装する - Over&Out その後
- 参考 : GitHub - shu223/SlowMotionVideoRecorder: 120/240 fps SLO-MO video recorder using AVFoundation. Including convenient wrapper class.
コチラは Objective-C で書かれているようだったので、同様の処理を Swift 4 に移植してみようと思う。
目次
環境情報
- 対象 iOS バージョン : v11.3
- 実機 : iPhoneSE … iOS v11.3 (15E216)
- Xcode : v9.3 (9E145)
- Swift : v4.1 (swiftlang-902.0.48 clang-902.0.37.1)
元のコードの仕組み
今回参考とする元コードは以下。
- 参考 : SlowMotionVideoRecorder/TTMCaptureManager.m at master · shu223/SlowMotionVideoRecorder · GitHub
バックカメラを使うビデオ用の AVCaptureDevice
を作り、その中から利用できるフォーマット情報 AVCaptureDeviceFormat
の一覧を取り出している。フォーマット情報をループで1つずつ見ていって、指定のフレームレート (FPS) で解像度が一番大きいフォーマットを拾い上げている。期待するフォーマットが取得できたら、最初に生成した AVCaptureDevice
の activeFormat
に設定し、フレームレートを設定して完了、という流れだ。
参考までに、iPhone7 Plus で利用できるフォーマット情報を取得した結果は以下のとおり。
- record-slow-motion-video-on-iphone/ViewController.swift at 157d202724f8c080ca0014ff4c00c5c7edee90a1 · Neos21/record-slow-motion-video-on-iphone
- 変数
format
を出力した
- 変数
<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()
でいきなり取得できるようになったようだ。
- ↓Swift 4・iOS 11 で正しいやり方
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 に変わったようだ。
- ↓Swift 4・iOS 11 で正しいやり方
try videoDevice.lockForConfiguration()
videoDevice.activeFormat = selectedFormat
- ↓古いやり方
if videoDevice.lockForConfiguration() {
videoDevice.activeFormat = selectedFormat
}
CMFormatDescription
の vide
って何?
上述のコードでいう変数 description
、CMFormatDescription
を見てみると、vide
という項目があり、420v
とか 420f
とかいう値が入っている。コレは何かというと、ビデオフレームのデータの並び順のことらしい。
420v
は値域がビデオレンジ。420f
はフルレンジ。フルレンジである 420f
の方が画質としては綺麗なのだが、そこまで大差はない様子。
両者の区別をしたいが上手くできなかった。for ... in
ループの中で 420f
の方が後に来ていたので、selectedFormat
は2回更新されて 420f
の方のフォーマットが設定されるので、このままほっといていいかな。
CMFormatDescription
の binned
って何?ビニングの話
CMFormatDescription
の中に、binned
という項目が遭った。コレはビニングという仕組みを利用したフォーマットか否かを示している。
ビニングとは、隣接するピクセルをひとまとめにすることで暗所で画質の劣化をおさえたりする技術のこと。60fps までは、同一の解像度とフレームレートで、ビニングするフォーマットとビニングしないフォーマットが用意されており、120fps 以上はビニングするフォーマットしかない。
format.isVideoBinned
でも拾える。
セッションに出力を追加する時、追加できるか確認する
セッションに出力を設定できるか確認するための canAddOutput()
というメソッドがあったりする。
- ↓特に確認しない場合、出力を用意したらそのまま
addOutput()
する
fileOutput = AVCaptureMovieFileOutput()
session.addOutput(fileOutput)
- ↓確認する場合
fileOutput = AVCaptureMovieFileOutput()
if session.canAddOutput(fileOutput) {
session.addOutput(fileOutput)
}
以上
コレで、Swift アプリに手軽にスーパースロー動画撮影の仕組みを組み込めるようになったかと思う。
カメラアプリとしての機能拡充は皆様に別途やっていただくとしよう…。