マルチメディアデータの解析とデコード基盤
現代の映像処理システムでは、MP4やMKVなどのコンテナ形式から圧縮済みの映像ストリームを抽出し、生データへと復元する工程が不可欠である。FFmpeg(libav)ライブラリは、この一連の処理を統一的なAPIで提供している。処理は大きく「入力フォーマットの解析(デマルチプレクシング)」「コーデックの初期化」「圧縮データの送受信」「ピクセル変換」の4段階に分類できる。
本稿では、FFmpeg 4.4以降の推奨APIパターン(avcodec_send_packet / avcodec_receive_frame)を採用し、ローカルファイル、USBカメラ、デスクトップキャプチャなど複数の入力ソースに対応するC++実装を示す。復元されたフレームはそのままOpenCVのcv::Matへ変換され、後の画像処理や推論パイプラインへ引き渡せる状態とする。
C++による実装サンプル
以下のコードは、従来の手続き型の記述をクラスベースの設計へ再構成し、リソースの自動解放(RAII)とエラーハンドリングを強化したものである。
#include <iostream>
#include <memory>
#include <string>
#include <chrono>
#include <opencv2/opencv.hpp>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>
}
class VideoStreamDecoder {
private:
AVFormatContext* fmt_ctx_ = nullptr;
AVCodecContext* codec_ctx_ = nullptr;
AVFrame* decoded_frame_ = nullptr;
AVPacket* compressed_pkt_ = nullptr;
int video_stream_idx_ = -1;
void release_resources() {
if (codec_ctx_) avcodec_free_context(&codec_ctx_);
if (fmt_ctx_) avformat_close_input(&fmt_ctx_);
if (decoded_frame_) av_frame_free(&decoded_frame_);
if (compressed_pkt_) av_packet_free(&compressed_pkt_);
}
public:
~VideoStreamDecoder() { release_resources(); }
bool setup(const std::string& source_path, const std::string& input_fmt = "") {
fmt_ctx_ = avformat_alloc_context();
AVDictionary* setup_opts = nullptr;
// フォーマット指定がある場合は明示的に設定
if (!input_fmt.empty()) {
AVInputFormat* fmt_ptr = av_find_input_format(input_fmt.c_str());
if (avformat_open_input(&fmt_ctx_, source_path.c_str(), fmt_ptr, &setup_opts) != 0) return false;
} else {
if (avformat_open_input(&fmt_ctx_, source_path.c_str(), nullptr, &setup_opts) != 0) return false;
}
av_dict_free(&setup_opts);
if (avformat_find_stream_info(fmt_ctx_, nullptr) < 0) return false;
// ビデオストリームの探索とデコーダー初期化
const AVCodec* selected_codec = nullptr;
for (unsigned int i = 0; i < fmt_ctx_->nb_streams; ++i) {
AVCodecParameters* params = fmt_ctx_->streams[i]->codecpar;
if (params->codec_type == AVMEDIA_TYPE_VIDEO) {
selected_codec = avcodec_find_decoder(params->codec_id);
if (!selected_codec) continue;
video_stream_idx_ = i;
codec_ctx_ = avcodec_alloc_context3(selected_codec);
avcodec_parameters_to_context(codec_ctx_, params);
if (avcodec_open2(codec_ctx_, selected_codec, nullptr) < 0) return false;
break;
}
}
if (video_stream_idx_ == -1 || !codec_ctx_) return false;
decoded_frame_ = av_frame_alloc();
compressed_pkt_ = av_packet_alloc();
return true;
}
bool pull_frame(cv::Mat& out_bgr) {
int ret = av_read_frame(fmt_ctx_, compressed_pkt_);
if (ret < 0) return false; // ストリーム終了またはエラー
// 非映像ストリームはスキップ
if (compressed_pkt_->stream_index != video_stream_idx_) {
av_packet_unref(compressed_pkt_);
return true;
}
// デコーダーへ圧縮データを送信
ret = avcodec_send_packet(codec_ctx_, compressed_pkt_);
av_packet_unref(compressed_pkt_);
if (ret < 0) return false;
// デコード済みのフレームを取得
ret = avcodec_receive_frame(codec_ctx_, decoded_frame_);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) return false;
if (ret < 0) return false;
// OpenCV互換のBGR24へスケール変換(解像度は半分に縮小)
int out_w = codec_ctx_->width / 2;
int out_h = codec_ctx_->height / 2;
out_bgr.create(out_h, out_w, CV_8UC3);
SwsContext* scaler = sws_getContext(
codec_ctx_->width, codec_ctx_->height, codec_ctx_->pix_fmt,
out_w, out_h, AV_PIX_FMT_BGR24,
SWS_FAST_BILINEAR, nullptr, nullptr, nullptr
);
if (scaler) {
uint8_t* dest_planes[1] = { out_bgr.data };
int dest_strides[1] = { static_cast<int>(out_bgr.step) };
sws_scale(scaler, decoded_frame_->data, decoded_frame_->linesize, 0, decoded_frame_->height, dest_planes, dest_strides);
sws_freeContext(scaler);
}
return true;
}
int width() const { return codec_ctx_ ? codec_ctx_->width : 0; }
int height() const { return codec_ctx_ ? codec_ctx_->height : 0; }
};
int main() {
avdevice_register_all();
VideoStreamDecoder engine;
// 例: ローカル動画ファイル / USBカメラ("0") / デスクトップ("desktop")
if (!engine.setup("source_media.mp4")) {
std::cerr << "ストリームの初期化に失敗しました。\n";
return 1;
}
cv::Mat bgr_image;
int processed = 0;
const int limit = 150;
while (processed < limit) {
auto t0 = std::chrono::high_resolution_clock::now();
if (!engine.pull_frame(bgr_image)) break;
auto t1 = std::chrono::high_resolution_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
std::cout << "Frame#" << processed++ << " | Time: " << ms << "ms | Res: "
<< bgr_image.cols << "x" << bgr_image.rows << "\n";
// ここでcv::imshow()や推論エンジンへの転送などが実行可能
}
std::cout << "デコードセッションを終了しました。\n";
return 0;
}
FFmpeg コア構造体の役割とデータフロー
FFmpegの処理系は、複数の構造体が連携してデータパイプラインを構成している。各構造体の責務を理解することは、メモリリークの防止やパフォーマンスチューニングに直結する。
- AVFormatContext:入力または出力のコンテナファイルを表す大元のコンテキスト。ストリーム数、総再生時間、ビットレート、メタデータなどのファイルレベル情報を保持する。この構造体を介して
av_read_frameなどの関数が呼び出される。 - AVInputFormat:MP4やAVI、dshow、x11grabといった入力形式の特性を定義する構造体。ファイル拡張子やプロトコルスキームに応じてFFmpegが自動検出するが、デバイスクリーンキャプチャなど明示的な指定が必要な場合は手動で設定する。
- AVStream:ファイル内に含まれる個別のメディアストリーム(映像、音声、字幕など)を管理する。各ストリームは
AVCodecParametersへのポインタを持ち、コーデックの種別や解像度、フレームレートなどの基本属性を保持している。 - AVCodecContext:エンコーダーまたはデコーダーの動作状態を管理するコンテキスト。コーデックのID、ピクセルフォーマット、タイムベース、デコード済みフレーム番号などの実行時パラメータを格納する。初期化には
avcodec_parameters_to_contextによるパラメータ転送とavcodec_open2によるオープン処理が必須である。 - AVCodec:H.264やHEVC、VP9などのアルゴリズム実体そのものを表す。登録済みコーデックの検索には
avcodec_find_decoderが用いられる。 - AVPacket:デコード前の圧縮ビットストリームを格納する。PTS(表示時刻)やDTS(デコード時刻)、ストリームインデックス、キーフレームフラグなどのメタデータが付与される。
av_read_frameで読み出され、avcodec_send_packetへ渡される。 - AVFrame:デコード完了後の生映像データ(YUVやRGB)を保持する。
data配列に平面ごとのメモリポインタが格納され、linesizeに各行のバイト長が記録される。レンダラーや画像処理ライブラリへ渡される最終形態である。
ピクセルフォーマット変換と OpenCV 連携
FFmpegのデコーダーが出力するデフォルトのピクセルフォーマットは通常AV_PIX_FMT_YUV420Pである。これに対し、OpenCVや多くのGUIライブラリはBGR24またはRGB24を要求するため、中間変換ステージが必要となる。この変換はlibswscaleモジュールのSwsContextが担当する。
sws_getContext関数は、ソース解像度・フォーマットからデスティネーション解像度・フォーマットへのルーティングテーブルを事前計算する。変換アルゴリズムはflagsパラメータで指定でき、品質と速度のバランスを選択できる。代表的なオプションは以下の通りである。
SWS_FAST_BILINEAR:リアルタイム用途に最適化された双線形補間。軽快だが高周波成分の劣化が発生しやすい。SWS_BICUBIC:双3次補間。画質優先の静止画処理や高解像度ダウンサンプリングに適する。SWS_POINT:最近傍補間。演算負荷が最も低いが、エイリアシングが目立ちやすい。
実際にデータを転送するsws_scaleは、入力フレームのデータポインタとストライド配列を受け取り、目標のメモリバッファへ直接書き込む。OpenCVのcv::Matと連携する場合、Mat::dataのポインタとMat::step(1行あたりのバイト数)をそのまま出力バッファとして渡すことができる。これにより、メモリコピーを最小限に抑え、デコードから画像処理パイプラインへのシームレスな連携が実現する。
CMake 設定例
Linux環境で本コードをビルドする場合、CMakeLists.txtは以下の構成が標準的である。FFmpegのヘッダーとライブラリパス、およびOpenCVの依存関係を追記する。
cmake_minimum_required(VERSION 3.10)
project(media_decoder_app LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(OpenCV REQUIRED)
# FFmpeg パス設定 (環境に合わせて調整)
set(FFMPEG_INCLUDE_DIR "/usr/local/include")
set(FFMPEG_LIBRARY_DIR "/usr/local/lib")
include_directories(${FFMPEG_INCLUDE_DIR})
link_directories(${FFMPEG_LIBRARY_DIR})
add_executable(decoder_main main.cpp)
target_link_libraries(decoder_main
PRIVATE
${OpenCV_LIBS}
avformat avcodec avutil swscale swresample avdevice pthread
)