1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OBSプラグイン開発 - C++移植とQtによるUI拡張

Last updated at Posted at 2026-01-03

はじめに

前々回の記事 では、obs-plugintemplate を使って Tint Filter を C で実装し、OBS Studio 上で動作することまで確認しました。

また、 前回の記事 では、GitHub Releases を用いたビルド成果物の配布方法について紹介しました。

今回は、その Tint Filter を C++17 で書ける形に移植します。OBS Studio の Developer Guide では、プラグインは一般的に C++ で実装されると説明されています。ただし、OBS 本体 (libobs) がプラグインを読み込むとき探しにくる入口は、obs_module_load() のように 関数名が決め打ち です。C++ は同じ名前の関数を複数作れる都合で、関数名を内部的に少し加工してしまうため、そのままだと OBS が入口を見つけられないことがあります。そこで extern "C" を付けて、関数名を C と同じルールで公開 します。これにより、プラグイン本体は C++ で書きつつ、入口だけは OBS 側が期待する形に揃えられます。

本記事では C++17 を採用します。C++17 は利用実績が多く、Windows/macOS の両方でまず動く土台を揃えやすい規格です。今回の目的は最新機能の活用ではなく、OBS の C API 境界を保ったまま、実装側をクラス化して保守しやすくすることなので、言語規格は保守性と再現性を優先して選びます。C++20 を使いたい場合は、CMake の cxx_std_17cxx_std_20 に変えるだけで基本方針は同じです。

なお、OBS Studio の UI は Qt で実装されています。Qt は C++ 向けのフレームワークであるため、将来プラグインに設定 UI や専用ウィンドウを足すなら、C++ 側へ寄せておくと選択肢が増えます。今回は移植のついでに、Qt を使って「ツール」メニューへ 1 項目を追加し、クリックすると簡単なダイアログを表示できるようにします。

check item added

test plugin dialog

なぜ C++ へ移すのか

「C が悪いから C++ にする」のではなく、フィルタの持ち物 (effect や設定値) が増えたときに破綻しにくい形へ整える ために、最小限の C++ 化を行います。

項目 何が増えると困るか C で起こる問題 C++ で書くメリット
状態管理 effect / param / strength / tint_color など「持ち物」が増える。 struct にまとめて、create/update/render/destroy 関数がその struct を受け取る形になりやすい。 クラスにまとめやすい。初期化・更新・描画・破棄が 1 つの塊として追いやすい。
後片付け 途中で失敗したときの return 経路が増える。 destroy 側での解放漏れ、早期 return 時の片付け忘れが起きやすい。 コンストラクタ / デストラクタに寄せやすく、構造として片付け漏れを減らしやすい。
UI (Qt) 設定 UI やウィンドウを追加したくなる。 Qt を使う時点で C だけだと選択肢が狭い。 obs-plugintemplate の ENABLE_QT と相性が良い。今すぐ UI が要らなくても将来の下準備になる。

この記事でやること

  • obs-plugintemplate のビルド対象を C++17 にする
  • plugin-main.ctint-filter.c.cpp に置き換え
  • Tint Filter を C++ で実装し直し、OBS Studio で動作確認
  • Qt を有効にし「ツール」メニューから開ける簡単なダイアログを追加する

対象読者

  • 前々回の記事 の状態 (Tint Filter が動く状態) まで到達している方
  • C++ は詳しくなくてもよいが、まずは C++ で書ける土台を作りたい方

検証環境

  • OS: Windows 11
  • OBS Studio: 32.0.4 (32. 系)
  • Visual Studio: Visual Studio 2022 Community
  • Windows 11 SDK: 10.0.22621.0
  • MSVC ビルドツール: v143
  • CMake: 4.2.1
  • Git: 2.52.0.windows.1

CMake で C++17 を有効にする

CMake は、ソースファイルの拡張子で C と C++ を判断します。.cpp を含めると C++ コンパイルが動きますが、規格 (C++17) を明示しておくと、環境差で迷いにくくなります。

CMakeLists.txt の変更例

前々回の記事で src/*.cfile(GLOB ...) で拾う形にしている場合は、まず .cpp も拾えるようにします。

# src/ と data/ を自動で拾う (前々回の記事の構成を想定)

file(GLOB PLUGIN_SOURCES CONFIGURE_DEPENDS
  "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c"
  "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
)

# 以降は (前々回の記事どおり) target_sources() などで登録

次に、C++17 を有効にします。add_library(...) によるターゲット作成よりうしろに書きます。 このように cxx_std_17 を使う形にしておけば、CMake 側が適切なコンパイルフラグへ変換してくれます。

# C++17 を要求する
target_compile_features(${CMAKE_PROJECT_NAME} PRIVATE cxx_std_17)

project()LANGUAGES を明示しておくと「C++ を使う意図」が CMake からも読み取れるようになります。ただし、 CMake は LANGUAGES を省略すると既定で CCXX を有効化するため、ここはオプションです。

-project(${_name} VERSION ${_version})
+project(${_name} VERSION ${_version} LANGUAGES C CXX)

.c.cpp にリネームする

拡張子を .cpp にするだけで、Visual Studio 側も C++ として扱います。まずは「C++ としてビルドされる状態」を作ります。

Git Bash で、リポジトリ直下から次を実行します。

git mv src/plugin-main.c src/plugin-main.cpp
git mv src/tint-filter.c src/tint-filter.cpp

git mv は「Git が追跡しているファイル」に対してだけ使えます。もし src/tint-filter.c が未追跡だと、次のように失敗します。


fatal: not under version control, source=src/tint-filter.c, destination=src/tint-filter.cpp

この場合は、先にファイルを追跡させてから git mv してください。

git add src/tint-filter.c
git commit -m "Add tint filter (C version)"
git mv src/tint-filter.c src/tint-filter.cpp

plugin-main.cpp を C++ で書き直す

OBS のモジュールは OBS_DECLARE_MODULE() で宣言し、obs_module_load() を実装してロード時の処理を書きます。前々回の記事では C でしたが、ここを C++ にしても構いません。

まず、 build_x64\<プラグイン名>.sln (以降プラグイン名は test-plugin とします。) を Visual Studio で開きます。構成を RelWithDebInfo x64 として、リビルドします。

configuration

rebuild

再読み込みを促すダイアログが出るので「すべて再読み込み」を選択します。これで、 cpp ファイルが Visual Studio のソリューションエクスプローラー上で表示されるようになります。

reload dialog

solution explorer

src/plugin-main.cpp を次の内容で保存します。extern "C" は「関数名の見え方を C 互換にする指定」です。

#include <obs-module.h>
#include <plugin-support.h>

OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE(PLUGIN_NAME, "en-US")

void register_tint_filter(void);

extern "C" bool obs_module_load(void)
{
	register_tint_filter();
	obs_log(LOG_INFO, "plugin loaded successfully (version %s)", PLUGIN_VERSION);
	return true;
}

extern "C" void obs_module_unload(void)
{
	obs_log(LOG_INFO, "plugin unloaded");
}

Tint Filter を C++17 に移植する

前々回の記事の C 実装は obs_source_info を指定付き初期化で書いていました。これは C としては自然ですが、C++17 では使えません。そこで C++17 では「ゼロ初期化して、必要なフィールドを代入する」書き方に変えます。

src/tint-filter.cpp の実装

src/tint-filter.cpp を次の内容で保存します。

.effect の読み込みに成功しても、パラメータ名の不一致などで gs_effect_get_param_by_name()nullptr を返す可能性があります。その場合にクラッシュさせないため、本記事では Render() 冒頭で param_* の取得結果も含めて検証し、条件を満たさないときは obs_source_skip_video_filter() で安全にバイパスしています。

tint.effect は前々回の記事のまま使います。

#include <obs-module.h>
#include <plugin-support.h>
#include <graphics/effect.h>

#include <cstdint>
#include <new>

class TintFilter final {
public:
	TintFilter(obs_data_t *settings, obs_source_t *source) : context_(source)
	{
		obs_enter_graphics();

		char *path = obs_module_file("effects/tint.effect");
		if (path) {
			effect_ = gs_effect_create_from_file(path, nullptr);
			bfree(path);
		} else {
			obs_log(LOG_WARNING, "[tint_filter] obs_module_file() failed: effects/tint.effect");
		}

		if (effect_) {
			param_tint_color_ = gs_effect_get_param_by_name(effect_, "tint_color");
			param_strength_ = gs_effect_get_param_by_name(effect_, "strength");
		} else {
			obs_log(LOG_WARNING, "[tint_filter] Failed to load effect: effects/tint.effect");
		}

		obs_leave_graphics();

		Update(settings);
	}

	~TintFilter()
	{
		obs_enter_graphics();
		if (effect_) {
			gs_effect_destroy(effect_);
			effect_ = nullptr;
		}
		obs_leave_graphics();
	}

	void Update(obs_data_t *settings)
	{
		const std::uint32_t rgba = static_cast<std::uint32_t>(obs_data_get_int(settings, "tint_color"));
		vec4_from_rgba(&tint_color_, rgba);

		strength_ = static_cast<float>(obs_data_get_double(settings, "strength"));
	}

	void Render()
	{
		if (!effect_ || !param_tint_color_ || !param_strength_) {
			obs_source_skip_video_filter(context_);
			return;
		}

		if (!obs_source_process_filter_begin(context_, GS_RGBA, OBS_ALLOW_DIRECT_RENDERING)) {
			return;
		}
	
		gs_effect_set_vec4(param_tint_color_, &tint_color_);
		gs_effect_set_float(param_strength_, strength_);
		obs_source_process_filter_end(context_, effect_, 0, 0);
	}

	static const char *GetName(void *unused)
	{
		UNUSED_PARAMETER(unused);
		return obs_module_text("TintFilterName");
	}

	static void *Create(obs_data_t *settings, obs_source_t *source)
	{
		return new (std::nothrow) TintFilter(settings, source);
	}

	static void Destroy(void *data) { delete static_cast<TintFilter *>(data); }

	static void UpdateCallback(void *data, obs_data_t *settings)
	{
		static_cast<TintFilter *>(data)->Update(settings);
	}

	static obs_properties_t *Properties(void *unused)
	{
		UNUSED_PARAMETER(unused);

		obs_properties_t *props = obs_properties_create();
		obs_properties_add_color(props, "tint_color", obs_module_text("TintColor"));
		obs_properties_add_float_slider(props, "strength", obs_module_text("Strength"), 0.0, 1.0, 0.01);
		return props;
	}

	static void VideoRender(void *data, gs_effect_t *unused)
	{
		UNUSED_PARAMETER(unused);
		static_cast<TintFilter *>(data)->Render();
	}

private:
	obs_source_t *context_ = nullptr;

	gs_effect_t *effect_ = nullptr;
	gs_eparam_t *param_tint_color_ = nullptr;
	gs_eparam_t *param_strength_ = nullptr;

	struct vec4 tint_color_ = {};
	float strength_ = 0.35f;
};

void register_tint_filter(void)
{
	static obs_source_info info = {};

	info.id = "tint_filter";
	info.type = OBS_SOURCE_TYPE_FILTER;
	info.output_flags = OBS_SOURCE_VIDEO;

	info.get_name = TintFilter::GetName;
	info.create = TintFilter::Create;
	info.destroy = TintFilter::Destroy;
	info.update = TintFilter::UpdateCallback;
	info.get_properties = TintFilter::Properties;
	info.video_render = TintFilter::VideoRender;

	obs_register_source(&info);
}

plugin-support.h は obs-plugintemplate が用意している補助ヘッダです。本記事の構成では、obs_log() を使うソースで #include <plugin-support.h> を併記する必要がありました。そのため、ログ出力を行うソースでは obs-module.h に加えて plugin-support.h も include しています。

OBS Studio で動作確認する

まず、 OBS Studio を閉じます。次に、Visual Studio でリビルドします。問題がなければ次のような出力が得られます。

18:04 で再構築が開始されました...
1>------ すべてのリビルド開始: プロジェクト:ZERO_CHECK, 構成: RelWithDebInfo x64 ------
1>Checking File Globs
1>1>Checking Build System
2>------ すべてのリビルド開始: プロジェクト:plugin-support, 構成: RelWithDebInfo x64 ------
2>Building Custom Rule /test-plugin/CMakeLists.txt
2>plugin-support.c
2>plugin-support.vcxproj -> \test-plugin\build_x64\RelWithDebInfo\plugin-support.lib
3>------ すべてのリビルド開始: プロジェクト:test-plugin, 構成: RelWithDebInfo x64 ------
3>Building Custom Rule /test-plugin/CMakeLists.txt
3>plugin-main.cpp
3>tint-filter.cpp
3>   ライブラリ /test-plugin/build_x64/RelWithDebInfo/test-plugin.lib とオブジェクト /test-plugin/build_x64/RelWithDebInfo/test-plugin.exp を作成中
3>コード生成しています。
3>コード生成が終了しました。
3>test-plugin.vcxproj -> \test-plugin\build_x64\RelWithDebInfo\test-plugin.dll
3>Copy test-plugin to rundir	Copy test-plugin resources to rundir	Installing OBS plugin (cmake --install)
3>-- Installing: C:/ProgramData/obs-studio/plugins/test-plugin/bin/64bit/test-plugin.dll
3>-- Installing: C:/ProgramData/obs-studio/plugins/test-plugin/bin/64bit/test-plugin.pdb
3>-- Up-to-date: C:/ProgramData/obs-studio/plugins/test-plugin/data
3>-- Up-to-date: C:/ProgramData/obs-studio/plugins/test-plugin/data/effects
3>-- Up-to-date: C:/ProgramData/obs-studio/plugins/test-plugin/data/effects/tint.effect
3>-- Up-to-date: C:/ProgramData/obs-studio/plugins/test-plugin/data/locale
3>-- Up-to-date: C:/ProgramData/obs-studio/plugins/test-plugin/data/locale/en-US.ini
3>-- Up-to-date: C:/ProgramData/obs-studio/plugins/test-plugin/data/locale/ja-JP.ini
4>------ すべてのリビルド開始: プロジェクト:ALL_BUILD, 構成: RelWithDebInfo x64 ------
4>Building Custom Rule /test-plugin/CMakeLists.txt
5>------ [すべてリビルド] のスキップ: プロジェクト:INSTALL, 構成: RelWithDebInfo x64 ------
5>プロジェクトはこのソリューション構成に対してビルドするように選択されていません。 
========== すべて再構築: 4 正常終了、0 失敗、1 スキップ ==========
=========== リビルド は 18:04 で完了し、01.436 秒 掛かりました ==========

前回記事のガイドの通りに、 POST_BUILDcmake --install を有効にしている場合は、ビルド直後にプラグインが配置されます。

次のようなエラーが出るときは、 OBS を閉じてから再度リビルドしてください。

重大度レベル	コード	説明	プロジェクト	ファイル	行	抑制状態	詳細
エラー	MSB3073	コマンド "setlocal
cmake.exe -E make_directory /test-plugin/build_x64/rundir/RelWithDebInfo
if %errorlevel% neq 0 goto :cmEnd
cmake.exe -E copy_if_different /test-plugin/build_x64/RelWithDebInfo/test-plugin.dll /test-plugin/build_x64/RelWithDebInfo/test-plugin.pdb /test-plugin/build_x64/rundir/RelWithDebInfo
if %errorlevel% neq 0 goto :cmEnd
:cmEnd
endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone
:cmErrorLevel
exit /b %1
:cmDone
if %errorlevel% neq 0 goto :VCEnd
:VCEnd" はコード 1 で終了しました。	test-plugin	C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VC\v170\Microsoft.CppCommon.targets	166		

OBS Studio を起動し、プラグインがロードされることを確認します。

open logs

check loading on obs

Qt で簡単な UI を追加する

せっかく C++ に対応させたので、ここからは試しに Qt を使って「ツール」メニューへ項目を追加し、クリックしたら QMessageBox を表示するようにしてみましょう。フィルタのプロパティ UI は obs_properties_* だけでも作れますが、専用ウィンドウを持ちたい場合は Qt を直接使えると便利です。

ここで注意したいのは、obs-plugintemplate を clone した直後の GitHub Actions は、既定では Qt 無効の preset でビルドすることが多い点です。その状態で Qt 依存のソースを常時ビルド対象にしたり、Qt 有効時だけ存在する関数を plugin-main.cpp から常に呼び出したりすると、たとえば次のような CI エラーになります。

  • Qt ヘッダが見つからずコンパイルエラーになる (例: QAction が見つからない)
  • register_qt_tools() が未定義でリンクエラーになる (Windows と macOS の両方で起きる)

本記事では、ローカルでは Qt を有効化して UI を作り、CI を含む Qt 無効のビルドでも通る構成にします。

1. ENABLE_QTENABLE_FRONTEND_API を有効にする

obs-plugintemplate では、CMake preset 経由で Qt とフロントエンド API を切り替えられます。

  • ENABLE_QT: Qt のリンク
  • ENABLE_FRONTEND_API: 「ツール」メニューなど、OBS 本体 UI と連携する API のリンク

本記事では cmake --preset を前提に進めるため、前々回の記事で作成した CMakeUserPresets.json に Qt 用 preset を追加します。

まず、リポジトリ直下の CMakeUserPresets.json を、次のように更新します。

{
  "version": 8,
  "configurePresets": [
    {
      "name": "windows-x64-local",
      "inherits": "windows-x64",
      "cacheVariables": {
        "CMAKE_INSTALL_PREFIX": "C:/ProgramData/obs-studio/plugins"
      }
    },
    {
      "name": "windows-x64-local-qt",
      "inherits": "windows-x64-local",
      "cacheVariables": {
        "ENABLE_QT": "ON",
        "ENABLE_FRONTEND_API": "ON"
      }
    }
  ]
}

次に、Qt を有効にしたときだけ Qt 実装をビルドし、Qt を無効にしたときは no-op 実装をビルドするように、ソース構成を先に整えます。

まず、2 つのファイルを用意します。

touch src/qt-tools.cpp
touch src/qt-tools-stub.cpp

続いて CMakeLists.txt 側で、file(GLOB ...) の収集結果から qt-tools*.cpp を除外し、ENABLE_QTENABLE_FRONTEND_API の組み合わせに応じてどちらか一方だけを追加します。

file(
  GLOB PLUGIN_SOURCES
  CONFIGURE_DEPENDS
  "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c"
  "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
)

# Qt の有無で差し替えるため、GLOB には含めない
list(FILTER PLUGIN_SOURCES EXCLUDE REGEX ".*/qt-tools(-stub)?\\.cpp$")

if(ENABLE_QT AND ENABLE_FRONTEND_API)
  list(APPEND PLUGIN_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/qt-tools.cpp")
else()
  list(APPEND PLUGIN_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/qt-tools-stub.cpp")
endif()

# 以降は、PLUGIN_SOURCES を target_sources() または add_library() に渡す

これで、ローカルの Qt 有効ビルドでは qt-tools.cpp が入り、CI を含む Qt 無効ビルドでは qt-tools-stub.cpp が入ります。結果として、Qt を入れていない環境でもコンパイルとリンクが通るようになります。

次に、Qt 用 preset でソリューションを再生成します。

cmake --preset windows-x64-local-qt

再生成した test-plugin.sln を開きます。すでに Visual Studio が起動中であれば、再読み込みを促すダイアログが出るので、すべて再読み込み します。

2. 「ツール」メニューへ項目を追加する

src/qt-tools.cpp は、Qt が有効なときだけコンパイルされる本体実装です。内容を次のようにします。

#include <obs-module.h>
#include <obs-frontend-api.h>
#include <plugin-support.h>

#include <QAction>
#include <QMainWindow>
#include <QMessageBox>
#include <QObject>

static QAction *g_tools_action = nullptr;

void register_qt_tools()
{
	if (g_tools_action) {
		return;
	}

	auto *main_window = static_cast<QMainWindow *>(obs_frontend_get_main_window());
	if (!main_window) {
		obs_log(LOG_WARNING, "[tint_filter] obs_frontend_get_main_window() returned nullptr");
		return;
	}

	g_tools_action = static_cast<QAction *>(obs_frontend_add_tools_menu_qaction(obs_module_text("TintToolsMenu")));
	if (!g_tools_action) {
		obs_log(LOG_WARNING, "[tint_filter] obs_frontend_add_tools_menu_qaction() returned nullptr");
		return;
	}

	QObject::connect(g_tools_action, &QAction::triggered, main_window, [main_window]() {
		QMessageBox::information(main_window, obs_module_text("TintToolsTitle"),
					 obs_module_text("TintToolsBody"));
	});
}

void unregister_qt_tools()
{
	g_tools_action = nullptr;
}

obs_frontend_add_tools_menu_qaction()void * を返すため、QAction * にキャストしています。ダイアログの親は obs_frontend_get_main_window() で取得した QMainWindow * を使います。

2026/01 時点の Frontend API には obs_frontend_add_tools_menu_qaction() に対応する削除 API が公開されていません。そのため本記事では、obs_module_unload() で QAction を明示的に取り除きません。二重登録を防ぐ目的で、保持ポインタを nullptr に戻すだけにします。

次に src/qt-tools-stub.cpp は、Qt が無効なビルドでもリンクを通すための no-op 実装です。内容を次のようにします。

#include <obs-module.h>
#include <plugin-support.h>

void register_qt_tools() {}
void unregister_qt_tools() {}

qt-tools.cppqt-tools-stub.cpp は、preset によってビルド対象が切り替わります。Qt 有効 preset で生成した Visual Studio ソリューションでは qt-tools.cpp のみが表示され、Qt 無効 preset で生成したソリューションでは qt-tools-stub.cpp のみが表示されます。

3. plugin-main.cpp から呼び出す

src/plugin-main.cpp から呼び出します。宣言と呼び出しだけ足してください。

void register_tint_filter(void);

void register_qt_tools(void);
void unregister_qt_tools(void);

extern "C" bool obs_module_load(void)
{
	register_tint_filter();
	register_qt_tools();

	obs_log(LOG_INFO, "plugin loaded successfully (version %s)", PLUGIN_VERSION);
	return true;
}

extern "C" void obs_module_unload(void)
{
	unregister_qt_tools();
	obs_log(LOG_INFO, "plugin unloaded");
}

Qt を無効にしたビルドでも qt-tools-stub.cpp が入るため、register_qt_tools() の未定義シンボルで CI が落ちる問題を避けられます。

4. ロケール文字列を追加する

「ツール」メニューとダイアログの文言を追加します。

data/locale/en-US.ini

TintToolsMenu="Tint Filter: About"
TintToolsTitle="Tint Filter"
TintToolsBody="This is a sample Qt dialog opened from Tools menu."

data/locale/ja-JP.ini

TintToolsMenu="Tint Filter: 情報"
TintToolsTitle="Tint Filter"
TintToolsBody="「ツール」メニューから開く、サンプルの Qt ダイアログです。"

5. 動作確認

OBS Studio を起動し、上部メニューの「ツール」に Tint Filter: 情報 が追加されていることを確認します。

check item added

クリックするとダイアログが表示されます。

test plugin dialog

これで、フィルタ本体は obs_source_info のコールバックで動かしつつ、必要なときだけ Qt で補助 UI を追加できる状態になります。

まとめ

Tint Filter を題材に、obs-plugintemplate を C++17 で書ける形へ移植しました。OBS がプラグインを探す入口は obs_module_load() のように関数名が固定なので、C++ 側では extern "C" を付けて C と同じ名前で公開します。C++17 では C の指定付き初期化が使えないため、obs_source_info はゼロ初期化してから必要なフィールドを代入しました。最後に Qt とフロントエンド API を有効にし「ツール」メニューから開ける簡単なダイアログを追加しました。

次回 は音声フィルタについて執筆します。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?