C++とFFmpegによるマルチメディアストリームのデコード処理

マルチメディアデータの解析とデコード基盤

現代の映像処理システムでは、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
)

タグ: FFmpeg C++ ビデオデコード OpenCV ストリーミング処理

8月26日 14:04 投稿