FFmpeg APIの使い方(2): シークやAVFrameなど

2017年9月5日

前回は、ただデコードしただけでしたが、今回はもう少しいろいろやってみます。

シーク

ファイルの途中から再生したり、スキップしたりする場合、av_seek_frameでシークします。

// seek
if (av_seek_frame(format_context, video_stream->index,
    dst_time, AVSEEK_FLAG_BACKWARD) < 0) {
  printf("av_seek_frame failed\n");
}

シークは少しコツがあります。av_seek_frameは以下のように定義されています。

int av_seek_frame(AVFormatContext* s, int stream_index, int64_t timestamp, int flags);

timestampはフレーム表示時刻のタイムスタンプ(PTS, Presentation Time Stamp)で、stream_indexで指定したストリームの時刻で指定します。ドキュメントには、「timestampで指定されたキーフレームにシークする」と説明があります。av_seek_frameは、flagsに何も指定しないと、timestampで指定した時刻より後の最初のキーフレームにseekするようです。例えば、キーフレームのタイムスタンプが1200と1800だった場合、1500を指定してシークすると、1800にシークされます。これだとタイムスタンプ1500のフレームを取得できません。flagsにAVSEEK_FLAG_BACKWARDを指定すると、指定したタイムスタンプより前のキーフレームにシークしてくれます。先程の例の場合は1200にシークしてくれるので、そこからデコードしていけば1500のフレームも取得できます。

ただし、キーフレームへのシークがサポートされていないフォーマットもあります。また、私が試した限りでは、MPEG2-TSでは、タイムスタンプ指定のシークもうまくできないようでした。そのような場合は、AVSEEK_FLAG_BYTEを指定して、ファイルのバイト数で目的の位置を指定すれば、シークできます。

av_seek_frame(format_context, video_stream->index, file_offset, AVSEEK_FLAG_BYTE);

AVSEEK_FLAG_BYTEを指定した場合は、3番目の引数はタイムスタンプではなく、ファイルの先頭からのバイト数になります。

ただし、これだと目的のフレームをデコードするには、ファイルの何バイト目からデコードすればいいかが分かっていないとシークできません。解決方法としては、

  • バイナリサーチで目的のフレームを探索する
  • あらかじめファイルを全部読んで、キーフレームがどこにあったかなどのインデックスを作成しておく

のいずれかになると思います。L-SMASH WorksやAvisynthのFFmpegSourceは、後者の方法を取っています。

av_seek_frameでファイルをシークしたら、パケットをデコーダに入れる前に、デコーダをリセットする必要があります。av_seek_frameはAVFormatContextに対して呼び出すので、AVFormatContextと何の関係もないAVCodecContextは、シークされたことを知らないからです。そのままシーク後のパケットを渡すと、壊れたフレームが返ってきます。

avcodec_flush_buffers(codec_context);

これで、リセットできます。ただ、AVCodecContextを一旦削除して作り直したほうが安全のようです。

また、シークした後は、必ずキーフレームからしかデコードできませんが、ファイルによっては、なぜか、キーフレームでないフレームが最初にデコードされて、avcodec_receive_frameで返ってくることがあります。このフレームは壊れているので、キーフレーム前のフレームは捨てるようにした方が安全です。(バグなのか仕様なのか不明)

// frame counter
int count = 0;
...
while (avcodec_receive_frame(codec_context, frame) == 0) {
  if(count == 0 || frame->keyframe) {
    on_frame_decoded(frame);
    ++count;
  }
}
...

QSVデコード

デフォルトでは、ソフトウェアデコーダが使われるところを、HWデコーダを使ってみましょう。Quick Sync Video (QSV) は、Sandy Bridge以降のIntel CPUに搭載されているHWデコーダ・エンコーダです。これを使ってみましょう。前回のデコードサンプルで、AVCodecを探しているところ

AVCodec* codec = avcodec_find_decoder(video_stream->codecpar->codec_id);

これを、以下のように書き換えるとH264ではQSVを使ってくれるようになります。

// find codec
AVCodec* codec = nullptr;
if(video_stream->codecpar->codec_id == AV_CODEC_ID_H264) {
  codec = avcodec_find_decoder_by_name("h264_qsv");
}
if(codec == nullptr) {
  codec = avcodec_find_decoder(video_stream->codecpar->codec_id);
}

ただし、QSVでデコードすると、通常のソフトウェアデコーダでは、avcodec_receive_frameでYV12フォーマットのフレームが返ってくるところが、QSVデコーダを使うとNV12で返ってくるので、そこは注意が必要です。

他のHWデコーダもavcodec_find_decoder_by_nameの引数の名前を変えるだけで対応できるはずです。

AVFrameのあれこれ

参照を増やす

ここまでのコードのデコードループにおいて、デコードされたフレームframeは、on_frame_decodedでしかアクセスできませんでした。

// decode loop
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);
}

on_frame_decodedを抜けるとすぐに次のavcodec_receive_frameでframeが解放されてしまいます。フレームをバッファリングしたり、別スレッドに渡したりしたい場合は、すぐに解放されては困ります。フレームデータの生存管理は参照カウンタで行っているので、そういう場合はav_frame_refで新たに参照を作ってやります。例えば、フレームを溜め込む場合は、以下のようにすればできます。

// frame buffer
std::vector<AVFrame*> frame_buffer;

void on_frame_decoded(AVFrame* frame) {
  AVFrame* new_ref = av_frame_alloc();
  av_frame_ref(new_ref, frame);
  frame_buffer.push_back(new_ref);
}

AVFrameの解放は、データへの参照を持っている場合でも、持っていない場合でも、av_frame_freeを呼び出せばOKです。データへの参照を持っていた場合でも、よしなに処理してくれます。

画像データへのアクセス

frame->data, frame->linesizeでフレームの画像データにアクセスできます。ただし、画像のフォーマットが分からないと、dataに何が入っているのかが分かりません。frame->formatにフォーマット識別の番号がありますが、これだけでは、様々なフォーマットに対応しようとしたときに大変なので、フォーマット情報を取得します。

const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get((AVPixelFormat)(frame->format));

AVPixFmtDescriptorにプレーン数や、chromaの縦横サイズ、ビット深度などの情報が入っています。

同じプロパティの空フレーム作成

画像データをいじってフレームを追加したり、変更したりしたい場合、同じフォーマット、サイズのフレームを作りたいことがあると思います。以下のようなコードで同じフォーマット、サイズ、プロパティを持つ空フレームを作成できます。

// alloc new frame, that has the same format, size and properties as src
AVFrame* get_blank_frame(AVFrame* src) {
  AVFrame* ret = av_frame_alloc();
  
  // フレームのプロパティをコピー
  av_frame_copy_props(ret, src);

  // メモリサイズに関する情報をコピー
  ret->format = src->format;
  ret->width = src->width;
  ret->height = src->height;

  // メモリ確保
  if (av_frame_get_buffer(ret, 32) != 0) {
    printf("av_frame_get_buffer failed\n");
  }

  return ret;
}

 

Tags

About Author

Koji Ueno

Leave a Comment

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

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

Recent Comments

Social Media