このブログは、株式会社フィックスターズのエンジニアが、あらゆるテーマについて自由に書いているブログです。
FFmpeg APIの使い方を解説してみます。FFmpegは、様々な動画の変換に対応したツールです。コマンドラインからバイナリを叩くのもいいですが、ライブラリAPIを使えば、もっと色々なことができるようになります。ただ、チュートリアル的なものがなかったり、あったとしても古いAPIで書かれていたりしていて、とっつきにくいところがあります。長年開発されてきたOSSなだけあって、APIとしては洗練されているので、分かってしまえば使い方は簡単です。
他の記事
FFmpegは現時点で最新の3.3.3を使っていきます。開発環境はVisual Studio 2017です。使い方をざっくり説明しますが、各関数の詳細は公式のdoxygen等で確認してください。もっと他の使い方が知りたい場合は、公式のサンプルも参考にしてください。
今回は、動画ファイルを始めから読んで、映像をデコードするだけのプログラムを作ってみます。FFmpegライブラリは、libavcodecやlibavformatなどの複数のモジュールに分かれています。今回の動画のデコードで使うのは、libavformat, libavcodec, libavutilの3つです。
mp4やmkvなどのコンテナから、H264やAACなどの映像や音声ストリームを取り出したり(demux)、逆に映像・音声ストリームをコンテナに入れたり(mux)するライブラリ
H264やAACなどの映像・音声をデコードしたり、エンコードしたりするライブラリ
必要な構造体の初期化やメモリ確保などの便利関数を集めたライブラリ
まず、これらのライブラリを使う準備をします。
// FFmpeg library
extern "C" {
#include <libavutil/imgutils.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}
#pragma comment(lib, "avutil.lib")
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avformat.lib")
FFmpegはCのライブラリなので、C++から使うときはインクルードファイルをextern “C”指定します。
(2017/09/24追記) FFmpegのインクルードファイルやライブラリファイル(.lib)へのパスを設定する必要があります。この記事ではAPIの使い方を書きたいので、環境構築方法は書きません。ビルド済みバイナリをどこかから探してくるか、公式のドキュメントや他のサイトを参考にFFmpegをビルドするかして、ライブラリファイル(.lib)とインクルードファイルを入手してください。その上でパスを設定して、上記のコードがコンパイルできるようにしてください。
次にライブラリ初期化みたいなおまじない。
av_register_all();
これはプログラム開始時などに1回呼んでやればいいです。これをやらないと、コーデックやフォーマットが登録されないので、未対応フォーマットってエラーが返ってきたりします。
これで準備は終わり。
いよいよファイルを開いてみます。まず、libavformatでファイルを開きます。今回は適当にiPhoneで撮影した動画ファイルを開いています、
// open file
const char* input_path = "hoge.mov";
AVFormatContext* format_context = nullptr;
if (avformat_open_input(&format_context, input_path, nullptr, nullptr) != 0) {
printf("avformat_open_input failed\n");
}
今回書くサンプルでは、エラー発生時は簡単のためprintfしてるだけですが、実際のプログラムでは適切なエラー処理をしてください。
次に、動画ファイルに入ってるストリームの情報を取得します。
// get stream info
if (avformat_find_stream_info(format_context, nullptr) < 0) {
printf("avformat_find_stream_info failed\n");
}
ここで取得されるのは、”ffprobe hoge.mov”と叩いたときに出てくる以下のような情報です。
Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709), 1920x1080, 9938 kb/s, 29.98 fps, 29.97 tbr, 600 tbn, 1200 tbc (default)
Metadata:
creation_time : 2017-08-15T01:50:38.000000Z
handler_name : Core Media Data Handler
encoder : H.264
Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, mono, fltp, 63 kb/s (default)
Metadata:
creation_time : 2017-08-15T01:50:38.000000Z
handler_name : Core Media Data Handler
Stream #0:2(und): Data: none (mebx / 0x7862656D), 0 kb/s (default)
Metadata:
creation_time : 2017-08-15T01:50:38.000000Z
handler_name : Core Media Data Handler
Stream #0:3(und): Data: none (mebx / 0x7862656D), 0 kb/s (default)
Metadata:
creation_time : 2017-08-15T01:50:38.000000Z
handler_name : Core Media Data Handler
ストリームの数がformat_context->nb_streams、各ストリームがformat_context->streamsからアクセスできるようになります。
今回は映像をデコードしたいので、映像ストリームを探します。
// find video stream
AVStream* video_stream = nullptr;
for (int i = 0; i < (int)format_context->nb_streams; ++i) {
if (format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream = format_context->streams[i];
break;
}
}
if (video_stream == nullptr) {
printf("No video stream ...\n");
}
ここからavcodecでデコーダを作っていきます。まず、ストリーム情報のcodec_idからコーデックを探します。
// find decoder
AVCodec* codec = avcodec_find_decoder(video_stream->codecpar->codec_id);
if (codec == nullptr) {
printf("No supported decoder ...\n");
}
次にコーデックコンテキストをalloc
// alloc codec context
AVCodecContext* codec_context = avcodec_alloc_context3(codec);
if (codec_context == nullptr) {
printf("avcodec_alloc_context3 failed\n");
}
ストリーム情報からコーデックパラメータをコピーしてオープンします。
// open codec
if (avcodec_parameters_to_context(codec_context, video_stream->codecpar) < 0) {
printf("avcodec_parameters_to_context failed\n");
}
if (avcodec_open2(codec_context, codec, nullptr) != 0) {
printf("avcodec_open2 failed\n");
}
これでデコードの準備が整いました。では、デコードしてみます。
// decode frames
AVFrame* frame = av_frame_alloc();
AVPacket packet = AVPacket();
while (av_read_frame(format_context, &packet) == 0) {
if (packet.stream_index == video_stream->index) {
if (avcodec_send_packet(codec_context, &packet) != 0) {
printf("avcodec_send_packet failed\n");
}
while (avcodec_receive_frame(codec_context, frame) == 0) {
on_frame_decoded(frame);
}
}
av_packet_unref(&packet);
}
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になります。
// flush decoder
if (avcodec_send_packet(codec_context, nullptr) != 0) {
printf("avcodec_send_packet failed");
}
while (avcodec_receive_frame(codec_context, frame) == 0) {
on_frame_decoded(frame);
}
これで全ての映像がデコードされました。後は、フレームやコンテキストを解放して終わりです。
// clean up
av_frame_free(&frame);
avcodec_free_context(&codec_context);
avformat_close_input(&format_context);
ここまでのコードをまとめたものを貼っておきます。
#define __STDC_CONSTANT_MACROS
#define __STDC_LIMIT_MACROS
#include <stdio.h>
extern "C" {
#include <libavutil/imgutils.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}
#pragma comment(lib, "avutil.lib")
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avformat.lib")
void on_frame_decoded(AVFrame* frame) {
printf("Frame decoded PTS: %jd\n", frame->pts);
}
int main_(int argc, char* argv[])
{
av_register_all();
const char* input_path = "hoge.mov";
AVFormatContext* format_context = nullptr;
if (avformat_open_input(&format_context, input_path, nullptr, nullptr) != 0) {
printf("avformat_open_input failed\n");
}
if (avformat_find_stream_info(format_context, nullptr) < 0) {
printf("avformat_find_stream_info failed\n");
}
AVStream* video_stream = nullptr;
for (int i = 0; i < (int)format_context->nb_streams; ++i) {
if (format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream = format_context->streams[i];
break;
}
}
if (video_stream == nullptr) {
printf("No video stream ...\n");
}
AVCodec* codec = avcodec_find_decoder(video_stream->codecpar->codec_id);
if (codec == nullptr) {
printf("No supported decoder ...\n");
}
AVCodecContext* codec_context = avcodec_alloc_context3(codec);
if (codec_context == nullptr) {
printf("avcodec_alloc_context3 failed\n");
}
if (avcodec_parameters_to_context(codec_context, video_stream->codecpar) < 0) {
printf("avcodec_parameters_to_context failed\n");
}
if (avcodec_open2(codec_context, codec, nullptr) != 0) {
printf("avcodec_open2 failed\n");
}
AVFrame* frame = av_frame_alloc();
AVPacket packet = AVPacket();
while (av_read_frame(format_context, &packet) == 0) {
if (packet.stream_index == video_stream->index) {
if (avcodec_send_packet(codec_context, &packet) != 0) {
printf("avcodec_send_packet failed\n");
}
while (avcodec_receive_frame(codec_context, frame) == 0) {
on_frame_decoded(frame);
}
}
av_packet_unref(&packet);
}
// flush decoder
if (avcodec_send_packet(codec_context, nullptr) != 0) {
printf("avcodec_send_packet failed");
}
while (avcodec_receive_frame(codec_context, frame) == 0) {
on_frame_decoded(frame);
}
av_frame_free(&frame);
avcodec_free_context(&codec_context);
avformat_close_input(&format_context);
return 0;
}
初めまして。Koji様。
業務で動画を扱うことになりffmpegを使用してみたいと思いこのページにたどり着きました。
VisualStudioでソースをコンパイルしようと試みたのですが、コンパイルが通りません。
ffmpegのHPからソースをダウンロードしてインクルードパスに追加してみたりとやってみたのですが。。。
できれば開発環境の構築方法を教えてもらえないでしょうか?
以上、よろしくお願いします。