おはよう君需要なし

求不得苦な日々

C#からHDMIキャプチャを使おう【MonsterX u3.0r】

はじめに

DirectShowを使うとHDMIをキャプチャしたデータをC#でそのまま使うことができます。カメラから出力されたデータをリアルタイムで処理したり使い方は色々!

MonsterX u3.0rとは

これは、HDMIのキャプチャデバイスです。他にも安いのが結構あるんですが、これは1920x1080 60Hzを比圧縮で取ってこられるので画質を優先したい人などには結構重宝されている製品だと思います。まぁ非圧縮のキャプチャデバイスなんてそんなに時代で変わるものでもないので買って損はないかと。

準備

MonsterX u3.0rのドライバを入れる

www.sknet-web.co.jp

こっから最新版のドライバをダウンロードして入れましょう。パソコンにつないだMonsterX u3.0rのLEDが緑色になればOKです。

DirectShow.NETへの参照をプロジェクトに追加

sourceforge.net

C#からDirectShowを使えるようにしたラッパーライブラリです。「DirectShowLib-2005」というファイルがダウンロードできたら適当な場所においてプロジェクトに追加しましょう。

f:id:yoh_mar28:20160627183555p:plain

「参照」を右クリックして「参照の追加」をして「参照」ボタンを押してこのファイルを選びます。

コードを書く

フォームを準備する

f:id:yoh_mar28:20160627183907p:plain

こんな感じでフォームを準備します。パネル(panel1)と、ボタン(button1, button2)をとりあえず配置してみました。

コードを書こう

とりあえず配置したbutton1とbutton2ですが、button1を初期化、button2をキャプチャ開始ボタンにしたいと思います。まずはキャプチャをつかさどるクラスの作成からです。

キャプチャクラス

ざざざっとキャプチャクラスを作ってみました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Runtime.InteropServices;


using System.Drawing;
using DirectShowLib;



namespace DirectShowCaptureTest
{
    class DSHDMICapture : IDisposable, ISampleGrabberCB
    {
        const int WMGraphNotify = 0x0400 + 13;

        private Size _renderingSize;
        public Size renderingSize
        {
            get
            {
                return _renderingSize;
            }
            set
            {
                _renderingSize = value;

                int pWidth, pHeight , hr ;
                hr = this.basicVideo.GetVideoSize(out pWidth, out pHeight);
                DsError.ThrowExceptionForHR(hr);
                Console.WriteLine("{{Width:{0}, Height:{1}}}", pWidth, pHeight);

                Rectangle windowPos = new Rectangle();
                if(renderingSize.Width < renderingSize.Height)
                {
                    // 縦長
                    windowPos.X = 0;
                    windowPos.Width = value.Width;
                    windowPos.Height = value.Width * pHeight / pWidth;
                    windowPos.Y = (value.Height - (windowPos.Height)) / 2;
                }else
                {
                    // 横長
                    windowPos.Y = 0;
                    windowPos.Height = value.Height;
                    windowPos.Width = value.Height * pWidth / pHeight;
                    windowPos.X = (value.Width - (windowPos.Width)) / 2;
                }
                hr = this.videoWindow.SetWindowPosition(
                    windowPos.X,
                    windowPos.Y,
                    windowPos.Width,
                    windowPos.Height);
                DsError.ThrowExceptionForHR(hr);
            }
        }

        IGraphBuilder graphBuilder = null;
        IMediaEventEx mediaEventEx = null;
        IMediaControl mediaControl = null;
        IVideoWindow videoWindow = null;
        IBasicVideo basicVideo = null;

        ISampleGrabber sampleGrabber = null;

        ICaptureGraphBuilder2 captureGraphBuilder;

        /*
         * @brief       キャプチャデバイスの取得
         * 
         * @param[in]   deviceIndex 開きたいデバイスのID
         * @return      指定インデックスのキャプチャフィルタ
         */
        private static IBaseFilter GetCaptureDevice(int deviceIndex)
        {
            DsDevice[] devices = DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice);

            if (devices.Length <= deviceIndex)
            {
                Console.WriteLine("指定されたインデックスのデバイスは存在しません。");
                return null;
            }
            else
            {
                DsDevice device = devices[deviceIndex];

                object source;
                Guid filterId = typeof(IBaseFilter).GUID;

                device.Mon.BindToObject(null, null, ref filterId, out source);

                return (IBaseFilter)source;
            }
        }

        void IDisposable.Dispose() 
        {
            if(this.graphBuilder != null)Marshal.ReleaseComObject(this.graphBuilder);
            if(this.captureGraphBuilder != null)Marshal.ReleaseComObject(this.captureGraphBuilder);
            if (this.sampleGrabber != null) Marshal.ReleaseComObject(this.sampleGrabber);
        }
        int ISampleGrabberCB.SampleCB(double SampleTime, IMediaSample pSample)
        {
            Console.WriteLine("SampleCB");
            return 0;
        }
        int ISampleGrabberCB.BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen)
        {
            Console.WriteLine("BufferCB");
            return 0;
        }

        
        public DSHDMICapture(int deviceIndex, IntPtr Handle):
            base()
        {
            int hr = 0;
            Console.WriteLine("Opening :{0}", deviceIndex);
            // 初期化
            this._renderingSize = new Size(320, 240);
            this.graphBuilder = (IGraphBuilder)new FilterGraph();
            this.captureGraphBuilder = (ICaptureGraphBuilder2)new CaptureGraphBuilder2();

            // キャプチャフィルタの作成
            IBaseFilter videoCaptureFilter = GetCaptureDevice(deviceIndex);

            // キャプチャグラフの初期化
            hr = captureGraphBuilder.SetFiltergraph(graphBuilder);
            DsError.ThrowExceptionForHR(hr);

            hr = graphBuilder.AddFilter(videoCaptureFilter, "Video Capture");
            DsError.ThrowExceptionForHR(hr);

            // サンプルグラバの初期化
            this.sampleGrabber = (ISampleGrabber)new SampleGrabber();

            AMMediaType mediaType = new AMMediaType();
            mediaType.majorType = MediaType.Video;        // メジャータイプ
            mediaType.subType = MediaSubType.RGB24;       // サブタイプ
            mediaType.formatType = FormatType.VideoInfo;  // フォーマットタイプ

            // メディア タイプを設定する
            hr = sampleGrabber.SetMediaType(mediaType);
            DsError.ThrowExceptionForHR(hr);

            DsUtils.FreeAMMediaType(mediaType);

            hr = graphBuilder.AddFilter((IBaseFilter)sampleGrabber, "Sample Grabber");
            DsError.ThrowExceptionForHR(hr);

            // ビデオキャプチャフィルタをレンダリングフィルタに接続
            hr = this.captureGraphBuilder.RenderStream(
                PinCategory.Preview,
                MediaType.Video,
                videoCaptureFilter,
                (IBaseFilter)sampleGrabber,
                null);
            DsError.ThrowExceptionForHR(hr);

            // 解放
            System.Runtime.InteropServices.Marshal.ReleaseComObject(videoCaptureFilter);

            // フィルタグラフの設定
            this.mediaEventEx = (IMediaEventEx)graphBuilder;
            this.mediaControl = (IMediaControl)graphBuilder;
            this.videoWindow = this.graphBuilder as IVideoWindow;
            this.basicVideo = this.graphBuilder as IBasicVideo;

            this.mediaEventEx.SetNotifyWindow(Handle, WMGraphNotify, IntPtr.Zero);

            hr = this.videoWindow.put_Owner(Handle);
            DsError.ThrowExceptionForHR(hr);
            hr = this.videoWindow.put_WindowStyle(WindowStyle.Child | WindowStyle.ClipChildren | WindowStyle.ClipSiblings);
            DsError.ThrowExceptionForHR(hr);

            this.renderingSize = _renderingSize;
 
            {
                // Filterを列挙する
                IEnumFilters hoge;
                graphBuilder.EnumFilters(out hoge);

                IBaseFilter[] filters = new IBaseFilter[1];

                Console.WriteLine("hoge");
                while (hoge.Next(filters.Length, filters, IntPtr.Zero) == 0)
                {
                    FilterInfo info;
                    filters[0].QueryFilterInfo(out info);

                    Guid id;
                    filters[0].GetClassID(out id);

                    Console.WriteLine(string.Format("Guid:{0} Name:{1}", id, info.achName));
                    filters[0] = null;
                }
                filters = null;
            }
        }

        public void Play()
        {
            int hr = 0;

            sampleGrabber.SetBufferSamples(true);
            sampleGrabber.SetOneShot(false);
            sampleGrabber.SetCallback(this, 1);

            // スタート
            hr = this.mediaControl.Run();
            DsError.ThrowExceptionForHR(hr);
        }

        public void Stop()
        {
            // ストップ
            int hr = this.mediaControl.Stop();
            DsError.ThrowExceptionForHR(hr);
        }
    }
}

参考にさせていただいたのは以下のサイト。ほとんどパーツは共通だけど、このクラスにはSampleGrabberという、キャプチャーしたフレーム1枚ごとにコールバック関数を呼んでくれるフィルタを挿入するように実装してあるので、実行すると「BufferCB」というメッセージが表示されます。そこにちゃんと処理を書いてあげればキャプチャした画像1枚毎に処理を施せます。

ビデオ キャプチャの方法 | DirectShow .NET プログラミング解説

フォームのコード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace DirectShowCaptureTest
{
    public partial class Form1 : Form
    {

        DSHDMICapture m_Capture;

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            this.m_Capture = new DSHDMICapture(0, this.panel1.Handle);
            this.m_Capture.renderingSize = this.panel1.ClientSize;
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (this.m_Capture != null)
            {
                this.m_Capture.Play();
            }
        }

        private void panel1_Resize(object sender, EventArgs e)
        {
            if(this.m_Capture != null)
            {
                m_Capture.renderingSize = this.panel1.ClientSize;
            }
        }
    }
}

実装するだけで力尽きてしまって整理できてなくてすみませんが、こんな感じです。

イベントのバインドは適宜行ってくださいね。

いざ実験

セカンドディスプレイ用のHDMIポートをキャプチャしてみました。button1のあとにbutton2を押すとキャプチャ結果の表示が始まり、こんな感じになるはずです。あとはウィンドウのサイズを調整すればアスペクト比を保ったまま拡大縮小できるはずです。

f:id:yoh_mar28:20160627205852p:plain

おわりに

このプログラムは普段使っているものを書き直したものなんですが、思いのほか時間がかかってしまい、力尽きてしまった感はありますが、一応キャプチャできるとこまでこぎつけました。

これを使って配信するもよし、画像処理をするもよし、って感じです。何かお気づきの点等ございましたらコメントよろしくお願いいたします。

ちなみに、以下のHDMIキャプチャは圧縮されてしまうので画質はアレですが非常に使いやすいです。