はい、承知いたしました。FFmpegとC++を連携させるための、ライブラリ導入から基本的なサンプルコードまでを詳細に解説する約5000語の記事を作成します。
FFmpeg C++連携:ライブラリの導入と基本サンプルの詳細解説
はじめに:なぜC++でFFmpegを使うのか?
マルチメディア処理の世界において、FFmpegはその強力な機能と柔軟性から、デファクトスタンダードと言える存在です。動画や音声のエンコード、デコード、変換、ストリーミング、フィルタリングなど、考えられるほとんどのマルチメディアタスクをこなすことができます。
FFmpegは主にC言語で書かれていますが、多くの開発者がC++アプリケーション内でその機能を利用したいと考えています。C++のオブジェクト指向や高度な機能と組み合わせることで、より複雑で洗練されたマルチメディア処理アプリケーションを構築することが可能になります。例えば、カスタムプレイヤー、トランスコーダー、動画編集ソフトウェア、分析ツールなどにFFmpegの強力なバックエンドを組み込むことができます。
しかし、FFmpegのC APIは非常に低レベルであり、学習コストが決して低いものではありません。また、ライブラリの導入(特にC++プロジェクトへのリンク)が最初の大きな壁となることが多いです。メモリ管理やエラーハンドリングも手作業で行う必要があるため、注意が必要です。
この記事では、FFmpegをC++プロジェクトに導入し、動画ファイルをデコードしてフレームを取得するという基本的なタスクを実行する方法を、詳細な解説とともにステップバイステップで説明します。約5000語を目標に、導入の壁を乗り越え、FFmpeg C APIの基本的な使い方を理解できるよう、具体的に解説していきます。
この記事を読むことで、以下のことができるようになります。
- FFmpegライブラリをC++開発環境に導入する方法を理解する。
- C++プロジェクト(CMakeを使用)でFFmpegライブラリをリンクする方法を学ぶ。
- FFmpeg C APIの基本的な構造体と関数の役割を理解する。
- 動画ファイルをオープンし、ストリーム情報を取得する。
- 動画ストリームのデコーダーを見つけて開く。
- パケットを読み込み、フレームにデコードするプロセスを理解する。
- デコードされたフレームを扱う基本的な方法を知る。
- FFmpeg使用後の適切なリソース解放方法を学ぶ。
さあ、FFmpegとC++の強力な連携の世界へ踏み出しましょう。
第1章:FFmpegの基本アーキテクチャ
FFmpegのAPIを理解するためには、その内部で扱われる主要な概念とデータ構造を知っておく必要があります。ここでは、FFmpegの基本的なアーキテクチャを簡単に見ていきましょう。
FFmpegは、主に以下のコンポーネントで構成されていると考えることができます。
-
フォーマット (Formats) / コンテナ (Containers): これは
.mp4
,.mov
,.mkv
,.avi
,.mp3
,.wav
などのファイル形式やストリーミングプロトコルを扱います。コンテナは、圧縮された(エンコードされた)動画、音声、字幕、メタデータなどのストリームを格納する「箱」のようなものです。FFmpegではlibavformat
ライブラリがこれを担当します。- 主要な構造体:
AVFormatContext
(ファイルやストリーム全体の状態を表すコンテキスト)
- 主要な構造体:
-
ストリーム (Streams): コンテナ内に含まれる個々のメディアデータ系列です。例えば、動画ストリーム、音声ストリームなどです。一つのコンテナファイルは複数のストリームを持つことができます。FFmpegでは
libavformat
内で扱われます。- 主要な構造体:
AVStream
(個々のストリーム(動画、音声など)の情報を持つ)
- 主要な構造体:
-
コーデック (Codecs): メディアデータ(動画や音声)を圧縮したり(エンコード)、解凍したり(デコード)するためのアルゴリズムまたはその実装です。例えば、H.264, HEVC, VP9 (動画)、AAC, MP3, FLAC (音声) などがあります。FFmpegでは
libavcodec
ライブラリがこれを担当します。- 主要な構造体:
AVCodec
(特定のエンコーダーまたはデコーダーを表す) - 主要な構造体:
AVCodecParameters
(ストリームに関連付けられたコーデックの基本的なパラメータ(解像度、フレームレートなど)を持つ) - 主要な構造体:
AVCodecContext
(エンコードまたはデコードを行うための実行時コンテキスト。コーデックのパラメータ、バッファ、状態などを保持する)
- 主要な構造体:
-
パケット (Packets): ストリーム内の圧縮されたデータの断片です。これは通常、エンコーダーによって生成され、デコーダーに渡されます。パケットは通常、独立してデコード可能なデータの単位(例えば、動画の1フレームまたは複数フレーム、音声の短い区間)を含みますが、そうでない場合もあります(Bフレームなど)。FFmpegでは
libavcodec
内で扱われます。- 主要な構造体:
AVPacket
(圧縮されたデータ(パケット)とその関連情報(タイムスタンプ、ストリームインデックスなど)を持つ)
- 主要な構造体:
-
フレーム (Frames): デコードされた、生の(非圧縮の)メディアデータです。動画であれば1枚の画像、音声であれば短い区間のPCMデータなどです。エンコーダーはこの生のフレームを受け取ってパケットを生成し、デコーダーはパケットを受け取って生のフレームを復元します。FFmpegでは
libavutil
とlibavcodec
内で扱われます。- 主要な構造体:
AVFrame
(非圧縮のデータ(フレーム)とその関連情報(タイムスタンプ、ピクセルフォーマット、音声サンプルフォーマットなど)を持つ)
- 主要な構造体:
処理の基本的な流れ(デコードの場合):
AVFormatContext
を使って入力ファイル(コンテナ)をオープンする。AVFormatContext
からストリーム情報(AVStream
のリスト)を取得する。- 目的のストリーム(例:動画ストリーム)を見つけ、その
AVCodecParameters
を取得する。 AVCodecParameters
に基づいて適切なAVCodec
(デコーダー)を見つける。AVCodec
からAVCodecContext
を作成し、パラメータをコピーして、デコーダーをオープンする。- ループで
AVFormatContext
から圧縮されたAVPacket
を読み込む。 - 読み込んだ
AVPacket
をAVCodecContext
に送り込む(デコード要求)。 AVCodecContext
からデコードされたAVFrame
を受け取る。AVFrame
の生のデータ(ピクセルデータや音声サンプルデータ)を処理する。- 全てのパケットを処理し終えたら、デコーダーをフラッシュして残りのフレームを取得する。
- 使用したFFmpegリソースを解放する。
この流れを頭に入れておくと、後のサンプルコードが理解しやすくなります。
第2章:FFmpegライブラリの導入
FFmpegライブラリをC++プロジェクトで使用するためには、まずライブラリ本体を開発環境に導入する必要があります。これは多くの場合、初心者にとって最初の難関となります。導入方法はいくつかありますが、ここでは主要なものを紹介します。
注意点: FFmpegは頻繁に更新されており、APIに破壊的な変更が入ることもあります。特に古い記事やドキュメントを参照する場合、使用しているFFmpegのバージョンとAPIが一致しない可能性があることに注意してください。この記事は執筆時点(FFmpeg 5.x または 6.x 系を想定)の一般的なAPIに基づいています。
2.1 導入方法の選択肢
-
ソースコードからコンパイルする:
- 利点: 完全にカスタマイズ可能。必要なコーデックや機能を厳密に選択できる。最新バージョンを入手できる。デバッグがしやすい。
- 欠点: 環境構築が複雑。ビルドには様々な依存ライブラリが必要。OSごとに手順が異なる。時間がかかる。
- 向いているケース: 特定の機能が必要、最新バージョンが必要、ライブラリの内部を深く理解したい、配布用のカスタムビルドが必要な場合。
-
ビルド済みバイナリを使用する:
- 利点: 手軽に導入できる。コンパイルの手間がない。
- 欠点: 提供されている機能やビルドオプションに制約がある。DLL Hell(Windows)や共有ライブラリのバージョン問題が発生する可能性。セキュリティや信頼性の確認が必要な場合がある。
- 向いているケース: とにかく手軽に試したい、標準的な機能で十分な場合。
-
パッケージマネージャーを使用する:
- 利点: 依存関係の管理が容易。インストールやアップデートが簡単。OSや環境に合わせたビルドが提供されることが多い。C++プロジェクトとの連携(特にCMake)がしやすいものが多い。
- 欠点: 提供されるFFmpegのバージョンが最新でない場合がある。カスタマイズ性はソースビルドより低い。特定のパッケージマネージャーに依存する。
- 向いているケース: 一般的な開発環境。依存関係の管理をツールに任せたい場合。CMakeなどのビルドシステムを利用している場合。(個人的には、C++開発にはこの方法が最も推奨されます)
この記事では、最も現代的で推奨される方法である「パッケージマネージャーを使用する」方法と、手軽な「ビルド済みバイナリを使用する」方法を中心に説明します。ソースコードからのビルドは、それだけで詳細な記事が必要になるため、ここでは概要のみに留めます。
2.2 ビルド済みバイナリを使用する方法 (Windows向け)
Windowsでは、コミュニティによって提供されているビルド済みバイナリを利用するのが手軽です。かつてZeranoe’s FFmpeg Buildsが有名でしたが、現在は更新されていません。代わりに、BtbN氏などが提供するビルドがよく使われています。
- バイナリのダウンロード:
- BtbN氏のビルドサイトなどを探し、「ffmpeg-master-latest-win64-gpl.zip」のような名前のファイルをダウンロードします。(GPL版かLGPL版か、shared版かstatic版かなど、目的に応じて選択してください。C++アプリケーションに組み込む場合は、通常shared版を選び、アプリケーションと共にDLLを配布します。static版は単一の実行ファイルになりますが、ライセンスに注意が必要です。GPL版は派生コードもGPLにする必要があります。)
- サイトによってはデバッグシンボル付きなどのオプションがあります。今回は
shared
版を想定します。
- ファイルの展開:
- ダウンロードしたZIPファイルを、開発プロジェクトとは別の場所に展開します。(例:
C:\Libraries\ffmpeg-master-latest-win64-gpl-shared
) - 展開されたフォルダには、通常以下のようなサブフォルダがあります。
bin
: FFmpegの実行ファイル (ffmpeg.exe
,ffplay.exe
,ffprobe.exe
) や必要なDLL (avcodec.dll
,avformat.dll
, etc.) が含まれます。include
: FFmpegのヘッダーファイル(.h
ファイル)が含まれます。これらをC++コードからインクルードします。lib
: ライブラリファイル(.lib
ファイル)が含まれます。これらをC++コンパイラに指定してリンクします。
- ダウンロードしたZIPファイルを、開発プロジェクトとは別の場所に展開します。(例:
- プロジェクト設定:
- Visual StudioなどのIDEを使用する場合、プロジェクトのプロパティで以下の設定を行います。
- C/C++ > General > Additional Include Directories: 先ほど展開したフォルダの
include
サブフォルダへのパスを追加します。 - Linker > General > Additional Library Directories: 展開したフォルダの
lib
サブフォルダへのパスを追加します。 - Linker > Input > Additional Dependencies: リンクしたいライブラリファイルの名前(例:
avformat.lib
,avcodec.lib
,avutil.lib
,swscale.lib
など、使用する機能に応じて)を追加します。
- C/C++ > General > Additional Include Directories: 先ほど展開したフォルダの
- コンパイルした実行ファイルをデバッグまたは実行する際には、
bin
フォルダにあるDLLファイルが実行ファイルから見える場所に配置されている必要があります。実行ファイルと同じフォルダにコピーするか、bin
フォルダのパスをシステムのPATH環境変数に追加する方法があります(ただしPATHへの追加はシステム全体に影響するため注意が必要です)。
- Visual StudioなどのIDEを使用する場合、プロジェクトのプロパティで以下の設定を行います。
2.3 パッケージマネージャーを使用する方法
パッケージマネージャーを使うと、依存ライブラリの解決やFFmpeg自体のインストール、さらにはC++プロジェクトでの設定(特にCMake)が格段に楽になります。
2.3.1 vcpkg (Windows, Linux, macOS)
Microsoftが開発を主導するC++ライブラリマネージャーです。クロスプラットフォームで動作し、多くのライブラリをCMakeと連携させて簡単に使用できます。
-
vcpkgのインストール:
- vcpkgのリポジトリをクローンします。
bash
git clone https://github.com/microsoft/vcpkg
cd vcpkg - vcpkgをビルドします。
bash
./bootstrap-vcpkg.sh # Linux/macOS
.\bootstrap-vcpkg.bat # Windows - 必要に応じて、システム全体のインテグレーションを行います(推奨)。
bash
./vcpkg integrate install
- vcpkgのリポジトリをクローンします。
-
FFmpegのインストール:
- FFmpegをインストールします。通常、デコーダー/エンコーダーなど多くの機能を含むデフォルト設定で十分です。
bash
vcpkg install ffmpeg - ハードウェアアクセラレーションや特定のコーデックが必要な場合は、オプションを指定します。例えば、cuDNNサポート付きのFFmpegをインストールする場合(ポート名やオプションはvcpkgのバージョンにより変わる可能性があります):
bash
vcpkg install ffmpeg[cuda] - インストールには時間がかかります。
- FFmpegをインストールします。通常、デコーダー/エンコーダーなど多くの機能を含むデフォルト設定で十分です。
-
CMakeとの連携:
- vcpkgはCMakeとの連携を非常に容易にします。プロジェクトの
CMakeLists.txt
ファイルでfind_package(ffmpeg CONFIG REQUIRED)
のように指定し、target_link_libraries
でリンクするだけで済みます。vcpkgをインストールしたディレクトリをCMakeに指定してビルドを実行すれば、vcpkgが自動的にFFmpegを見つけて必要なインクルードパスとライブラリパスを設定してくれます。
- vcpkgはCMakeとの連携を非常に容易にします。プロジェクトの
2.3.2 Homebrew (macOS, Linux)
macOSで広く使われているパッケージマネージャーです。Linuxにも対応しています。
- Homebrewのインストール: 公式サイトの手順に従ってインストールします。
- FFmpegのインストール:
bash
brew install ffmpeg - CMakeとの連携: HomebrewもFFmpegのpkg-configファイルを提供するため、CMakeの
find_package(PkgConfig)
とpkg_check_modules
を使用してライブラリ情報(インクルードパス、ライブラリパス、リンクフラグ)を取得し、ターゲットにリンクすることができます。
2.3.3 MSYS2 (Windows)
Windows上でUnixライクな環境を提供し、pacmanパッケージマネージャーを使えます。
- MSYS2のインストール: 公式サイトからインストーラーをダウンロードして実行します。
- FFmpegのインストール: MSYS2環境を起動し、必要なパッケージをインストールします。MinGW-w64環境を使用するのが一般的です。
bash
# リポジトリを更新
pacman -Syu
# MinGW-w64 toolchain と FFmpeg shared libraries をインストール (x86_64の場合)
pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-ffmpeg - CMakeとの連携: MSYS2 MinGW-w64環境内でCMakeを使ってビルドすれば、pacmanでインストールしたFFmpegライブラリが自動的に見つかります。
2.4 CMakeLists.txt の設定
どの導入方法を選んだとしても、C++プロジェクトからFFmpegライブラリを利用するには、コンパイラにヘッダーファイルの場所を、リンカーにライブラリファイルの場所とリンクするライブラリ名を伝える必要があります。CMakeを使うと、この設定をクロスプラットフォームで行うことができます。
以下は、FFmpegライブラリ(avformat
, avcodec
, avutil
, swscale
)をリンクするための基本的な CMakeLists.txt
の例です。
“`cmake
cmake_minimum_required(VERSION 3.10) # CMakeのバージョンを指定
project(FFmpegSampleProject CXX) # プロジェクト名と言語を指定
C++標準を指定 (例: C++14)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
FFmpeg を探す
vcpkg を使っている場合、vcpkg_installed//share/ffmpeg/ に CONFIG ファイルがインストールされるため
find_package(ffmpeg CONFIG REQUIRED) が推奨されます。
pkg-config が利用可能な場合 (Linux, macOS with Homebrew, MSYS2) は PkgConfig を使うこともできます。
手動でパスを指定する場合は、以下の設定を追加します。
— vcpkg を使用する場合 —
vcpkg toolchain file を指定して CMake を実行する (例: cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake ..)
find_package(ffmpeg CONFIG REQUIRED) # vcpkgによって提供される設定ファイルを探す
— pkg-config を使用する場合 (Homebrew, MSYS2など) —
find_package(PkgConfig REQUIRED)
pkg_check_modules(FFMPEG REQUIRED libavformat libavcodec libavutil libswscale) # 必要なモジュール名を指定
— 手動でパスを指定する場合 —
set(FFMPEG_INCLUDE_DIRS “C:/Libraries/ffmpeg-master-latest-win64-gpl-shared/include”) # FFmpeg include フォルダへのパス
set(FFMPEG_LIB_DIRS “C:/Libraries/ffmpeg-master-latest-win64-gpl-shared/lib”) # FFmpeg lib フォルダへのパス
set(FFMPEG_LIBRARIES avformat avcodec avutil swscale) # リンクするライブラリ名 (.lib や .a は付けない)
include_directories(${FFMPEG_INCLUDE_DIRS}) # 手動の場合
link_directories(${FFMPEG_LIB_DIRS}) # 手動の場合
実行可能ファイルを定義
add_executable(ffmpeg_decoder_sample main.cpp) # ソースファイル名を指定
実行可能ファイルに FFmpeg ライブラリをリンク
vcpkg の場合:
target_link_libraries(ffmpeg_decoder_sample PRIVATE ffmpeg::avformat ffmpeg::avcodec ffmpeg::avutil ffmpeg::swscale)
pkg-config の場合:
target_link_libraries(ffmpeg_decoder_sample PRIVATE ${FFMPEG_LIBRARIES}) # PkgConfig で取得した変数を使用
手動の場合:
target_link_libraries(ffmpeg_decoder_sample PRIVATE ${FFMPEG_LIBRARIES}) # 手動で定義した変数を使用
必要に応じて、実行時にFFmpegのDLLを見つけられるように設定する(Windows shared buildの場合)
install コマンドや、ビルド後のステップでDLLをコピーするなど、方法はいくつかあります。
以下はビルドディレクトリにDLLをコピーする例 (Windows, MSVC向け。環境によって調整必要)
if(CMAKE_SYSTEM_NAME STREQUAL “Windows”)
find_program(CMAKE_COPY_FILE copy_file)
if(CMAKE_GENERATOR MATCHES “Visual Studio”)
# Visual Studio ジェネレータの場合、Debug/Release サブディレクトリに実行ファイルが作成される
set(FFMPEG_BIN_DIR “C:/Libraries/ffmpeg-master-latest-win64-gpl-shared/bin”) # FFmpeg bin フォルダへのパス
add_custom_command(TARGET ffmpeg_decoder_sample POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory “${FFMPEG_BIN_DIR}” $
COMMENT “Copying FFmpeg DLLs to build directory”
)
else()
# その他のジェネレータ (Ninja, MinGW Makefiles など) の場合
set(FFMPEG_BIN_DIR “C:/Libraries/ffmpeg-master-latest-win64-gpl-shared/bin”) # FFmpeg bin フォルダへのパス
add_custom_command(TARGET ffmpeg_decoder_sample POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory “${FFMPEG_BIN_DIR}” $
COMMENT “Copying FFmpeg DLLs to build directory”
)
endif()
endif()
“`
CMakeでビルドする手順 (例: vcpkgを使用し、Visual Studioでビルドする場合):
main.cpp
とCMakeLists.txt
を同じディレクトリに置きます。- ビルドディレクトリを作成し、そこに移動します。
bash
mkdir build
cd build - vcpkgツールチェインファイルを指定してCMakeを実行します。
bash
cmake .. -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake -A x64 # Visual Studio 64bitの場合
# または Ninja の場合:
# cmake .. -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake -G Ninja - ビルドを実行します。
bash
cmake --build . --config Release # または Debug
これにより、FFmpegが適切にリンクされた実行可能ファイルがビルドディレクトリ内に生成されます。
第3章:FFmpeg C APIの基本とサンプルコード
いよいよFFmpeg C APIを使ってマルチメディアファイルを扱う方法を見ていきます。ここでは、入力された動画ファイルから最初の数フレームをデコードし、その情報を表示する簡単なサンプルコードを作成します。
このサンプルでは、以下のステップを実行します。
- FFmpegライブラリの初期化(現代のFFmpegでは必須ではないが、古いコードや一部のフォーマット/コーデックでは必要だった名残として
avdevice_register_all
,avfilter_register_all
,av_register_all
などがあるが、これらは現在は非推奨または不要)。 - 入力ファイルを開き、コンテナフォーマットを読み込む (
avformat_open_input
)。 - ファイル内のストリーム情報(コーデック、時間情報など)を取得する (
avformat_find_stream_info
)。 - 動画ストリームを見つける。
- 動画ストリームからデコーダーを見つける (
avcodec_find_decoder
)。 - デコーダーに対応するコンテキストを作成・設定する (
avcodec_alloc_context3
,avcodec_parameters_to_context
)。 - デコーダーを開く (
avcodec_open2
)。 - デコードに必要なパケットとフレームの構造体を確保する (
av_packet_alloc
,av_frame_alloc
)。 - ファイルからパケットを読み込むループに入る (
av_read_frame
)。 - 読み込んだパケットをデコーダーに送る (
avcodec_send_packet
)。 - デコーダーからデコード済みのフレームを受け取る (
avcodec_receive_frame
)。 - デコードされたフレームの情報を表示する。
- パケットとフレームのリソースを解放する (
av_packet_unref
,av_frame_unref
)。 - 全てのパケットを読み終えたら、デコーダーをフラッシュして残りのフレームを取得する。
- 使用したFFmpegリソースを解放してファイルを閉じる (
avformat_close_input
,avcodec_free_context
, etc.)。 - エラーが発生した場合は、エラーコードをFFmpegのエラー文字列に変換して表示する (
av_strerror
またはav_err2str
)。
注意: FFmpeg APIはC++のエラー処理(例外)を使いません。ほとんどのFFmpeg関数は成功時に0以上、エラー時に負の値を返します。特に avcodec_receive_frame
や av_read_frame
などは、特定の条件下で非負の値(例えば AVERROR(EAGAIN)
や AVERROR_EOF
)を返すことがあり、これらはエラー ではない ので区別して処理する必要があります。
3.1 サンプルコード (main.cpp
)
ここでは、動画ファイルパスをコマンドライン引数として受け取り、動画ストリームを見つけて数フレームをデコードするコードを示します。ピクセルフォーマット変換(swscale
)は含まず、デコード後の生フレーム情報表示までを行います。swscale
は後述します。
“`cpp
include
include
include
// C言語APIとしてFFmpegヘッダーをインクルード
extern “C” {
include
include
include
include // av_image_get_buffer_size など
include // 必要に応じてスケーリング/色空間変換用
}
// FFmpegのエラーコードを文字列に変換するヘルパー関数
// FFmpeg 4.0 以降で推奨される av_err2str を使用
// 互換性のために古い av_strerror も含む形で記述することがあるが、ここでは av_err2str のみ使用
std::string ffmpeg_error_string(int errnum) {
char err_buf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(errnum, err_buf, sizeof(err_buf));
return std::string(err_buf);
}
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << “Usage: ” << argv[0] << ”
return 1;
}
const char* in_filename = argv[1];
// FFmpeg ライブラリ初期化(現代のFFmpegではほとんど不要だが、古いコードや一部機能では見られることがある)
// av_register_all(); // 古いバージョンで使用されていたが、現在は非推奨かつ不要
AVFormatContext* format_ctx = nullptr;
AVCodecContext* codec_ctx = nullptr;
int video_stream_idx = -1;
// 1. 入力ファイルを開く
// avformat_open_input(AVFormatContext**, filename, AVInputFormat*, AVDictionary**)
// 成功時は 0 を返す
std::cout << "Opening input file: " << in_filename << std::endl;
int ret = avformat_open_input(&format_ctx, in_filename, nullptr, nullptr);
if (ret < 0) {
std::cerr << "Could not open input file " << in_filename << ": "
<< ffmpeg_error_string(ret) << std::endl;
return 1;
}
std::cout << "Input file opened successfully." << std::endl;
// 2. ストリーム情報を取得する
// avformat_find_stream_info(AVFormatContext*, AVDictionary**)
// 成功時は 0 を返す
std::cout << "Finding stream information..." << std::endl;
ret = avformat_find_stream_info(format_ctx, nullptr);
if (ret < 0) {
std::cerr << "Could not find stream information: "
<< ffmpeg_error_string(ret) << std::endl;
avformat_close_input(&format_ctx); // ファイルを閉じる
return 1;
}
std::cout << "Stream information found." << std::endl;
// デバッグ用にストリーム情報をダンプ (オプション)
// av_dump_format(AVFormatContext*, int index, const char *url, int is_output);
av_dump_format(format_ctx, 0, in_filename, 0);
// 3. 動画ストリームを見つける
// format_ctx->nb_streams はストリームの数
// format_ctx->streams[i] は i 番目のストリーム (AVStream*)
// AVStream->codecpar はコーデックパラメータ (AVCodecParameters*)
for (unsigned int i = 0; i < format_ctx->nb_streams; ++i) {
// AVCodecParameters->codec_type はストリームのタイプ (AVMEDIA_TYPE_VIDEO, AVMEDIA_TYPE_AUDIO, etc.)
if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_idx = i;
break; // 最初に見つかった動画ストリームを使用
}
}
if (video_stream_idx == -1) {
std::cerr << "Could not find a video stream in the input file." << std::endl;
avformat_close_input(&format_ctx);
return 1;
}
std::cout << "Video stream found at index: " << video_stream_idx << std::endl;
// 4. デコーダーを見つける
// avcodec_find_decoder(enum AVCodecID id);
// AVCodecParameters->codec_id はコーデックID
AVCodecParameters* codec_params = format_ctx->streams[video_stream_idx]->codecpar;
AVCodec* decoder = avcodec_find_decoder(codec_params->codec_id);
if (!decoder) {
std::cerr << "Failed to find decoder for video stream." << std::endl;
avformat_close_input(&format_ctx);
return 1;
}
std::cout << "Decoder found: " << decoder->name << std::endl;
// 5. デコーダーコンテキストを作成・設定する
// avcodec_alloc_context3(const AVCodec* codec);
codec_ctx = avcodec_alloc_context3(decoder);
if (!codec_ctx) {
std::cerr << "Failed to allocate codec context." << std::endl;
avformat_close_input(&format_ctx);
return 1;
}
// avcodec_parameters_to_context(AVCodecContext* avctx, const AVCodecParameters* codecpar);
// AVCodecParameters から AVCodecContext にパラメータをコピーする
ret = avcodec_parameters_to_context(codec_ctx, codec_params);
if (ret < 0) {
std::cerr << "Failed to copy codec parameters to codec context: "
<< ffmpeg_error_string(ret) << std::endl;
avcodec_free_context(&codec_ctx);
avformat_close_input(&format_ctx);
return 1;
}
std::cout << "Codec context created and parameters copied." << std::endl;
// 6. デコーダーを開く
// avcodec_open2(AVCodecContext* avctx, const AVCodec* codec, AVDictionary** options);
// 成功時は 0 を返す
ret = avcodec_open2(codec_ctx, decoder, nullptr);
if (ret < 0) {
std::cerr << "Failed to open codec: " << ffmpeg_error_string(ret) << std::endl;
avcodec_free_context(&codec_ctx);
avformat_close_input(&format_ctx);
return 1;
}
std::cout << "Codec opened successfully." << std::endl;
std::cout << "Video Resolution: " << codec_ctx->width << "x" << codec_ctx->height << std::endl;
std::cout << "Pixel Format: " << av_get_pix_fmt_name(codec_ctx->pix_fmt) << std::endl;
// 7. デコードに必要なパケットとフレームの構造体を確保する
// av_packet_alloc() と av_frame_alloc() は成功時 NULL 以外を返す
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
if (!packet || !frame) {
std::cerr << "Failed to allocate packet or frame." << std::endl;
// 解放処理は goto cleanup を使うと楽だが、ここでは順を追って書く
av_packet_free(&packet); // NULL チェックは内部で行われる
av_frame_free(&frame); // NULL チェックは内部で行われる
avcodec_free_context(&codec_ctx);
avformat_close_input(&format_ctx);
return 1;
}
std::cout << "Packet and Frame allocated." << std::endl;
int frames_decoded_count = 0;
const int max_frames_to_decode = 5; // 最初の5フレームだけデコードする例
// 8. ファイルからパケットを読み込み、デコードするループ
// av_read_frame(AVFormatContext* s, AVPacket* pkt);
// 成功時 0, ファイル終端 (EOF) 時 AVERROR_EOF, エラー時 負の値 を返す
std::cout << "Starting to read and decode frames..." << std::endl;
while (av_read_frame(format_ctx, packet) >= 0) {
// このパケットが動画ストリームのものであるか確認
if (packet->stream_index == video_stream_idx) {
// 9. 読み込んだパケットをデコーダーに送る
// avcodec_send_packet(AVCodecContext* avctx, const AVPacket* pkt);
// 成功時 0, エラー時 負の値, デコーダーが入力受付準備できていない時 AVERROR(EAGAIN), EOF時 AVERROR_EOF
ret = avcodec_send_packet(codec_ctx, packet);
if (ret < 0) {
// AVERROR(EAGAIN) は一時的なエラーなので、後でリトライするか、receive_frame を呼んでバッファを空ける必要がある
// AVERROR_EOF はパケット送信終了のシグナルだが、av_read_frame のループ内では通常発生しない
std::cerr << "Error sending a packet for decoding: "
<< ffmpeg_error_string(ret) << std::endl;
// エラーが発生した場合、ループを抜けるか、リカバリー処理を行う
break;
}
// 10. デコーダーからデコード済みのフレームを受け取る
// 1つのパケットから複数のフレームが得られる場合や、バッファリングのため
// send_packet の後に receive_frame を複数回呼ぶ必要がある
// avcodec_receive_frame(AVCodecContext* avctx, AVFrame* frame);
// 成功時 0, デコーダーが出力準備できていない時 AVERROR(EAGAIN), EOF時 AVERROR_EOF, エラー時 負の値
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
// AVERROR(EAGAIN): もっとパケットを送る必要がある
// AVERROR_EOF: デコーダーがフラッシュされたか、ストリームの終端に達した
break; // receive_frame ループを抜けて、次の av_read_frame に進む
} else if (ret < 0) {
// デコードエラー
std::cerr << "Error during decoding: "
<< ffmpeg_error_string(ret) << std::endl;
// エラーが発生した場合、receive_frame ループも抜けて、外側の read_frame ループも抜ける
goto end_decoding_loop; // goto を使って複数のループを抜ける
}
// 11. デコードされたフレームの情報を表示
// フレームが正常に受け取られた
std::cout << "Decoded Frame " << frames_decoded_count << ": "
<< "pts=" << frame->pts << ", "
<< "pkt_pts=" << frame->pkt_pts << ", "
<< "pkt_dts=" << frame->pkt_dts << ", "
<< "key_frame=" << frame->key_frame << ", "
<< "format=" << av_get_pix_fmt_name((AVPixelFormat)frame->format) << ", "
<< "width=" << frame->width << ", "
<< "height=" << frame->height << std::endl;
// TODO: ここでフレームデータ (frame->data, frame->linesize) を処理する
// 例: swscale で別のピクセルフォーマットに変換、画像ファイルとして保存など
frames_decoded_count++;
// 特定のフレーム数をデコードしたらループを抜ける
if (frames_decoded_count >= max_frames_to_decode) {
goto end_decoding_loop; // 指定フレーム数に達したので終了
}
// 12. フレームのリソースを解放
// av_frame_unref は frame が保持している内部バッファの参照カウントを減らし、0になれば解放する
// これを忘れるとメモリリークの原因になる
av_frame_unref(frame); // 次のフレームを格納するために、現在のフレームデータを解放
}
}
// 12. パケットのリソースを解放
// av_packet_unref は packet が保持している内部バッファの参照カウントを減らし、0になれば解放する
// これを忘れるとメモリリークの原因になる
av_packet_unref(packet); // 次のパケットを読み込むために、現在のパケットデータを解放
}
end_decoding_loop:; // goto のターゲットラベル
// 14. デコーダーのフラッシュ
// ファイル終端に達した後、デコーダー内部に残っているフレームをすべて取得する
// avcodec_send_packet(ctx, NULL) でフラッシュモードに入る
std::cout << "End of input file, flushing decoder..." << std::endl;
ret = avcodec_send_packet(codec_ctx, NULL); // NULL パケットを送る
if (ret < 0) {
std::cerr << "Error sending flush packet: " << ffmpeg_error_string(ret) << std::endl;
}
// フラッシュ中に残りのフレームを受け取る
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break; // フラッシュ完了
} else if (ret < 0) {
std::cerr << "Error during flushing: " << ffmpeg_error_string(ret) << std::endl;
goto cleanup; // エラー発生、クリーンアップへ
}
// デコードされたフレームの情報を表示 (フラッシュされたフレーム)
std::cout << "Flushed Frame " << frames_decoded_count << ": "
<< "pts=" << frame->pts << ", "
<< "pkt_pts=" << frame->pkt_pts << ", "
<< "pkt_dts=" << frame->pkt_dts << ", "
<< "key_frame=" << frame->key_frame << ", "
<< "format=" << av_get_pix_fmt_name((AVPixelFormat)frame->format) << ", "
<< "width=" << frame->width << ", "
<< "height=" << frame->height << std::endl;
frames_decoded_count++;
av_frame_unref(frame); // フレームデータ解放
}
cleanup: // goto によるクリーンアップ処理の開始地点
// 15. 使用したFFmpegリソースを解放する
std::cout << "Cleaning up resources..." << std::endl;
// av_packet_free は NULL ポインタを渡しても安全
av_packet_free(&packet);
// av_frame_free は NULL ポインタを渡しても安全
av_frame_free(&frame);
// avcodec_free_context は NULL ポインタを渡しても安全
avcodec_free_context(&codec_ctx);
// avformat_close_input は NULL ポインタを渡しても安全
// &format_ctx を渡すことで、内部で format_ctx を NULL に設定してくれる
avformat_close_input(&format_ctx);
std::cout << "Cleanup complete." << std::endl;
return 0;
}
“`
3.2 サンプルコードの解説
上記のサンプルコードをステップごとに詳しく見ていきましょう。
インクルード:
“`cpp
include
include
include
extern “C” {
include
include
include
include // av_image_get_buffer_size など
include // 必要に応じてスケーリング/色空間変換用
}
``
extern “C”
FFmpegライブラリはC言語で書かれているため、C++コードから呼び出す際にはブロックで囲む必要があります。これにより、C++のリンケージ規則ではなくC言語のリンケージ規則(名前修飾なし)が使用され、リンカーがFFmpegの関数を見つけられるようになります。
インクルードしているヘッダーファイルは以下の通りです。
*: コンテナ形式の操作(ファイルオープン、ストリーム情報の取得、パケット読み込みなど)に関する関数や構造体。
*: コーデックの操作(デコーダー/エンコーダーの検索、コンテキスト管理、パケットの送受信)に関する関数や構造体。
*: 基本的なユーティリティ関数や構造体(エラー処理、メモリ管理など)。
*: 画像関連のユーティリティ関数(バッファサイズの計算など)。今回は直接使っていませんが、ピクセルデータを扱う際にはよく使われます。
*
エラーハンドリング:
cpp
std::string ffmpeg_error_string(int errnum) {
char err_buf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(errnum, err_buf, sizeof(err_buf));
return std::string(err_buf);
}
FFmpeg APIはエラーが発生した場合、負の整数値を返します。av_strerror
(または av_err2str
) 関数を使うと、このエラーコードに対応する人間が読める文字列を取得できます。このヘルパー関数は、エラー発生時に詳細なメッセージを表示するために使用します。AV_ERROR_MAX_STRING_SIZE
はエラー文字列を格納するバッファの推奨サイズです。
main
関数と引数処理:
cpp
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <input_video_file>" << std::endl;
return 1;
}
const char* in_filename = argv[1];
// ...
}
プログラムの開始点です。コマンドライン引数として入力動画ファイルのパスを受け取ります。引数が不足していれば使い方を表示して終了します。
FFmpeg ライブラリ初期化:
cpp
// av_register_all(); // 古いバージョンで使用されていたが、現在は非推奨かつ不要
かつてのFFmpegでは、使用するフォーマットやコーデックを登録するために av_register_all()
を呼ぶ必要がありました。しかし、FFmpeg 4.0以降、これは非推奨となり、ほとんどの場合不要になりました。FFmpegが動的に利用可能なコンポーネントを検出するようになったためです。ただし、古いコードや特定の組み込み環境、あるいは静的リンクを行う場合にはまだ必要になるケースもあるかもしれません。ここではコメントアウトして、最新のAPIに沿っています。
AVFormatContext の確保と入力ファイルオープン:
cpp
AVFormatContext* format_ctx = nullptr;
// ...
int ret = avformat_open_input(&format_ctx, in_filename, nullptr, nullptr);
if (ret < 0) {
// エラー処理
}
AVFormatContext
は、入力ファイル(コンテナ)の情報を保持する重要な構造体です。avformat_open_input
は、指定されたファイルパスをオープンし、コンテナフォーマットを自動的に判別して AVFormatContext
を初期化します。第一引数は AVFormatContext*
へのポインタ (&format_ctx
) です。成功すると、関数内で新しい AVFormatContext
オブジェクトが確保され、そのポインタが format_ctx
に代入されます。第三引数には特定の入力フォーマット(例: av_find_input_format("avi")
)を指定することもできますが、nullptr
を渡すとFFmpegが自動判別します。第四引数はオプションで、フォーマット固有のパラメータを指定できますが、通常は nullptr
で十分です。成功すると0が返り、失敗すると負のエラーコードが返ります。
ストリーム情報の取得:
cpp
ret = avformat_find_stream_info(format_ctx, nullptr);
if (ret < 0) {
// エラー処理とクリーンアップ
}
avformat_open_input
はファイルヘッダーを読み込むだけで、全てのストリームの詳細な情報を取得するわけではありません。avformat_find_stream_info
を呼び出すことで、FFmpegはファイル全体を少し読み進め、各ストリームのコーデック、時間ベース、期間などの詳細情報を解析し、format_ctx->streams
配列に設定します。これが成功すると、後続の処理でストリーム情報やコーデックパラメータを参照できるようになります。
動画ストリームの検索:
cpp
int video_stream_idx = -1;
for (unsigned int i = 0; i < format_ctx->nb_streams; ++i) {
if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_idx = i;
break;
}
}
if (video_stream_idx == -1) {
// エラー処理とクリーンアップ
}
format_ctx->nb_streams
はファイル内のストリームの総数です。format_ctx->streams
は AVStream*
の配列です。各 AVStream
の codecpar
メンバーは AVCodecParameters
構造体へのポインタを持っており、その中の codec_type
メンバーでストリームの種類(動画、音声、字幕など)が指定されます。AVMEDIA_TYPE_VIDEO
は動画ストリームを表す列挙値です。ここでは、最初の動画ストリームのインデックスを見つけて video_stream_idx
に保存しています。
デコーダーの検索:
cpp
AVCodecParameters* codec_params = format_ctx->streams[video_stream_idx]->codecpar;
AVCodec* decoder = avcodec_find_decoder(codec_params->codec_id);
if (!decoder) {
// エラー処理とクリーンアップ
}
AVStream
から取得した AVCodecParameters
には、そのストリームに使用されているコーデックのID (codec_id
) が含まれています。avcodec_find_decoder
関数にこのIDを渡すことで、対応するデコーダーの実装 (AVCodec
) を見つけます。成功すれば AVCodec*
が返り、見つからなければ NULL
が返ります。
AVCodecContext の作成と設定:
“`cpp
AVCodecContext* codec_ctx = avcodec_alloc_context3(decoder);
if (!codec_ctx) {
// エラー処理とクリーンアップ
}
ret = avcodec_parameters_to_context(codec_ctx, codec_params);
if (ret < 0) {
// エラー処理とクリーンアップ
}
``
AVCodecContext
デコード処理はを通じて行われます。
avcodec_alloc_context3は、指定された
AVCodecに対応する
AVCodecContextを新しく確保し、初期化します。成功すれば
AVCodecContext*が返り、失敗すれば
NULLが返ります。確保したばかりのコンテキストは空なので、
avcodec_parameters_to_contextを使って
AVStreamから得た
AVCodecParameters` の情報(解像度、ピクセルフォーマット、ビットレートなど)をコピーして設定します。
デコーダーを開く:
cpp
ret = avcodec_open2(codec_ctx, decoder, nullptr);
if (ret < 0) {
// エラー処理とクリーンアップ
}
デコード処理を開始するために、avcodec_open2
でデコーダーを開きます。これにより、コーデックの内部状態が初期化され、デコードの準備が整います。第三引数はオプションで、コーデック固有のパラメータを指定できますが、通常は nullptr
で十分です。
AVPacket と AVFrame の確保:
cpp
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
if (!packet || !frame) {
// エラー処理とクリーンアップ
}
デコード処理では、圧縮されたデータを格納する AVPacket
と、デコードされた生データを格納する AVFrame
が必要になります。これらの構造体を av_packet_alloc
および av_frame_alloc
で確保します。これらの関数は構造体だけでなく、内部で必要となる動的なバッファも一緒に確保してくれる場合があります(特に AVFrame
のデータ部分)。成功すれば非NULLのポインタ、失敗すればNULLが返ります。
パケット読み込みとデコードのループ:
cpp
while (av_read_frame(format_ctx, packet) >= 0) {
if (packet->stream_index == video_stream_idx) {
// デコード処理
}
av_packet_unref(packet); // パケット解放(重要!)
}
av_read_frame
は、入力ファイルから圧縮されたデータの塊(パケット)を一つ読み込み、AVPacket
構造体に格納します。この関数はファイル終端に達するまで繰り返し呼び出されます。戻り値が負になるまでループを続けます(負の値にはエラーを示すものと、終端を示す AVERROR_EOF
があります)。
読み込んだパケットが目的の動画ストリームのものであるか (packet->stream_index == video_stream_idx
) 確認し、動画パケットであればデコード処理に進みます。
非常に重要: av_read_frame
が返すパケットは内部バッファへの参照を持つため、パケットのデータを使い終わったら必ず av_packet_unref(packet)
を呼び出して、その参照を解放する必要があります。これを忘れるとメモリリークの原因となります。ループの最後に毎回呼び出すのが一般的なパターンです。
パケット送信とフレーム受信:
“`cpp
ret = avcodec_send_packet(codec_ctx, packet);
// … エラー処理 …
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break; // 受信ループを抜ける
} else if (ret < 0) {
// デコードエラー
goto end_decoding_loop; // 外側のループも含めて終了
}
// フレーム処理...
av_frame_unref(frame); // フレーム解放(重要!)
}
``
avcodec_send_packet
動画や音声のデコードはステートフルなプロセスです。エンコーダーの種類によっては、1つのパケットから複数のフレームが生成されたり、逆に複数のパケットをまとめて1つのフレームを生成したりすることがあります。また、Bフレームのような前後のフレームに依存するフレームを扱う場合、デコーダーはパケットを受け取ってもすぐにはフレームを出力せず、内部バッファに溜め込むことがあります。
そのため、デコードは「パケットをデコーダーに送る ()」と「デコーダーからフレームを受け取る (
avcodec_receive_frame)」という2段階のプロセスで行われます。
avcodec_send_packetは、圧縮された
AVPacketをデコーダーの入力キューに送ります。成功すると0が返ります。キューが満杯で一時的にパケットを受け付けられない場合は
AVERROR(EAGAIN)を返すことがあります。
avcodec_receive_frameは、デコーダーの出力キューからデコード済みの
AVFrameを取り出します。成功すると0が返り、
frameにデータが格納されます。まだ出力可能なフレームがない場合は
AVERROR(EAGAIN)を返し、全てのフレームが出力済み(例えばファイル終端でフラッシュされた後など)の場合は
AVERROR_EOFを返します。エラーが発生した場合は負の値が返ります。
avcodec_send_packet
重要なのは、の後に
avcodec_receive_frameをエラーや
AVERROR(EAGAIN)/
AVERROR_EOFが返るまでループして呼び出す必要がある点です。これにより、1つのパケットから生成された全てのフレームや、バッファリングされていたフレームを取りこぼさずに取得できます。
avcodec_receive_frame
**非常に重要:**が成功して返すフレームは内部バッファへの参照を持つため、フレームのデータを使い終わったら必ず
av_frame_unref(frame)を呼び出して、その参照を解放する必要があります。これを忘れるとメモリリークの原因になります。通常、
avcodec_receive_frame` の内側のループの最後に呼び出します。
フレーム情報の表示:
cpp
std::cout << "Decoded Frame " << frames_decoded_count << ": "
<< "pts=" << frame->pts << ", "
// ... その他の情報 ...
<< std::endl;
AVFrame
構造体には、ピクセルデータ (data
, linesize
) だけでなく、フレームのタイムスタンプ (pts
, pkt_pts
, pkt_dts
)、キーフレームであるか (key_frame
)、ピクセルフォーマット (format
)、解像度 (width
, height
) など、様々な情報が含まれています。これらの情報を表示することで、デコードが正しく行われているか確認できます。av_get_pix_fmt_name
は列挙値であるピクセルフォーマットIDを文字列に変換するヘルパー関数です。
フレーム処理の TODO:
cpp
// TODO: ここでフレームデータ (frame->data, frame->linesize) を処理する
デコードされた生のフレームデータは frame->data
に格納されています。これは通常、平面形式(YUV各成分が別々のメモリブロックに格納されている)またはパック形式(RGBなど、色が連続して格納されている)で格納されています。frame->linesize
は各成分の1ラインあたりのバイト数を示します。ピクセルフォーマットによってデータのレイアウトは異なります。この部分で、例えばピクセルフォーマットをRGBに変換 (swscale
を使用) したり、画像ファイルとして保存したり、OpenGL/Vulkanテクスチャとしてアップロードしたりするなどの処理を行います。このサンプルでは簡単のため処理は省略しています。
デコーダーのフラッシュ:
cpp
ret = avcodec_send_packet(codec_ctx, NULL); // NULL パケットを送る
// ... 残りのフレームを受け取るループ ...
av_read_frame
がファイル終端に達して負の値を返した後、デコーダーの内部バッファにはまだ処理待ちのパケットや出力待ちのフレームが残っている可能性があります。特にBフレームのように遅延して出力されるフレームがある場合は、このフラッシュ処理が必須です。デコーダーをフラッシュするには、avcodec_send_packet
に NULL
パケットを渡します。これにより、デコーダーは内部バッファにあるデータを全て処理し、可能な限りのフレームを出力キューに送ります。その後、avcodec_receive_frame
をエラーまたは AVERROR_EOF
が返るまで再度呼び出すことで、残りのフレームをすべて取得します。
リソースの解放 (Cleanup):
cpp
av_packet_free(&packet);
av_frame_free(&frame);
avcodec_free_context(&codec_ctx);
avformat_close_input(&format_ctx);
FFmpegによって確保された動的リソースは、使い終わったら適切に解放する必要があります。
* av_packet_free(&packet)
: av_packet_alloc
で確保した AVPacket
構造体とその内部バッファを解放します。引数はポインタへのポインタ (&packet
) です。解放後、packet
は NULL
に設定されます。packet
が NULL
であっても安全に呼び出せます。
* av_frame_free(&frame)
: av_frame_alloc
で確保した AVFrame
構造体とその内部バッファを解放します。引数はポインタへのポインタ (&frame
) です。解放後、frame
は NULL
に設定されます。frame
が NULL
であっても安全に呼び出せます。
* avcodec_free_context(&codec_ctx)
: avcodec_alloc_context3
で確保した AVCodecContext
を解放します。引数はポインタへのポインタ (&codec_ctx
) です。解放後、codec_ctx
は NULL
に設定されます。codec_ctx
が NULL
であっても安全に呼び出せます。
* avformat_close_input(&format_ctx)
: avformat_open_input
で開いた入力ファイルを閉じ、関連する AVFormatContext
とその内部リソース(AVStream
など)を解放します。引数はポインタへのポインタ (&format_ctx
) です。解放後、format_ctx
は NULL
に設定されます。format_ctx
が NULL
であっても安全に呼び出せます。
これらの解放関数は、対応する alloc
や open
関数と対になっています。確保したリソースは、エラーが発生した場合でも、プログラム終了時までに必ず解放するように注意深くコーディングする必要があります。goto
を使うと、複数の終了地点(正常終了、各種エラー終了)から一箇所に処理をまとめられるため、クリーンアップ処理の記述が楽になります。
3.3 ビルドと実行
- 上記の
main.cpp
コードを保存します。 - 第2章で説明した方法でFFmpegライブラリを導入し、CMakeLists.txt を設定します。
- CMakeを使ってプロジェクトをビルドします。
bash
# build ディレクトリを作成・移動して
# vcpkg の場合:
cmake .. -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake -A x64
cmake --build . --config Release
# pkg-config (Linux/macOS) の場合:
# cmake ..
# cmake --build .
# 手動パス指定の場合:
# cmake .. -DFFMPEG_INCLUDE_DIRS="..." -DFFMPEG_LIB_DIRS="..."
# cmake --build . - ビルドが成功すると、実行可能ファイル(例:
build/Release/ffmpeg_decoder_sample.exe
またはbuild/ffmpeg_decoder_sample
)が生成されます。 - 実行する際には、入力となる動画ファイルを指定します。例えば、手元にある適当なMP4ファイル(
my_video.mp4
)をターゲットに指定します。WindowsでsharedビルドのDLLをビルドディレクトリにコピーする設定をしている場合、そのまま実行できます。そうでない場合は、FFmpegのbin
フォルダにあるDLL群を実行ファイルと同じ場所にコピーするか、システムのPATHにbin
フォルダを追加する必要があります。
bash
# Linux/macOS の場合
./build/ffmpeg_decoder_sample my_video.mp4
# Windows の場合 (コマンドプロンプト/PowerShell)
.\build\Release\ffmpeg_decoder_sample.exe my_video.mp4
正常に実行できれば、ファイルオープン、ストリーム検索、デコーダーオープン成功のメッセージが表示され、最初の数フレームのデコード情報(PTS, DTS, 解像度, ピクセルフォーマットなど)がコンソールに出力されるはずです。
第4章:さらに進むために
上記のサンプルは、FFmpeg C APIを使った基本的なデコード処理の入り口です。実際のアプリケーション開発では、さらに多くの機能が必要になります。
- ピクセルフォーマット変換 (swscale): デコードされたフレームのピクセルフォーマットは、エンコード時に使用されたフォーマット(YUV420p, YUV422pなど)であることが多く、直接表示や画像処理に使いにくい場合があります。
libswscale
ライブラリを使うと、これらのフレームを一般的なRGB形式などに変換できます。sws_getContext
,sws_scale
関数を使用します。 - オーディオデコード: 動画ファイルには通常、音声ストリームも含まれています。動画と同様の手順で音声ストリームを見つけ、オーディオデコーダーを開き、パケットを読み込んでフレーム(PCMデータなど)にデコードできます。音声処理には
libswresample
が役立つことがあります。 - エンコード: デコードとは逆に、生のフレームデータ(動画フレーム、音声サンプル)を圧縮してパケットを生成し、特定のフォーマットでファイルやストリームとして出力する処理です。
avcodec_find_encoder
,avcodec_send_frame
,avcodec_receive_packet
,av_write_frame
などを使用します。 - フィルタリング (AVFilter): 動画や音声に様々な効果を適用するための強力なモジュールです。リサイズ、クロップ、色調補正、ミキシングなど、多様なフィルタグラフを構築できます。
libavfilter
を使用します。 - 複数ストリームの同期: 動画と音声の両方を扱う場合、再生時にそれらを同期させる必要があります。これは、タイムスタンプ(PTS, DTS)を理解し、適切なタイミングで動画フレームと音声フレームを提示することで実現されます。
- ハードウェアアクセラレーション: FFmpegは多くのプラットフォームでGPUなどを使ったハードウェアアクセラレーションデコード/エンコードをサポートしています。これによりCPU負荷を大幅に下げることができますが、APIの使用法が少し複雑になります。
- メモリ管理の徹底: FFmpegの構造体やバッファは参照カウントで管理されているものが多く、
_unref
や_free
の呼び出しを忘れるとメモリリークやクラッシュの原因になります。特にループ内でパケットやフレームを使い回す場合は、av_packet_unref
とav_frame_unref
が重要です。 - エラーハンドリングの強化: 実際のアプリケーションでは、ファイルが存在しない、フォーマットが壊れている、デコードに失敗するなど、様々なエラーが発生する可能性があります。これらのエラーを適切に検出・処理し、ユーザーにフィードバックしたり、安全にリソースを解放したりする必要があります。
これらのトピックはそれぞれ詳細な知識が必要ですが、この記事で紹介した基本的なデコードのワークフローが理解できていれば、次のステップに進むための基礎はできています。FFmpegの公式ドキュメント(特にDoxygenで生成されたAPIドキュメント)や、コミュニティが提供する豊富なサンプルコードやチュートリアルが学習の助けとなるでしょう。
まとめ
この記事では、FFmpegをC++プロジェクトに導入し、基本的な動画デコード処理を行うためのステップを詳細に解説しました。
まず、FFmpegの基本的なアーキテクチャ(コンテナ、ストリーム、コーデック、パケット、フレーム)と、デコード処理の基本的な流れを説明しました。
次に、FFmpegライブラリをC++開発環境に導入するための主要な方法として、ビルド済みバイナリの使用とパッケージマネージャー(vcpkg, Homebrew, MSYS2)の使用を紹介し、特にC++開発で推奨されるパッケージマネージャーを用いたCMakeプロジェクトでの設定方法を具体的に示しました。ライブラリのリンク設定が最初の大きなハードルとなるため、CMakeLists.txtの記述方法について詳しく解説しました。
そして、入力動画ファイルを開き、動画ストリームを見つけ、デコーダーを開き、パケットを読み込んでフレームにデコードするという一連の処理をC++コードで実装し、そのコードをステップごとに詳細に解説しました。FFmpeg APIの関数呼び出し、エラーハンドリング、そして特に重要なメモリ管理(av_packet_unref
, av_frame_unref
, _free
, _close_input
)について丁寧に説明しました。
FFmpegのC APIは低レベルであるため、最初は難しく感じるかもしれません。しかし、この記事で解説した基本的な構造体と関数の役割、そしてパケット読み込み→パケット送信→フレーム受信というデコードの基本的なループ構造を理解すれば、FFmpegを使ったマルチメディア処理の扉を開くことができます。
この記事が、あなたがFFmpegをC++アプリケーションに組み込み、その強力な機能を活用するための最初の、そして確実な一歩となることを願っています。この基礎を元に、さらに高度な機能へと挑戦していってください。FFmpegの世界は広大ですが、その可能性は無限大です。