FFmpeg API の使い方(1): デコードしてみる

FFmpeg APIの使い方を解説してみます。FFmpegは、様々な動画の変換に対応したツールです。コマンドラインからバイナリを叩くのもいいですが、ライブラリAPIを使えば、もっと色々なことができるようになります。ただ、チュートリアル的なものがなかったり、あったとしても古いAPIで書かれていたりしていて、とっつきにくいところがあります。長年開発されてきたOSSなだけあって、APIとしては洗練されているので、分かってしまえば使い方は簡単です。

FFmpegは現時点で最新の3.3.3を使っていきます。開発環境はVisual Studio 2017です。使い方をざっくり説明しますが、各関数の詳細は公式のdoxygen等で確認してください。もっと他の使い方が知りたい場合は、公式のサンプルも参考にしてください。

使用するライブラリ

今回は、動画ファイルを始めから読んで、映像をデコードするだけのプログラムを作ってみます。FFmpegライブラリは、libavcodecやlibavformatなどの複数のモジュールに分かれています。今回の動画のデコードで使うのは、libavformat, libavcodec, libavutilの3つです。

libavformat

mp4やmkvなどのコンテナから、H264やAACなどの映像や音声ストリームを取り出したり(demux)、逆に映像・音声ストリームをコンテナに入れたり(mux)するライブラリ

libavcodec

H264やAACなどの映像・音声をデコードしたり、エンコードしたりするライブラリ

libavutil

必要な構造体の初期化やメモリ確保などの便利関数を集めたライブラリ

準備

まず、これらのライブラリを使う準備をします。

FFmpegはCのライブラリなので、C++から使うときはインクルードファイルをextern “C”指定します。

(2017/09/24追記) FFmpegのインクルードファイルやライブラリファイル(.lib)へのパスを設定する必要があります。この記事ではAPIの使い方を書きたいので、環境構築方法は書きません。ビルド済みバイナリをどこかから探してくるか、公式のドキュメントや他のサイトを参考にFFmpegをビルドするかして、ライブラリファイル(.lib)とインクルードファイルを入手してください。その上でパスを設定して、上記のコードがコンパイルできるようにしてください。

次にライブラリ初期化みたいなおまじない。

これはプログラム開始時などに1回呼んでやればいいです。これをやらないと、コーデックやフォーマットが登録されないので、未対応フォーマットってエラーが返ってきたりします。

これで準備は終わり。

ファイルを開く

いよいよファイルを開いてみます。まず、libavformatでファイルを開きます。今回は適当にiPhoneで撮影した動画ファイルを開いています、

今回書くサンプルでは、エラー発生時は簡単のためprintfしてるだけですが、実際のプログラムでは適切なエラー処理をしてください。

次に、動画ファイルに入ってるストリームの情報を取得します。

ここで取得されるのは、”ffprobe hoge.mov”と叩いたときに出てくる以下のような情報です。

ストリームの数がformat_context->nb_streams、各ストリームがformat_context->streamsからアクセスできるようになります。

今回は映像をデコードしたいので、映像ストリームを探します。

デコーダ作成

ここからavcodecでデコーダを作っていきます。まず、ストリーム情報のcodec_idからコーデックを探します。

次にコーデックコンテキストをalloc

ストリーム情報からコーデックパラメータをコピーしてオープンします。

デコード

これでデコードの準備が整いました。では、デコードしてみます。

av_read_frameでコンテナから取り出したストリームをAVPacketで受け取ります。packet.stream_indexでどのストリームのパケットかを見て、映像ストリームのパケットだったら、avcodec_send_packetで、デコーダに入力します。デコードされたフレームはavcodec_receive_frameで受け取ります。on_frame_decodedは自分で用意した関数です。ここでデコードされたフレームを使った処理を書きます。

AVFrameやAVPacketのデータは参照カウンタでメモリ管理されています。上のコードで、av_read_frameでpacketを受け取ると、参照を保持した状態になるので、使い終わったらav_packet_unrefで解放してあげます。frameはunrefを呼んでいませんが、これは少しカラクリがあります。frameも同じようにavcodec_receive_frameで受け取ると、参照を保持した状態になりますが、次にavcodec_receive_frameに参照を保持した状態のframeを渡すと、中でav_frame_unrefを呼んでくれるので、ループ内でunrefする必要がないのです。

av_read_frameは映像ストリームの場合、1回の呼び出しで1フレーム分のデータが返ってきます。iPhoneで撮影した動画は、IフレームとPフレームしか使われていないようなので、av_read_frameで取り出したパケットをavcodec_send_packetでデコーダに渡すと、必ず1パケットにつき1枚avcodec_receive_frameでフレームが取得されました。ですが、一般的に、Bフレームも使った動画ですと、最初は、数フレーム分のパケットをデコーダに渡して初めてデコードされたフレームを受け取れるようになります。

ファイルの最後に到達するとav_read_frameは負の値を返すので、それでループを抜けます。動画ファイルのパケットは全て渡しても、デコーダの中にフレームが残っている可能性があるので、デコーダをflushします。avcodec_send_packetにnullptrを渡せばflushになります。

終了処理

これで全ての映像がデコードされました。後は、フレームやコンテキストを解放して終わりです。

ここまでのコードをまとめたものを貼っておきます。

 

Comments

  1. By 徳永

    返信

    • 返信

コメントを残す

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