Media Foundation で WebCam 静止画キャプチャを試みる

ググりながら、調べながら作っていると、できました :thumbsup: 

![2020-01-09_19h43_18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364126/cee13efa-fd3e-08c6-0f76-90d02726674d.png)

一番参考になったのはこちら: [Media Foundation でカメラ画像を取得](http://codeit.blog.fc2.com/blog-entry-5.html)

つまみ食い形式で解体していきます…

## .NET Framework 4.7.2 (C#) WinForms プロジェクトにて

http://mfnet.sourceforge.net/ から `MFLibv3_1.zip` を入手・解凍し、つぎの 2 つを参照に追加:

- `MediaFoundation.dll`
- `MediaFoundation.Extension.dll`

### ComReleaser

`Marshal.ReleaseComObject` を `IDisposable` の発動に合わせて呼び出すユーティリティーです。

```ComReleaser.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace WebCamCapPic.Utils
{
    class ComReleaser : IDisposable
    {
        private List<object> items = new List<object>();

        public void Add(object obj)
        {
            items.Add(obj);
        }

        public void Dispose()
        {
            foreach (var one in items)
            {
                Marshal.ReleaseComObject(one);
            }

            items.Clear();
        }
    }
}
```

### Media Foundation の初期化

特にやっていません

### デバイス一覧の列挙

ビデオキャプチャ能力のあるデバイスの列挙です。

![2020-01-09_19h50_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364126/07cec762-ae03-c672-faaf-8a9799af3515.png)

```Form1.cs
        private void Form1_Load(object sender, EventArgs e)
        {
            // http://codeit.blog.fc2.com/blog-entry-5.html
            MF.EnumVideoDeviceSources(out IMFActivate[] devices);
            foreach (var device in devices)
            {
                device.GetString(MFAttributesClsid.MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, out string name);
                device.GetString(MFAttributesClsid.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, out string symLink);
                device.GetGUID(MFAttributesClsid.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, out Guid type);
                devicesCombo.Items.Add(
                    new DeviceItem
                    {
                        Name = name,
                        Type = type,
                        SymLink = symLink,
                    }
                );
            }
        }

        class DeviceItem
        {
            public string Name { get; internal set; }
            public Guid Type { get; internal set; }
            public string SymLink { get; internal set; }

            public override string ToString() => Name;
        }
```

`SymLink` には `\\?\usb#vid_046d&pid_0825&mi_00#7&31e6e3b&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\{bbefb6c7-2fc4-4139-bb8b-a58bba724083}` のような内容が入ります。

## メディアタイプの列挙

デバイスを特定したら、つぎに対応フォーマットの列挙です

![2020-01-09_19h53_46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364126/bc4c04ab-019d-17b1-0138-a57147621c9c.png)

```Form1.cs
        private void devicesCombo_SelectedIndexChanged(object sender, EventArgs e)
        {
            var item = (DeviceItem)devicesCombo.SelectedItem;
            if (item != null)
            {
                mediaCombo.Items.Clear();

                using (var releaser = new ComReleaser())
                {
                    MF.CreateVideoDeviceSource(item.SymLink, out IMFMediaSource source);
                    releaser.Add(source);
                    source.CreatePresentationDescriptor(out IMFPresentationDescriptor presDesc);
                    releaser.Add(presDesc);
                    presDesc.GetStreamDescriptorCount(out int descCount);
                    for (int descIndex = 0; descIndex < descCount; descIndex++)
                    {
                        presDesc.GetStreamDescriptorByIndex(descIndex, out bool selected, out IMFStreamDescriptor strmDesc);
                        releaser.Add(strmDesc);
                        strmDesc.GetMediaTypeHandler(out IMFMediaTypeHandler handler);
                        releaser.Add(handler);
                        handler.GetMediaTypeCount(out int typeCount);
                        for (int typeIndex = 0; typeIndex < typeCount; typeIndex++)
                        {
                            handler.GetMediaTypeByIndex(typeIndex, out IMFMediaType type);
                            releaser.Add(type);
                            type.GetSize(MFAttributesClsid.MF_MT_FRAME_SIZE, out uint width, out uint height);
                            type.GetGUID(MFAttributesClsid.MF_MT_SUBTYPE, out Guid subType);
                            type.GetUINT32(MFAttributesClsid.MF_MT_DEFAULT_STRIDE, out uint stride);
                            type.GetUINT32(MFAttributesClsid.MF_MT_SAMPLE_SIZE, out uint sampleSize);

                            mediaCombo.Items.Add(
                                new MediaItem
                                {
                                    Name = $"#{descIndex}.{typeIndex}: {width}x{height}, {GetSubTypeName(subType)}, {((int)stride)}, {sampleSize}",
                                    DescIndex = descIndex,
                                    TypeIndex = typeIndex,
                                    Width = (int)width,
                                    Height = (int)height,
                                    Stride = (int)stride,
                                    SampleSize = (int)sampleSize,
                                    DeviceItem = item,
                                    SubType = subType,
                                }
                            );
                        }
                    }
                }
            }
        }

        class MediaItem
        {
            public string Name { get; internal set; }
            public int Width { get; internal set; }
            public int Height { get; internal set; }
            public int Stride { get; internal set; }
            public int SampleSize { get; internal set; }
            public DeviceItem DeviceItem { get; internal set; }
            public int DescIndex { get; internal set; }
            public int TypeIndex { get; internal set; }
            public Guid SubType { get; internal set; }

            public override string ToString() => Name;
        }

        private string GetSubTypeName(Guid subType)
        {
            foreach (var field in typeof(MFMediaType).GetFields(BindingFlags.Static | BindingFlags.Public))
            {
                if (field.FieldType == typeof(Guid))
                {
                    if ((Guid)field.GetValue(null) == subType)
                    {
                        return field.Name;
                    }
                }
            }
            return null;
        }

```

### 静止画の連続キャプチャ

- `item.DeviceItem.SymLink` から一気に `IMFMediaSource source` 獲得まで進めます。
- さて。厄介なのは `RGB24` 以外の画像形式が来た場合です。`I420` などです。
- 一応 [VideoProcessorMFT](https://docs.microsoft.com/en-us/windows/win32/medfound/video-processor-mft) を用いて `RGB24` へ変換するようにしています。使い方はググりながら、試行錯誤しながら会得しました😅

```Form1.cs
        private void CaptureStillImages(MediaItem item)
        {
            using (var releaser = new ComReleaser())
            {
                MF.CreateVideoDeviceSource(item.DeviceItem.SymLink, out IMFMediaSource source);
                releaser.Add(source);
                source.CreatePresentationDescriptor(out IMFPresentationDescriptor presDesc);
                releaser.Add(presDesc);
                presDesc.GetStreamDescriptorByIndex(item.DescIndex, out bool selected, out IMFStreamDescriptor strmDesc);
                releaser.Add(strmDesc);
                strmDesc.GetMediaTypeHandler(out IMFMediaTypeHandler handler);
                releaser.Add(handler);
                handler.GetMediaTypeByIndex(item.TypeIndex, out IMFMediaType type);
                handler.SetCurrentMediaType(type);

                MF.CreateSourceReaderFromMediaSource(source, out IMFSourceReader reader);
                if (reader == null)
                {
                    return;
                }
                releaser.Add(reader);

                IMFTransform transform = null;
                MFTOutputDataBuffer[] outSamples = null;
                IMFSample outRgb24Sample = null;
                IMFMediaBuffer outRgb24Buffer = null;

                int rgbSize = item.Width * item.Height * 3;

                var needToConvert = item.SubType != MFMediaType.RGB24;
                if (needToConvert)
                {
                    var processor = new VideoProcessorMFT();
                    releaser.Add(processor);
                    transform = (IMFTransform)processor;
                    HR(transform.SetInputType(0, type, MFTSetTypeFlags.None));
                    var rgbMediaType = MF.CreateMediaType();
                    releaser.Add(rgbMediaType);
                    HR(type.CopyAllItems(rgbMediaType));
                    HR(rgbMediaType.SetGUID(MFAttributesClsid.MF_MT_SUBTYPE, MFMediaType.RGB24));
                    HR(rgbMediaType.SetUINT32(MFAttributesClsid.MF_MT_DEFAULT_STRIDE, 3 * item.Width));
                    HR(rgbMediaType.SetUINT32(MFAttributesClsid.MF_MT_SAMPLE_SIZE, rgbSize));
                    HR(transform.SetOutputType(0, rgbMediaType, MFTSetTypeFlags.None));

                    outSamples = new MFTOutputDataBuffer[1];
                    outSamples[0] = new MFTOutputDataBuffer();
                    outRgb24Sample = MF.CreateSample();
                    releaser.Add(outRgb24Sample);
                    outRgb24Buffer = MF.CreateMemoryBuffer(rgbSize);
                    releaser.Add(outRgb24Buffer);
                    outRgb24Sample.AddBuffer(outRgb24Buffer);
                    outSamples[0].pSample = Marshal.GetIUnknownForObject(outRgb24Sample);
                }

                int frames = 0;
                while (!stopEvent.WaitOne(0))
                {
                    var hrRS = reader.ReadSample(
                        (int)MF_SOURCE_READER.AnyStream,
                        MF_SOURCE_READER_CONTROL_FLAG.None,
                        out int streamIndex,
                        out MF_SOURCE_READER_FLAG flags,
                        out long timeStamp,
                        out IMFSample sample
                    );

                    if (sample != null)
                    {
                        try
                        {
                            IMFSample rgbSample = sample;

                            if (transform != null)
                            {
                                while (true)
                                {
                                    var hrPO = transform.ProcessOutput(
                                        MFTProcessOutputFlags.None,
                                        1,
                                        outSamples,
                                        out ProcessOutputStatus status
                                    );
                                    if (hrPO.Succeeded())
                                    {
                                        ConsumeBuffer(outRgb24Buffer, item);
                                        frames++;
                                    }
                                    else
                                    {
                                        break;
                                    }
                                }
                                var hrPI = transform.ProcessInput(0, sample, 0);
                                continue;
                            }

                            rgbSample.GetBufferByIndex(0, out IMFMediaBuffer buff);
                            if (ConsumeBuffer(buff, item))
                            {
                                frames++;
                            }
                            else
                            {
                                return;
                            }
                        }
                        finally
                        {
                            Marshal.ReleaseComObject(sample);
                        }
                    }
                }
            }
        }

        private bool ConsumeBuffer(IMFMediaBuffer buff, MediaItem item)
        {
            buff.Lock(out IntPtr ptr, out int maxLen, out int curLen);
            try
            {
                Bitmap pic = new Bitmap(item.Width, item.Height, PixelFormat.Format24bppRgb);
                var bitmapData = pic.LockBits(
                    new Rectangle(0, 0, pic.Width, pic.Height),
                    ImageLockMode.WriteOnly,
                    PixelFormat.Format24bppRgb
                );
                try
                {
                    byte[] temp = new byte[curLen];
                    Marshal.Copy(ptr, temp, 0, temp.Length);
                    Marshal.Copy(temp, 0, bitmapData.Scan0, Math.Min(temp.Length, bitmapData.Stride * bitmapData.Height));
                }
                finally
                {
                    pic.UnlockBits(bitmapData);
                }
                if (item.Stride < 0)
                {
                    pic.RotateFlip(RotateFlipType.RotateNoneFlipY);
                }
                try
                {
                    Invoke((Action<Bitmap>)UpdatePreview, pic);
                    return true;
                }
                catch (ObjectDisposedException)
                {
                    return false;
                }
            }
            finally
            {
                buff.Unlock();
            }
        }

        private void HR(HResult hr)
        {
            Debug.Assert(hr.Succeeded());
        }
```

- 画像のビットマップは `IMFSample` の中に `IMFMediaBuffer` を 1 個以上含む形で流れてきます。
- 画像の形式は `IMFMediaType` のメソッドを呼び出して必要情報を取得します。
- `IMFMediaType` の `MFAttributesClsid.MF_MT_SUBTYPE` が `MFMediaType.RGB24` であれば無変換で結構ですが、
  `MFMediaType.I420` などが来ると Gdiplus が取り扱いできる形に変えてあげなければなりません。
- それもググると計算式が出てくるので実装は不可能ではありませんが、手間なのと DirectShow の利用経験から「可能」という事が事前に分かっていたので方法を調べることにしました。
- 前述の `VideoProcessorMFT` を `new` して使います。
- `SetInputType` と `SetOutputType` で、フォーマットを教えてあげます。
- `IMFMediaType` には複製メソッドがありません。`SetGUID` を使うと、既存の `IMFMediaType` が書き換わってしまいます。多分…
- そこで `MF.CreateMediaType()` で新規 `IMFMediaType` を作成し、`CopyAllItems` で内容をコピーしてもらいます。
- コピー先の `IMFMediaType` を `MFMediaType.RGB24` に設定し、`SetOutputType` へ渡します。

変換:

- `IMFTransform.ProcessInput` と `IMFTransform.ProcessOutput` を使うんですが、これがまた面白い。
- やり方はつぎのような感じです。

```
  while (true) {
    if (xxx.ProcessOutput(outputSample) == 成功) {
      ConsumeBuffer(outputBuffer);
    }
    else {
      // MF_E_TRANSFORM_NEED_MORE_INPUT が返されているものと仮定。
      break;
    }
  }
  xxx.ProcessInput(inputSample);
```

- ちなみに `inputSample` は使い終わったあとに `Marshal.ReleaseComObject(sample)` しておかないと、いずれ `ReadSample` がブロックします。
- わたしが試した時は 18 個分 `inputSample` をドレインするとブロックしました。
- こういう不思議な仕組みは DirectShow でもありましたね…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364126/a183bd89-7f51-6568-7f7e-50e13ad97c6e.png)

という感じで重要事項を書きだしたので、これで終わります👋