このブログは、株式会社フィックスターズのエンジニアが、あらゆるテーマについて自由に書いているブログです。
前回は、ただデコードしただけでしたが、今回はもう少しいろいろやってみます。
ファイルの途中から再生したり、スキップしたりする場合、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;
}
}
...
デフォルトでは、ソフトウェアデコーダが使われるところを、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の引数の名前を変えるだけで対応できるはずです。
ここまでのコードのデコードループにおいて、デコードされたフレーム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;
}
keisuke.kimura in Livox Mid-360をROS1/ROS2で動かしてみた
Sorry for the delay in replying. I have done SLAM (FAST_LIO) with Livox MID360, but for various reasons I have not be...
Miya in ウエハースケールエンジン向けSimulated Annealingを複数タイルによる並列化で実装しました
作成されたプロファイラがとても良さそうです :) ぜひ詳細を書いていただきたいです!...
Deivaprakash in Livox Mid-360をROS1/ROS2で動かしてみた
Hey guys myself deiva from India currently i am working in this Livox MID360 and eager to knwo whether you have done the...
岩崎システム設計 岩崎 満 in Alveo U50で10G Ethernetを試してみる
仕事の都合で、検索を行い、御社サイトにたどりつきました。 内容は大変参考になりま...
Prabuddhi Wariyapperuma in Livox Mid-360をROS1/ROS2で動かしてみた
This issue was sorted....