コーデック、ファイルを開き、書き込み準備をする
変数の設定がすべて終わったので、次はコーデックとファイルを開きます。コードは次のようになります。
/* codecを開く */ ret = avcodec_open(codecCtx, codec); if(ret != 0) error("can't open codec(encoder)."); /* 出力ファイルを開く */ if (! ( format->flags & AVFMT_NOFILE )) { ret = url_fopen(&formatCtx->pb, formatCtx->filename, URL_WRONLY); if (ret < 0) error("can't open output file."); }
avcodec_openについては、読み込みの時に使った物と同じ関数です。この関数でエンコーダも開くことができます。
続く出力先のファイルを開く処理では、url_fopen関数を利用しています。この関数はlibavformat/avio.hで宣言されています。開いたファイルの情報はAVFormatContext構造体内に保管されます。
"image2"フォーマットのflagsフィールドにはAVFMT_NOFILEフラグが立っているので、url_fopenは実際には飛ばされます。
ヘッダを書き込む
動画データを書き出し始める前に、ファイルのヘッダ部を書き込みます。この処理は、libavformat/avformat.hで宣言されているav_write_header関数を呼ぶだけです。
/* ファイルへ、ヘッダの書き込み */
av_write_header(formatCtx);
パケットを符号化し、書き込む
ここから、いよいよパケットの書き出しループとなります。この部分のコードの概要は、次のようになります。
/* エンコードに利用するバッファを用意する */ int buf_size = codecCtx->width * codecCtx->height * 4; uint8_t *buf = av_malloc(buf_size); /* ファイルにコンテンツを書き込む */ while(1){ /* コーデックでデータを符号化 */ /* 符号化したデータをパケットに詰め込む */ /* パケットを書き出す */ }
ループに入る前に、符号化したデータを入れるバッファを用意しています。1フレームのデータの大きさは、圧縮しない状態では最大で「縦×横×4 byte」となるので、想定される最大値を用意しています。4というのは、赤、緑、青、不透明度、の4データです。
バッファが用意できたらループに入ります。書き出すフレームがなくなるまで、延々とループさせます。
コーデックでデータを符号化
まず、対象フレームを符号化します。符号化の結果は、先ほど用意したバッファに保存します。
/* フレームを符合化する */ int out_size = avcodec_encode_video ( codecCtx, buf, buf_size, frame ); /* 符号化がまだの場合は、次のフレームへ */ if (out_size == 0){ continue; } else if (out_size < 0){ error("can't encode frame."); }
読み込みにはavcodec_decode_videoを利用しましたが、書き込みではavcodec_encode_videoを利用します。読み込みのときと同様の理由で、フレームを渡しても符号化が先送りにされてバッファにデータが入って来ない場合があるので、データが来ない場合は次にフレームに処理を移しています。
符号化したデータをパケットに詰め込む
パケットデータを作成するコードは次です。
/* 書き込むパケットを準備 */ AVPacket packet; av_init_packet(&packet); packet.stream_index= stream->index; packet.data= buf; packet.size= out_size; /* codecのtime baseによるptsをstreamのtime baseによるものに修正 */ packet.pts= av_rescale_q( codecCtx->coded_frame->pts, codecCtx->time_base, stream->time_base ); if(codecCtx->coded_frame->key_frame) packet.flags |= PKT_FLAG_KEY;
まず、libavformat/avformat.hで宣言されているav_init_packet関数を利用し、AVPacket構造体を初期化します。その後、packetに対してstream_index(書き込み先のストリームID。ここでは最初に開いたストリーム)、data(書き込むデータ。ここでは符号化したデータ)、size(書き込みバイト数)をセットしています。
その後、PTSとキーフレームフラグを指定しています。
まず、PTS(Presentation Time Stamp)の指定部分です。PTSとは、そのフレームを画面に表示するタイミングを指定するための値ですが、これをパケットに指定する必要があります。このとき、AVFrameが持っているPTSの時間単位(time_base)と書き出そうとしているストリームの時間単位が異なることがありますので、これを単位変換しなければなりません。
time_baseフィールドはAVRational構造体となっていますが、これは分数(有理数)を表す構造体です。av_rescale_qはlibavutil/mathematics.hで宣言される関数で、64ビット整数とAVRationalに対して単純にA×B÷Cという算術演算を行います。
PTSと時間単位の関係は、次の等式で表されるはずです。
- フレームのPTS × コーデックの時間単位 = パケットのPTS × ストリームの時間単位
よって、これを変形すると、
- パケットのPTS = フレームのPTS × コーデックの時間単位 ÷ ストリームの時間単位
となります。コードを見てみると、確かにこのとおりの計算を行っています。
次にセットしているキーフレームフラグとは、文字通りキーフレームであることを示すフラグです。キーフレームとは、MPEG圧縮で言えばIフレームのことで、復号に他のフレームを必要としないフレームのことです。フレームがキーフレームの場合は、このフラグをパケットにも立てておきます。
パケットを書き出す
最後にこのパケットを書き出します。読み込みにはav_read_frameを使いましたが、書き込みにはav_interleaved_write_frameを使います。av_write_frameという関数もありますが、av_interleaved_write_frameだとパケットの書き出し順が正しくなるのを保証してくれるとのことですので、こちらの方が安心でしょう。
/* パケットを書き込む */ int ret = av_interleaved_write_frame(formatCtx, &packet); if(ret != 0) error("can't write frame."); /* 1フレームだけ書き込めればいいので、終了 */ break;
なお、今回は先頭フレームを書き出すだけですので、パケットを1つ書き込んだらこれ以上処理を続ける必要がないのでbreakでループを抜けることにしました。通常の動画を書き出す場合は、ここまでのループを、書き出すフレームがなくなるまで続けることになります。
