ググりながら、調べながら作っていると、できました
一番参考になったのはこちら: Media Foundation でカメラ画像を取得
つまみ食い形式で解体していきます…
.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 の初期化
特にやっていません
デバイス一覧の列挙
ビデオキャプチャ能力のあるデバイスの列挙です。
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}
のような内容が入ります。
メディアタイプの列挙
デバイスを特定したら、つぎに対応フォーマットの列挙です
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 を用いて
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 でもありましたね…
という感じで重要事項を書きだしたので、これで終わります👋