FFmpegはべつに1-frame-in-1-frame-outじゃない

どうもFFmpegも1-frame-in-1-frame-outに縛られたものじゃないかとの誤解があったのでサンプルを交えて誤解を解いてみる。サンプルはFFmpeg APIで、さまざまな動画を操る - 前編 (1/5):CodeZine(コードジン)から拝借した。

#include <libavcodec/avformat.h>
#include <libavcodec/avdevice.h>

/* FFmpegの初期化 */
avcodec_register_all();
avdevice_register_all();
av_register_all();

まあこれはお約束。

/* ファイルのヘッダを読み、フォーマットを得る */
AVFormatContext *formatCtx;
ret = av_open_input_file(&formatCtx, filename, NULL, 0, NULL);
if(ret != 0) error("can't open input file.");

/* ファイルの中身からストリーム情報を得る */
ret = av_find_stream_info(formatCtx);
if(ret < 0) error("can't find stream info.");

ここでメディアフォーマットを判別してその中にある個別のストリームの情報を得る。得られた情報はすべてAVFormatContextに保存される。formatCtx->streamsにAVStreamとして各ストリームの情報があるのでこれを見て映像ストリームを探す。

/* ビデオストリームを探す */
int streamIndex = -1;
for(i = 0; i < formatCtx->nb_streams; i++){
    if(formatCtx->streams[i]->codec->codec_type == CODEC_TYPE_VIDEO){
        /* ビデオストリームを発見 */
        streamIndex = i;
        break;
    }
}
if(streamIndex < 0) error("can't find video stream.");
AVCodecContext *codecCtx = formatCtx->streams[streamIndex]->codec;

ストリーム情報の中にコーデックの情報がAVCodecContextとしてあるので後のために取り出しておく。

/* codecを探す */
AVCodec *codec = avcodec_find_decoder(codecCtx->codec_id);
if(codec == NULL) error("can't find codec(decoder).");

/* codecを開く */
ret = avcodec_open(codecCtx, codec);
if(ret != 0) error("can't open codec(decoder).");

デコーダを探して適切なパラメータ(AVCodecContextにすでに設定済み)で初期化する。

/* ファイルからパケットを読み込む */
AVPacket packet;
while(av_read_frame(formatCtx, &packet) >= 0){
    /* 動画ストリーム以外は飛ばす */
    if(packet.stream_index != streamIndex) goto LAST;

    /* データを受け取るフレームの作成 */
    AVFrame *frame    = avcodec_alloc_frame();
    if(frame == NULL) error("can't allocate a frame to store data.");

    /* パケットからフレームを復号する  */
    avcodec_decode_video(codecCtx, frame, &isFinish,
                         packet.data, packet.size);

    /* 復号がまだの場合は次のパケットまで処理を飛ばす */
    if(! isFinish) goto LAST;

    /*
     frameに関する処理
     ex.)画面に表示する
     */

LAST:
    /* パケットを解放 */
    av_free_packet(&packet);
}
/* コーデックを閉じる */
avcodec_close(codecCtx);
/* ファイルを閉じる */
av_close_input_file(formatCtx);
/* 確保したメモリを解放する */
av_freep(&frame);

これがデコードのメインループになる。ストリームからパケット(http://cekirdek.pardus.org.tr/~ismail/ffmpeg-docs/structAVPacket.html)というストリームを小分けにし1フレーム分のデータにしたものを受け取ってavcodec_decode_video()でデコードしてフレーム(AVFrame)にする。この際、isFinishが0になる可能性もあることに注意。isFinishが常に0でないなら1-frame-in-1-frame-outといえるが0の場合がありえるので1-frame-in-1-frame-outというわけではない。
またAVFrameには以下のようなフィールドがある。

    int64_t pts;
    int coded_picture_number;
    int display_picture_number;

ptsはまんまPTS*1であるしcoded_picture_numberとdisplay_picture_numberが違う値をとることは別に問題ない。


デコードに関してだけではなくエンコードについても書いておくとフレームをavcodec_encode_video()に渡してビットストリームを得てそれをパケットにつめるという作業になる。このときでも常にビットストリームが得られるとは限らない。フレームを渡してもサイズ0のビットストリームが返ってくることがある。

/* フレームを符合化する */
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.");
}

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;

このようにFFmpegおよびその中のライブラリlibavcodeclibavformatは1-frame-in-1-frame-outに縛られていはいない。

*1:Presentation Time Stamp