FFmpeg APIの使い方(3): エンコードしてみる

今回は、エンコードしてみます。設定する項目が増えるので少し大変です。

まず、エンコードするフレームを用意しましょう。前々回作ったデコードプログラムを少し変更して、フレームを用意します。

前々回のデコードプログラムのmain関数を別の名前で関数化して、

on_frame_decodedを修正して、フレームを溜め込むようにします。

これで、全てのフレームを溜め込むようになりました。全て溜め込んだ後、これらのフレームをエンコードするようにします。サンプルなので、簡単のため「全フレームをデコード」→「全フレームをエンコード」という処理の流れにしますが、これだとすぐにメモリが溢れてしまうので、実際のプログラムでは、デコードしながらエンコードするようにしてください。

framesにフレームは全て集まりましたが、これだけだと、各フレームのタイムスタンプの単位が分かりません。video_streamのtime_baseも記憶するようにします。映像ストリームを探した後で、video_stream->time_baseをコピーしておけばOKです。

これで、エンコードするフレームに関する必要なデータは集まりました。エンコード処理を書いていきます。

まずは、書き込むファイルを開きます。

muxerをallocします。

mp4で出力したいので、3番目の引数format_nameに”mp4″を渡しています。

format_contextに先程開いた出力ファイルのio_contextをセットします。

次に、エンコーダを作っていきます。まず、コーデックを見つけてきます。

今回は、H264でエンコードしたいので、H264のコーデックを探してきました。

このコーデックでコーデックコンテキストをallocします。

デコードでは、ファイルから必要なパラメータを読み込んでくれるので、プログラムからパラメータを設定する必要はなかったのですが、エンコードでは、いくつかパラメータをセットする必要があります。まず、映像のフォーマット等をcodec_contextに設定します。

どのフレームの同じはずなので、最初のフレーム1枚参照して、値をセットしています。

デコードで取得したtime_baseも設定します。

フォーマットによっては必要なので以下のおまじないも書いてください。

エンコードのパラメータは、AVDictionaryで指定します。

ffmpegのコマンドライン引数で”-preset medium -crf 22 -profile:v high -level 4.0″と指定したときと同じになるようにしました。

これで必要なパラメータが設定できたので、コーデックをopenします。

これでエンコードはできるようになりましたが、まだ、エンコードされたH264ストリームを入れるストリームがありません。format_contextに新しくストリームを追加します。

codec_contextから必要なパラメータをコピーします。

time_baseはavcodec_open2で変わっているかもしれないので、codec_contextの値をコピーします。

ストリームのパラメータもセットできたので、セットップの最後にavformat_write_headerを呼び出します。

これで準備はできたので、エンコードして行きます。

PTSは入力フレームのptsをそのまま使いたいところですが、time_baseが変わっているかもしれないので、av_rescale_qでtime_baseの差を反映させます。key_frameとpict_typeをリセットしていますが、そのままだとエンコーダへのヒントとして使われてしまうので、自動判定させるためにリセットしています。他にもデコーダで設定された値が使われてしまう可能性はあるので、AVFrameを作り直して、必要な値だけセットした方が良いかもしれません。avcodec_receive_packetで受け取ったパケットのタイムスタンプを、av_packet_rescale_tsで修正していますが、これも、avformat_write_headerでstream->time_baseが変更されているかもしれないので、codec_context->time_baseとの差を反映させています。また、エンコーダから受け取ったパケットにstream_indexを設定するのは、呼び出し側の仕事です。ここでは、ストリームは1つしかないので、0を設定しています。デコード時と違って、av_packet_unrefを呼び出していませんが、これはav_interleaved_write_frameがパケットの所有権を奪うので、呼び出し側では必要ありません。

フレームを全てエンコーダに渡したら、エンコーダをflushします。avcodec_send_frameにnullptrを渡せばflushになります。

エンコードする前に、avformat_write_headerを呼び出しましたが、エンコードが終わったら、av_write_trailerを呼び出します。

これでほぼ完了です。コンテキストを解放、ファイルを閉じます。

なぜstreamのtime_baseを使うのか

デコード時にtime_baseはvideo_stream (AVStream)から取得しました。しかし、エンコード時はcodec_context (AVCodecContext) に設定しました(その後、streamに波及させてはいます)。video_streamはformat_contextの一部なので、コンテナ(mp4やmkvやmpeg2-tsなどのストリームの入れ物となるフォーマットをコンテナと言います)のパラメータです。codec_contextはエンコーダ・デコーダです。time_baseは、AVStreamにも、AVCodecContextにもあります。コンテナから取得したtime_baseをエンコーダにセットするのは不思議に思うかもしれません。なぜ、デコーダから取得しないのか?コンテナから取得したのだから、コンテナに設定すべきではないか?ということです。

これは、デコードとエンコードでの動作の違いによるものです。通常、フレームのタイムスタンプはコンテナで定義されます。なので、デコードされたフレームのタイムスタンプはコンテナのストリーム(AVStream)のtime_baseが単位になっています。codec_contextのtime_baseではありません。デコード時はcodec_contextのtime_baseは使われないのです。しかし、エンコードするときは、エンコーダがtime_baseを必要とするので、codec_contextにこれを設定するのは必須となっています。なので、デコード時にstreamから取得したタイムスタンプを、エンコード時はcodec_contextに設定するのです。

また、デコード時はコンテナのtime_baseしか存在しなかったのが、エンコード時は、エンコーダのtime_baseとコンテナのtime_baseの2つが存在することになります(しかも違う値で)。フレームやパケットをエンコーダやmuxerに流すときにタイムスタンプの変換が必要になったのはこのためです。

最後に、エンコードで使ったコード全文を貼っておきます。

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です