Vfwなcodecの設定画面を呼びたい

# 動機

現在、Caputraというデスクトップキャプチャソフトに、AviUtlを使っている人ならおなじみの可逆圧縮codecである、[Ut Video Codec Suiteを対応させる試みを行っています](https://github.com/MathewSachin/Captura/issues/407)。

その過程で、codecの設定をわざわざレジストリをいじるのは面倒だし、設定画面とか呼べないの？と思ったのですね。

例えばAviUtlなら
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/7a7584ce-0441-acc3-db26-bcd6de7d4779.png)
この設定ボタンから呼べる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/5e858a28-b605-0a14-ed1e-0c7828c8e4de.png)
こういうやつです。

このウィンドウはAviUtlに固有のものではなくて他のソフトからも同じものが出るということは、Ut Video Codec Suite側で生成しているものだと推測できます。

# Ut Video Codec Suiteの実装を読む

幸いなことにソースコードはGitHubで公開されているので読んでいきます。

[https://github.com/umezawatakeshi/utvideo](https://github.com/umezawatakeshi/utvideo)

するとそういえばVFW pluginだったなと思い出してきました。

VfwというのはVideo for Windowsの略で、Win32API触ったことある人ならわかるであろう、プロージャーを書いてメッセージを受け取っていくタイプの書き方で、動画のエンコードやデコードなどなどを実装したdllを登録することで、いろいろなソフトウェアからそれが呼べる仕組みのことです。Windows Media Playerなんかはこれをつかって再生していたと思います。Windows 10標準搭載の動画アプリはMicrosoft Media Foundationという別のものを使っていますが。

なんか検索してると某有名ゲームエンジンの関連のVexe Frameworkのほうが引っかかってくるようですが・・・。

## 設定画面の呼び出し

いろいろ読んでいくと、次のような箇所を見つけました。つまり`ICM_CONFIGURE`を受け取ったプロージャーは、`DialogBoxParam`によってWindowを生成しているではないですか。

```cpp:utv_vcm/DriverProc.cpp
	case ICM_CONFIGURE:
		if (lParam1 == -1)
			return pCodec->QueryConfigure();
		else
			return pCodec->Configure((HWND)lParam1);
```

https://github.com/umezawatakeshi/utvideo/blob/077a6a6202a3b5d60d1716559759210f6cedd795/utv_vcm/DriverProc.cpp#L45-L49

```cpp:utv_vcm/VCMCodec.cpp
LRESULT CVCMCodec::Configure(HWND hwnd)
{
	if (m_mode != ICMODE_COMPRESS)
		return ICERR_UNSUPPORTED;

	return m_pCodec->Configure(hwnd) == 0 ? ICERR_OK : ICERR_UNSUPPORTED;
}
```

https://github.com/umezawatakeshi/utvideo/blob/077a6a6202a3b5d60d1716559759210f6cedd795/utv_vcm/VCMCodec.cpp#L127-L133

```cpp:utv_core/UL00Codec.cpp
INT_PTR CUL00Codec::Configure(HWND hwnd)
{
	DialogBoxParam(hModule, MAKEINTRESOURCE(IDD_UL00_CONFIG), hwnd, DialogProc, (LPARAM)this);
	return 0;
}
```

https://github.com/umezawatakeshi/utvideo/blob/077a6a6202a3b5d60d1716559759210f6cedd795/utv_core/UL00Codec.cpp#L31-L35

つまりVfw codecに対して`ICM_CONFIGURE`メッセージを投げられれば良さそうです。

ただし

- `m_mode != ICMODE_COMPRESS`より、`m_mode`は`ICMODE_COMPRESS`でなければならない
- `lParam1 == -1`より、親ウィンドウのハンドルが必須

## 初期化

しかし、その前に`pCodec`とか`m_mode`とかいう変数が見えます。これらはどうやって初期化されているのでしょうか？

```cpp
LRESULT CALLBACK DriverProc(DWORD_PTR dwDriverId, HDRVR hdrvr, UINT uMsg, LPARAM lParam1, LPARAM lParam2)
{
	CVCMCodec *pCodec = (CVCMCodec *)dwDriverId;
```

変数の定義を見るだけでは、プロージャーの引数から渡ってくるんだなとしかわかりません。

```cpp:utv_vcm/DriverProc.cpp
	case DRV_OPEN:
		return (LRESULT)CVCMCodec::Open((ICOPEN *)lParam2);

	case DRV_CLOSE:
		if (pCodec != NULL)
			delete pCodec;
		return TRUE;
```

https://github.com/umezawatakeshi/utvideo/blob/077a6a6202a3b5d60d1716559759210f6cedd795/utv_vcm/DriverProc.cpp#L26-L32

もう少し下を見ると`DRV_CLOSE`を受け取ったときに、`pCodec`を`delete`しているところが見つかりました。さらにその上に`DRV_OPEN`というそれっぽいのがあります。`CVCMCodec::Open`を見てみましょう。

```cpp:utv_vcm/VCMCodec.cpp
CVCMCodec *CVCMCodec::Open(ICOPEN *icopen)
{
	if (IsLogWriterInitializedOrDebugBuild())
	{
		if (icopen != NULL)
			LOGPRINTF("CVCMCodec::Open(icopen=%p, icopen->fccType=%08X, icopen->fccHandler=%08X icopen->dwFlags=%08X)", icopen, icopen->fccType, icopen->fccHandler, icopen->dwFlags);
		else
			LOGPRINTF("CVCMCodec::Open(icopen=NULL)");
	}

	union
	{
		DWORD fccHandler;
		char fccChar[4];
	};

	DWORD icmode;

	if (icopen != NULL)
	{
		if (icopen->fccType != ICTYPE_VIDEO)
			return NULL;
		fccHandler = icopen->fccHandler;
		// なぜか小文字で渡されることがあるので、最初に大文字化しておく。
		for (int i = 0; i < 4; i++)
			fccChar[i] = toupper(fccChar[i]);

		icopen->dwError = ICERR_UNSUPPORTED;
		switch (icopen->dwFlags)
		{
		case ICMODE_COMPRESS:
			if (CheckInterfaceDisabledAndLog("VCM", "Encoder"))
				return NULL;
			break;
		case ICMODE_DECOMPRESS:
			if (CheckInterfaceDisabledAndLog("VCM", "Decoder"))
				return NULL;
			break;
		case ICMODE_QUERY:
			if (CheckInterfaceDisabledAndLog("VCM", "Query"))
				return NULL;
			break;
		default:
			return NULL;
		}

		icopen->dwError = ICERR_OK;
		icmode = icopen->dwFlags;
	}
	else
	{
		fccHandler = (DWORD)-1;
		icmode = 0;
	}

	return new CVCMCodec(fccHandler, icmode);
}
```

https://github.com/umezawatakeshi/utvideo/blob/077a6a6202a3b5d60d1716559759210f6cedd795/utv_vcm/VCMCodec.cpp#L31-L87

細かいところは読み飛ばすとして、まず`icmode = icopen->dwFlags;`が目に入ります。さっき言ってた変数ですね！さらにこの関数は`return new CVCMCodec(fccHandler, icmode);`してます。確かに`DRV_OPEN`と`DRV_CLOSE`は対応関係にあることがわかります。

# どうやってVfwなcodecにメッセージを投げるか

つまり、どうにかしてコーデックのプロージャーにメッセージを投げられればいいわけです。

ただ、検索の仕方が悪くてなかなかたどり着けず、

https://github.com/baSSiLL/SharpAvi

のソースコードを読み漁ってました。

結局使う関数は

- [`ICOpen`](https://docs.microsoft.com/ja-jp/windows/win32/api/vfw/nf-vfw-icopen)
- [`ICSendMessage`](https://docs.microsoft.com/ja-jp/windows/win32/api/vfw/nf-vfw-icsendmessage)
- [`ICClose`](https://docs.microsoft.com/ja-jp/windows/win32/api/vfw/nf-vfw-icclose)

の3つだとわかりました。

`ICOpen`は次のようなプロトタイプ宣言で定義されています。

```cpp
HIC VFWAPI ICOpen(
  DWORD fccType,
  DWORD fccHandler,
  UINT  wMode
);
```

`fcc`というのが見えますが、これは`FOURCC`のことです。

> [FourCC - Wikipedia](https://ja.wikipedia.org/wiki/FourCC)
> FourCC (フォーシーシー) とは、データフォーマットを一意に識別するための4バイトの並びである（four-character code の意）。

`fccType`には`VIDC`という文字列をFourCCとして整数に変換したもの、`wMode`には先に述べたように`ICMODE_COMPRESS`を渡すとして、`fccHandler`には何を渡せばいいのでしょうか？今回はUt Video Codec Suiteの設定画面を呼びたいので、早速FourCCを調べましょう。

といっても公式で
[Ut Video Codec Suite 21.2.0 readme （日本語）FourCC 一覧](http://umezawa.dyndns.info/archive/utvideo/utvideo-21.2.0-readme.ja.html#fourcc)
にまとめられているのでそれを見るだけですね。

とりあえず今回は`ULH2`にしましょう。

# 実装

## arikitari na world!

親ウィンドウがないといけないので、雑にWPFで作りましょう。

```xml:MainWindow.xaml
<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="VfwConfigCall" Height="100" Width="300">
    <Grid>
        <Button Margin="10" Click="SettingClick">Setting</Button>
    </Grid>
</Window>
```


```csharp:MainWindow.xaml.cs
using System.Windows;
namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void SettingClick(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("arikitari na world!");
        }
    }
}
```

まずはこれで全世界の人がまずは表示させるであろう恒例の`arikitari na world!`を表示させて動いたことを確認したら本実装に入りましょう。

## 定数

`vfw.h`をみつつ必要なものを取ってきましょう。

```csharp
        private const int DRV_USER = 0x4000;
        private const int ICM_RESERVED_LOW = DRV_USER + 0x1000;
        private const int ICM_RESERVED = ICM_RESERVED_LOW;
        private const int ICM_CONFIGURE = ICM_RESERVED + 10;
        private const int ICMODE_COMPRESS = 1;
```

## DLL読み込み

DLLを読まないと関数が呼べないので読み込んでおきましょう。

```csharp
        private const string VFW_DLL = "msvfw32.dll";
        [DllImport(VFW_DLL, CallingConvention = CallingConvention.Winapi)]
        public static extern IntPtr ICOpen(uint fccType, uint fccHandler, int mode);
        [DllImport(VFW_DLL, CallingConvention = CallingConvention.Winapi)]
        public static extern int ICClose(IntPtr handle);
        [DllImport(VFW_DLL, CallingConvention = CallingConvention.Winapi)]
        public static extern int ICSendMessage(IntPtr handle, int message, IntPtr param1, IntPtr param2);
```

## 設定画面の呼び出し

あとはclickイベントハンドラを書き換えて設定画面を呼びましょう。FourCCの計算が面倒だったので`SharpAvi`をNuGetでプロジェクトに追加しておきます。

```csharp
using System;
using System.Runtime.InteropServices;
using System.Windows;
using SharpAvi;

        private void SettingClick(object sender, RoutedEventArgs e)
        {
            var compressorHandle = ICOpen((uint)KnownFourCCs.CodecTypes.Video, (uint)new FourCC("ULH2"), ICMODE_COMPRESS);
            if (compressorHandle == IntPtr.Zero)
            {
                return;
            }
            try
            {
                var re = ICSendMessage(compressorHandle, ICM_CONFIGURE, new System.Windows.Interop.WindowInteropHelper(this).Handle, IntPtr.Zero);
            }
            finally
            {
                ICClose(compressorHandle);
            }
        }
```

# 結果

実行する前に忘れずにUt Video Codec Suiteをインストールしておきましょう。

[或るプログラマの一生 » Ut Video Codec Suite](http://umezawa.dyndns.info/wordpress/?cat=28)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/9da6c09a-ffd7-18af-70a8-69278cb2b2f4.png)

無事に呼び出せました。

# 余談: FourCCとcodecの関連付けの管理

FourCCとcodecの関連付けはレジストリで行われている。例えばUt Video Codec Suiteの場合、次のような項目が登録される。

```config
REGEDIT4

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Drivers32]
"VIDC.ULRA"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULRG"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULY0"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULY2"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULY4"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULH0"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULH2"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULH4"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UQY0"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UQY2"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UQRG"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UQRA"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMRA"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMRG"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMY2"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMY4"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMH2"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMH4"="C:\\Windows\\system32\\utv_vcm.dll"

[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Drivers32]
"VIDC.ULRA"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULRG"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULY0"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULY2"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULY4"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULH0"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULH2"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.ULH4"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UQY0"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UQY2"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UQRG"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UQRA"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMRA"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMRG"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMY2"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMY4"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMH2"="C:\\Windows\\system32\\utv_vcm.dll"
"VIDC.UMH4"="C:\\Windows\\system32\\utv_vcm.dll"
```

ではコーデックのあるFourCCを列挙するにはどうすればいいのか。レジストリを自分で読みたくはない。

どうやら`ICInfo`という関数を使えばいいようだ。使い方のサンプルが
[Locating and Opening Compressors and Decompressors - Win32 apps | Microsoft Docs](https://docs.microsoft.com/ja-jp/windows/win32/multimedia/locating-and-opening-compressors-and-decompressors)
にあった。