以前、動画を加工する記事をいくつか投稿した。

[iOS] 動画を加工・編集する(1) 指定した時間の範囲を切り出す
[iOS] 動画を加工・編集する(2) エラーコード-11841に気をつける
[iOS] 動画を加工・編集する(3) 動画に音を合成する
[iOS] 動画を加工・編集する(4) 動画をクリッピングする
[iOS] 動画を加工・編集する(5) 動画を拡大縮小する
[iOS] 動画を加工・編集する(6) 複数の動画を連結する

これらは「動画 → 動画」だったので、今回は「静止画 → 動画」を扱ってみる。

アニメーションというのは複数の静止画を連続で表示することで実現する。
そこで、今回は20枚の静止画からなる動画を生成してみる。
 
どのようなものかを簡単に説明しておこう。
image01 image09 image14
このように顔が左から右へと移動するというものだ。
この動きを20枚の静止画で実現する。

静止画から動画を生成する基本部分
まずはこのようなメソッドを用意しよう。
このメソッドは静止画から動画を生成する一連の流れを記述するもので、ここに少しずつ処理を足していって最終形にする。

1.今回使用する静止画は全て320x480なので、生成する動画も同様のサイズとする。

2.出力場所はDocumentsディレクトリとしておく

3.今回は出力ファイル名は固定とするので、出力前に同名のファイルがある場合は削除する。

AVAssetWriterの準備
ここまでは準備段階だ。
次からがいよいよ本番だ。
以下のコードをメソッドに付け足そう。

1.動画の出力先とファイル形式を指定する。
 今回はMPEG4を選択した。

2.出力する動画のビデオコーデックやサイズをNSDictionary形式で作成する。

3.入力の諸々を設定する。
 ここはおまじないのようなものと考えて良いだろう。

さて、ここで一つ気づくことがある。
Xcodeがエラーを幾つか吐いていることだろう。
これを解消するためにAVFoundationをインポートしよう。


出力処理開始
次は出力処理の開始部分を記述する。
以下のコードをメソッドに付け足そう。

1.後述するが、静止画は一旦ピクセルバッファに変換しなければならない。
 bufferはそれを入れておくためのものだ。

2.ソースとなる静止画はバンドルに置いてあるので、パスを用意する。

3.出力処理を開始する。

4.20枚の静止画を処理するためのループ

5.このautoreleasepoolはとても重要だ。
 ARCによる自動開放はスコープを抜けないと行われないので、これを忘れるとメモリを大量に消費してしまうことになる。
 今回のように枚数が少ない場合は問題になることは無いだろうが、数百枚の画像を使うような場合はメモリが足りなくなる可能性があるため、必ず忘れないようにしたい。

動画生成処理
それではループの内部処理を実装する。
以下のコードをメソッドに付け足そう。

1.ソースの画像を読み込む。
 ファイル名はimage01〜image20としている。

2.ソース画像をピクセルバッファに変換する。
 変換用のメソッドはまだ実装していないが、後ほど実装する。
 なお、このピクセルバッファは手動で解放しないとメモリリークを起こすので、忘れずに解放する。

3.ピクセルバッファ(画像)を追加する。
 ちなみに今回はフレームレートは30fpsとしている。
 全20枚なので、約0.66秒の動画になるはずだ。

4.先ほど述べたように、ピクセルバッファは忘れずに解放する。

5.ピクセルバッファの追加を失敗していたらループを抜ける。

これで内部処理は完了だ。

完了処理
最後に完了処理を行う。
これを忘れると動画が出力されないので注意したい。

1.セッションを終了させる。

2.完了を確認するためにデバッグ出力を行う。

これで基本の流れは完了だ。
あとは画像をピクセルバッファに変換するメソッドを実装すれば動くはずだ。

画像をピクセルバッファに変換する
この部分は結構複雑だ。
完全に理解する必要も無いかもしれないが、かいつまんで説明しておこう。

1.ピクセルバッファを作成するためのオプションを設定する。
 これもおまじないのようなものと考えておこう。

2.ピクセルバッファを作成する。
 これで、pxbufferにまっさらなピクセルバッファが作成される。

3.ピクセルバッファをロックする。
 第2引数のゼロはフラグで、ピクセルバッファを読み込み専用にしたい場合はCVPixelBufferLock_ReadOnlyを指定すれば出来るようだ。

4.ピクセルバッファのベースアドレスのポインタを返す。
 このポインタに対して画像の描画などの処理を行う。

5.カラースペースとコンテキストの作成を行う。
 CGBitmapContextCreate()については色々とわかりにくいことがある。
 第4引数の8はピクセルのRGBの各コンポーネントがそれぞれ何ビットで表されるかを示しているようだ。
 第5引数の4 * size.widthについては画像の1行(今回では320x1ピクセル)あたりのバイト数を表す。
 第6引数のkCGImageAlphaNoneSkipFirstはアルファチャンネルの扱いを指定するもののようだ。この場合は--------RRRRRRRRGGGGGGGGBBBBBBBBとなる。今回はpixelBufferFromCGImage()の中で画像を重ねたりはしないのでアルファチャンネルは必要ない。

6.画像をコンテキストに描画する。

7.カラースペースとコンテキストを解放する。

8.ピクセルバッファのロックを解除する。

9.ピクセルバッファを返す。
 ただし、これはメソッドを呼び出した側で解放する必要がある。

これで、エラーは無くなったはずだ。
早速実行してみよう。
avseetwriter_ss1
このように右に動くたびに前のフレームの情報が残ってしまっている。
これは元のpngが背景透過であるためだ。
なので背景を付けてみよう。

背景は単色の塗りつぶしでも構わないが、どうせなので画像を用意してみる。
動画を生成するforループを以下のように変更する。

1.背景画像を読み込む

2.各フレームの画像と背景を合成する。

3.合成した画像をピクセルバッファに変換する。

それでは実行して出力された動画を見てみよう。
avseetwriter_ss2
想定通りの動画が出力されているのを確認できた!

注意しなければならないこと
最後に注意しなければならないことが2つある。
私自身がハマってしまい、解決に多少の時間を要してしまったことだ。
せっかくなので、紹介しておこう。

writerのCompletionHandlerが呼ばれない
今回は出力が完了したら"write end"とコンソールに出力するようにコードを記述した。
この部分だ。
ところが、何故かこれが実行されないことがある。
本来はここで、次の処理に移るためのコードを記述したりするはずなので、これは由々しき問題だ。

調べてみるとstackoverlowで答えが見つかった。
avassetwriter - Completion handler is not called on iOS7 GM - Stack Overflow

今回のメソッドで使用しているAVAssetWriterインスタンスであるwriterがスコープを抜けて開放されてしまうことが原因のようだ。
なるほど、言われてみれば確かにその通りだ。

そこで、以下のようにインスタンス変数を定義しよう。

次は、このインスタンス変数にwriterを代入しておく。

これで問題なく動くようになったはずだ!

出力する動画の解像度はある程度大きくて、一般的によく使われるものにする
最初、この記事用に作ったサンプルコードでは出力する動画の解像度は240x320にしてあった。
しかし、これで出力された動画は、何故か再生すると画面がチラチラしてしまう。

色々と試してみた結果、320x480程度の大きさがないと正しい動画が出力されないようだ。
原因はよくわかっていない。

また、330x490等の変な解像度にすると、水平同期がズレたような映像になってしまう。
なので、出力する解像度は今回使った320x480や480x640みたいな一般的によく使われるものにするのが無難なようだ。