撮影した動画ファイルを iOS アプリ内に保存し、任意のタイミングでフォトライブラリに書き出す Swift コード
以前、スーパースロー動画を撮るための Swift コードを紹介した。
この時は AVCaptureSession#startRunning()
までで、実際の動画の撮影については触れていなかった。そこで今回は、このコードを利用した動画撮影のコードを掲載しておく。
目次
検証環境
- macOS Mojave
- Xcode v10.1
- Swift v4.2.1
- iOS v12.0.1
まずはコード全量
まずは ViewController.swift
のコード全量を載せる。
import UIKit
import AVFoundation
import Photos
class ViewController: UIViewController, AVCaptureFileOutputRecordingDelegate {
/// セッション
var session: AVCaptureSession!
/// ビデオデバイス
var videoDevice: AVCaptureDevice!
/// オーディオデバイス
var audioDevice: AVCaptureDevice!
/// ファイル出力
var fileOutput: AVCaptureMovieFileOutput!
/// 初期表示時の処理 : セッションの用意
override func viewDidLoad() {
super.viewDidLoad()
// セッション生成
session = AVCaptureSession()
// 入力 : 背面カメラ
videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
let videoInput = try! AVCaptureDeviceInput.init(device: videoDevice)
session.addInput(videoInput)
// フォーマット指定
switchFormat(desiredFps: 60.0)
// 入力 : マイク
audioDevice = AVCaptureDevice.default(for: .audio)
let audioInput = try! AVCaptureDeviceInput.init(device: audioDevice)
session.addInput(audioInput)
// 出力
fileOutput = AVCaptureMovieFileOutput()
session.addOutput(fileOutput)
// セッション開始
session.startRunning()
}
/// 録画完了時の処理 : オーバーライドしておくだけでココでは何もしない
///
/// - parameter output: AVCaptureFileOutput (アンダースコアは外部引数名を省略するもの・呼び出し元でも外部引数名を書かなくて呼び出せるようになる)
/// - parameter outputFileURL: URL (Option キーで参照できるドキュメントコメントを見ると内部引数名でコメントを書くっぽいので内部引数名を採用)
/// - parameter connections: AVCaptureConnection
/// - parameter error: Error
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
print("録画完了")
// XXX : もし録画完了時にフォトライブラリに書き出したければココで処理する
}
/// 指定の 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 の引数名を書かないといけなくなった
videoDevice.unlockForConfiguration()
if isRunning { session.startRunning() } // セッションが始動中だった場合は一時停止していたものを再開する
}
catch {
print("フォーマット・フレームレートが指定できなかった : \(desiredFps) fps")
}
}
else {
print("フォーマットが取得できなかった : \(desiredFps) fps")
}
}
/// 録画を開始する : ボタンからこの関数を呼び出してあげる
private func startRecording() {
// Documents ディレクトリ直下にファイルを生成する
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths[0] as String
// 現在時刻をファイル名に付与することでファイル重複を防ぐ : "myvideo-20190101125900999.mp4" な形式になる
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMddHHmmssSSS"
let filePath: String? = "\(documentsDirectory)/myvideo-\(formatter.string(from: Date())).mp4"
let fileURL = NSURL(fileURLWithPath: filePath!)
print("録画開始 : \(filePath!)")
fileOutput?.startRecording(to: fileURL as URL, recordingDelegate: self)
// XXX : あとココでプレビュー表示とか…
}
/// 録画を停止する : ボタンからこの関数を呼び出してあげる
private func stopRecording() {
print("録画停止")
fileOutput?.stopRecording()
// XXX : あとココでプレビュー表示の取り消しとか…
}
/// アプリ内に保存した mp4 ファイルをフォトライブラリに書き出す : ボタンからこの関数を呼び出してあげる
private func outputVideos() {
// Documents ディレクトリの URL
let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
do {
// Documents ディレクトリ配下のファイル一覧を取得する
let contentUrls = try FileManager.default.contentsOfDirectory(at: documentDirectoryURL, includingPropertiesForKeys: nil)
for contentUrl in contentUrls {
// 拡張子で判定する
if contentUrl.pathExtension == "mp4" {
// mp4 ファイルならフォトライブラリに書き出す
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: contentUrl)
}) { (isCompleted, error) in
if isCompleted {
// フォトライブラリに書き出し成功
do {
try FileManager.default.removeItem(atPath: contentUrl.path)
print("フォトライブラリ書き出し・ファイル削除成功 : \(contentUrl.lastPathComponent)")
}
catch {
print("フォトライブラリ書き出し後のファイル削除失敗 : \(contentUrl.lastPathComponent)")
}
}
else {
print("フォトライブラリ書き出し失敗 : \(contentUrl.lastPathComponent)")
}
}
}
}
}
catch {
print("ファイル一覧取得エラー")
}
}
}
長くなったが以上である。
コードの説明
今回のコードの構成は、以下のようになっている。
class ViewController: UIViewController, AVCaptureFileOutputRecordingDelegate {
// 初期表示時の処理
override func viewDidLoad() { }
// 録画完了時に自動的に呼ばれる・AVCaptureFileOutputRecordingDelegate が実装を必須にしているモノ
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { }
// 以下プライベート関数
// フォーマットの指定 : 今回のコードでは 60fps 固定にしたが、必要に応じて 120fps・240fps なども設定可能
private func switchFormat(desiredFps: Double) { }
// 録画開始用の処理
private func startRecording() { }
// 録画停止用の処理
private func stopRecording() { }
// 動画ファイルをフォトライブラリに書き出してアプリ内からは削除する処理
private func outputVideos() { }
}
これらのコードは一旦そのままにし、あとは画面 (Main.storyboard
) にボタンなどを配置して、ボタンタップ時にプライベート関数を呼び出すようにすれば良い。最低限必要になるのは、
- 録画開始ボタン
- 録画停止ボタン
- 動画ファイルの書き出しボタン
ぐらいだろうか。
録画開始時にファイル名と保存場所を指定している
今回のポイントは、動画の録画開始時にファイル名と保存場所を指定していること。startRecording()
を再掲する。
/// 録画を開始する : ボタンからこの関数を呼び出してあげる
private func startRecording() {
// Documents ディレクトリ直下にファイルを生成する
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths[0] as String
// 現在時刻をファイル名に付与することでファイル重複を防ぐ : "myvideo-20190101125900999.mp4" な形式になる
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMddHHmmssSSS"
let filePath: String? = "\(documentsDirectory)/myvideo-\(formatter.string(from: Date())).mp4"
let fileURL = NSURL(fileURLWithPath: filePath!)
print("録画開始 : \(filePath!)")
fileOutput?.startRecording(to: fileURL as URL, recordingDelegate: self)
// XXX : あとココでプレビュー表示とか…
}
このように、/Documents/
ディレクトリ配下に myvideo-20190101125900999.mp4
といった形式のファイル名で保存するように設定している。/Library/Caches/
や /tmp/
はシステムによる自動削除の危険があるので、/Documents/
ディレクトリを使用している。もしココで指定した名前のファイルが既に存在する場合は自動的に上書きされるので、ファイル名を固定にしてあえて重複させるようにしておけば、動画ファイルは常に最新の1つのみ保持するような作りにもできる。
この辺のコードは AVFoundation
で動画撮影する系の記事から借用しただけ。
また、// XXX
コメントで示しているとおり、このままだと動画の撮影状況を表示するためのプレビューがないままなので、そこは別途実装してやること。
録画停止用の関数は単に stopRecording()
を呼ぶだけ
録画停止は、fileOutput.stopRecording()
を呼ぶだけ。コレを呼ぶと、次に紹介する fileOutput()
メソッドが自動的に実行される、という関係。
/// 録画を停止する : ボタンからこの関数を呼び出してあげる
private func stopRecording() {
print("録画停止")
fileOutput?.stopRecording()
// XXX : あとココでプレビュー表示の取り消しとか…
}
前述のとおり、プレビュー表示に関するコードはないので、プレビュー表示用のレイヤーを非表示にするなどの処理はココでやると良いかと。
録画完了時に自動実行される fileOutput()
では何もしない
次に、AVCaptureFileOutputRecordingDelegate
を実装する際に必須となる fileOutput()
メソッドだが、この中では特に何もしない。
/// 録画完了時の処理 : オーバーライドしておくだけでココでは何もしない
///
/// - parameter output: AVCaptureFileOutput (アンダースコアは外部引数名を省略するもの・呼び出し元でも外部引数名を書かなくて呼び出せるようになる)
/// - parameter outputFileURL: URL (Option キーで参照できるドキュメントコメントを見ると内部引数名でコメントを書くっぽいので内部引数名を採用)
/// - parameter connections: AVCaptureConnection
/// - parameter error: Error
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
print("録画完了")
// XXX : もし録画完了時にフォトライブラリに書き出したければココで処理する
}
以下の文献では capture()
メソッドが必須、と紹介されているが、多分バージョン違いによるもの。
引数 outputFileURL
で、録画を終了させた動画ファイル名が分かるので、この場でフォトライブラリに書き出して、/Documents/
配下からはファイルを削除するよう実装しても良い。そのためのコードは後述する書き出し処理が参考になるだろう。
さて、録画開始時に /Documents/
配下に動画ファイルを保存するよう指定し、録画停止時には何もしない、とすることで、アプリ内に動画ファイルが溜まっていく作りになるのだ。このままではフォトライブラリにも書き出されない。
アプリ内に保存された動画ファイルをフォトライブラリに書き出す
さて、アプリ内に動画ファイルを溜め込めるようになったのは良いが、このままではアプリ内に溜まりっぱなしで取り出せない (Xcode から「Download Container」などして引き抜く、といったことはできるが…)。
そこで、アプリ内に保存されている mp4 ファイルをまとめてフォトライブラリ (= カメラロール) に書き出し、アプリ内からは動画ファイルを削除する処理を作る。それが以下の outputVideos()
だ。
/// アプリ内に保存した mp4 ファイルをフォトライブラリに書き出す : ボタンからこの関数を呼び出してあげる
private func outputVideos() {
// Documents ディレクトリの URL
let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
do {
// Documents ディレクトリ配下のファイル一覧を取得する
let contentUrls = try FileManager.default.contentsOfDirectory(at: documentDirectoryURL, includingPropertiesForKeys: nil)
for contentUrl in contentUrls {
// 拡張子で判定する
if contentUrl.pathExtension == "mp4" {
// mp4 ファイルならフォトライブラリに書き出す
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: contentUrl)
}) { (isCompleted, error) in
if isCompleted {
// フォトライブラリに書き出し成功
do {
try FileManager.default.removeItem(atPath: contentUrl.path)
print("フォトライブラリ書き出し・ファイル削除成功 : \(contentUrl.lastPathComponent)")
}
catch {
print("フォトライブラリ書き出し後のファイル削除失敗 : \(contentUrl.lastPathComponent)")
}
}
else {
print("フォトライブラリ書き出し失敗 : \(contentUrl.lastPathComponent)")
}
}
}
}
}
catch {
print("ファイル一覧取得エラー")
}
}
FileManager.default.contentsOfDirectory()
を使って、/Documents/
ディレクトリ配下からファイル一覧を取得する。そしてコレをループし、NSURL#pathExtension
で拡張子を参照する。録画開始時に指定したように、mp4
なファイルだったら書き出し対象として扱う。
フォトライブラリへの書き出しは PHAssetChangeRequest.creationRequestForAssetFromVideo()
とかいう関数を使う。書き出しに成功したら、FileManager.default.removeItem()
を使って、アプリ内に保存されているファイルを削除する。
この辺、try
とか do-catch
とかよく分かっていなくて、コードのネストが深くなってる。for
で回すのもイマイチな気がするのだが、フォトライブラリへの書き出しが非同期に行われてタイミングがおかしくなるので、書き出しの成功を isCompleted
で確認してからファイルを消すようにしている。もっと Swift 勉強しないとキレイなコードにならんち…。
しかしひとまずはコレで、アプリ内の動画ファイルをフォトライブラリに書き出した上で削除できるようになった。
コレについても、もう少し UI 面を加工していけば、アプリ内の動画ファイル一覧をカメラロールちっくに画面に表示し、選択した動画ファイルのみエクスポートする、みたいにも作れるだろう。
以上
動画ファイルの扱い方が全く分からなくて色々調べたが、随分すんなりと実装できた。
Swift らしいエレガントな書き方がまだ分かっておらず、コードの行数もかさむし、ネストが深くなりがち。!
と ?
の使い分けとかも定石を知らないので勉強しないと。
参考文献
今回実装するにあたって参照した文献。
- フォトライブラリへの書き出しについて
- iOS AVFoundation(AVCaptureMovieFileOutput)を使用したビデオ録画を作ってみた | DevelopersIO …
capture()
メソッドだがフォトライブラリへの書き出し方 - Swift4で動画を撮影・保存する - Qiita … こっちは
fileOutput()
で紹介されている
- iOS AVFoundation(AVCaptureMovieFileOutput)を使用したビデオ録画を作ってみた | DevelopersIO …
- 他のファイル形式でアプリ内にファイルを保存する手順など
- ファイルを出力してアプリ内に保存してみた@Swift3 - Goalist Developers Blog … テキストと View (PDF) の保存
- 【Xcode】データをアプリ内に保存するためのメモ2(画像編) | AS blind side … 画像ファイルの保存 (Objective-C)
- NSFileManagerでAPP保存領域のデータ操作(作成・保存・移動・削除・置換・コピー・ディレクトリ作成等) - Swiftサラリーマン … テキストファイルの保存
- 重複しないファイル名 - Qiita … ファイル名の重複を防ぐ連番の作り方
- 動画の保存に関するところ
- Swift 動画をエクスポート・保存する | Hahnah's Toybox … 動画の URL を指定してアプリ内に書き出し。なんか面倒臭い…。ただ、ファイルの上書きはできないらしいので消してから書き出している
- Swiftで動画ダウンロードして、シェアする方法 - Qiita… 動画の URL (HTTP ダウンロード) から
data
を得て、NSSearchPathForDirectoriesInDomains()
でDocumentDirectory
を指定し、data.write(to: URL)
で保存 - iphone - Save video with AVFoundation - Stack Overflow …
writeToFile()
というメソッド - save - swift video to document directory - Stack Overflow … Swift3。URL から Data を取り出し
write(fileURLWithPath)
で保存。削除時は FileManager (NSFileManager) を使っている - 【Swift4】URL先の画像をアプリ内に保存&ロードする - Qiita … 画像だが、保存する URL パスを作って
write()
。ファイルの存在チェックもしている - 今こそ復習したい、iOSアプリのディレクトリ構成 - Qiita … 保存先ディレクトリ。Documents は同期できる (設定すれば)。Caches・Tmp は自動削除される恐れが高い
- iTunesからiOSデバイスのDocumentsフォルダにアクセスするためには - Qiita … Documents を iTunes で確認できるようにする
plist
設定
- 保存後の再生関連 (アプリ内で再生するには)
- Xcode - 動画ファイルをアプリ内に保存、保存した動画を取り出すソースコードはどのようになりますでしょうか?|teratail
- Swift3.0 iOSでAVPlayerとAVPlayerViewControllerによる動画再生 - JoyPlotドキュメント … 動画再生は AVPlayer に URL 渡せばできるぽい。NSData (バイナリデータ) として
writeToFile()
する? - Swift3.0 パスとファイルURLの違いと相互変換の方法 - JoyPlotドキュメント … Swift におけるパス (String 型) とファイル URL (URL 型) の違い
- iosの動画再生周りの基礎を調べた - Qiita
- iOSで動画を再生する - Qiita
- iOS - Swift iOS ドキュメントディレクトリに保存されている動画の再生|teratail … ファイル再生時の URL 指定方法
- ファイル操作について
- swift - アプリ内で保存したファイルの一覧を取得したい - スタック・オーバーフロー … URL を使うのが主流だとか。
FileManager.default.contentsOfDirector()
でファイル名一覧が取れている - NSFileManagerでAPP保存領域のデータを操作する - Qiita
- swift - アプリ内で保存したファイルの一覧を取得したい - スタック・オーバーフロー … URL を使うのが主流だとか。