Capturer le flux vidéo d’une caméra avec MediaFoundation dans une application Windows Forms ou WPF

Télécharger le code

Dans mon précédant billet, j’ai abordé la manière d’utiliser les APIs du Windows Runtime afin de prendre une photo et de l’afficher dans une application Windows Forms. Restait alors à implémenter ce que fait la classe XAML CaptureElement du Windows Runtime afin de capturer directement le flux vidéo venant de la caméra.

La classe CaptureElement, utilisant les APIs MediaFoundation, et comme il ne sera pas possible d’utiliser dans le contexte d’une application de bureau, cette classe XAML, nous allons donc aborder la manière de capturer le flux vidéo en utilisant directement MediaFoundation et non pas les APIs du Windows Runtime.

Architecture de la solution.

Dans le code source disponible en téléchargement, vous trouverez 3 projets.

  • Une librairie C/C++ (Win32MediaCapture.dll) qui appelle les APIs MediaFoundation et qui exporte des méthodes afin qu’elles soient utilisables via la technologie PInvoke de .NET
  • Une Assembly C# Version 3.5 de .NET (FRDPE.MediaFoundation.Wrapper.dll) qui importe les méthodes de la librairie Win32MediaCapture et permet de faire le pont avec l’interface utilisateur.
  • Un projet Windows Forms qui utilise l’assembly C# et qui servira de container pour afficher le flux vidéo.

Utilisation de MediaFoundation dans la librairie Win32MediaCapture.dll

MediaFoundation est un jeu d’APIs disponible depuis Windows Vista et qui utilise COM comme modèle de programmation.

Avec Windows 8, de nouvelles interfaces ont été rajoutées, comme IMFCaptureEngine, IMFCapturePreviewSink, IMFCaptureSource et d’autres qui vont nous permettre de capturer et de pré-visualiser le flux vidéo.

Initialisation du moteur de capture vidéo.

Tout d’abord, il faut démarrer MediaFoundation et initialiser le moteur de capture, comme illustré dans le code suivant :

WIN32MEDIACAPTURE_API HRESULT MFInitialize(HWND preview, CBCOMPLETED *cb, UINT32 numdevice)
{
    HRESULT hr = S_OK;
    //Start the MediaFoundation
    
    m_pCaptureManager = std::unique_ptr<Win32CaptureManager>(new Win32CaptureManager());
    if (m_pCaptureManager == nullptr) return E_OUTOFMEMORY;
    hr=m_pCaptureManager->MFStartup();

    CComPtr<IMFActivate> device;
    hr = m_pCaptureManager->MFGetDevice(&device, numdevice);
    if (FAILED(hr)) return hr;

    
    hr=m_pCaptureManager->MFInitializeCaptureManager(preview, device, cb);
    if (FAILED(hr)) return hr;
    
    return hr;
}

La méthode MFInitialize, prend comme paramètre :

  • HWND preview, est un HANDLE de fenêtre qui servira de container pour pré-visualiser la vidéo.

  • CBCOMPLETED *cb, est un pointeur de méthode de rappel pour notifier le client qu’une action est finie.
     UINT32 numdevice, correspond à un index de caméra à utiliser. (Une tablette peux posséder deux caméras, une frontale et une arrière), il est donc important de pouvoir les sélectionner.

  1. On démarre MediaFoundation (MFStartup()).

  2. On récupère un pointeur IMFActivate MFGetDevice()) en fonction de l’index de la caméra.
    Exemple MFGetDevice
    HRESULT Win32CaptureManager::MFGetDevice(IMFActivate** device, UINT32 numdevice)
    {

        HRESULT hr = S_OK;
        CComPtr<IMFAttributes> pAttributes;
        hr = MFCreateAttributes(&pAttributes, 1);
        if (FAILED(hr)) return hr;

        hr = pAttributes->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
            MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
        if (FAILED(hr)) return hr;

        // Enumerate devices.
        UINT32 count;
        IMFActivate **ppDevices;
        
        hr = MFEnumDeviceSources(pAttributes, &ppDevices, &count);
        if (FAILED(hr)) return hr;
        if (count > 0)
        {
            if (0 < numdevice && numdevice >count)
            {
                *device = ppDevices[0];
            }
            else
            {
                *device = ppDevices[numdevice];
            }            
        }
        return hr;
    }

  3. Enfin on initialise le moteur de capture via la méthode MFInitializeCaptureManager illustré dans le code suivant :
    HRESULT Win32CaptureManager::MFInitializeCaptureManager(HWND hwndPreview,
                                                            IUnknown* pUnk,
                                                            CBCOMPLETED *cb)
    {
        HRESULT hr = S_OK;
        
        m_pCallback = std::unique_ptr<Win32CaptureEventCallback>(new (std::nothrow)Win32CaptureEventCallback(cb));
        
        if (nullptr == m_pCallback) return E_OUTOFMEMORY;

        m_hwndPreview = hwndPreview;
        CComPtr<IMFCaptureEngineClassFactory>   pFactory = nullptr;

        hr = CoCreateInstance(CLSID_MFCaptureEngineClassFactory, nullptr,
            CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFactory));
        if (FAILED(hr)) return hr;

        CComPtr<IMFAttributes> pAttributes = nullptr;

        hr = MFCreateAttributes(&pAttributes, 1);
        if (FAILED(hr)) return hr;

        //Ici DIRECTX
        //Create a D3D Manager
        hr = CreateD3DManager();
        if (FAILED(hr)) return hr;
        hr = pAttributes->SetUnknown(MF_CAPTURE_ENGINE_D3D_MANAGER, g_pDXGIMan);
        if (FAILED(hr)) return hr;

        //// Create and initialize the capture engine.
        hr = pFactory->CreateInstance(CLSID_MFCaptureEngine, IID_PPV_ARGS(&m_pEngine));
        if (FAILED(hr)) return hr;

        
        hr = m_pEngine->Initialize(m_pCallback.get(), pAttributes, nullptr, pUnk);
        if (FAILED(hr)) return hr;
        
        return hr;
    }

  • Dans cette méthode, on instancie la classe Win32CaptureEventCallBack, qui dérive de l’interface IMFCaptureEngineOnEventCallback et qui servira d’objet de rappel à l’infrastructure de MediaFoundation. Pour être notifié, il faut implémenter la méthode OnEvent de cette Interface. Vous remarquerez que dans son constructeur, nous lui passons un pointeur CBCOMPLETED *cb, qui permettra de rappeler le client C# comme nous le verrons par la suite.

    Exemple de méthode OnEvent
    STDMETHODIMP Win32CaptureEventCallback::OnEvent(_In_ IMFMediaEvent* pEvent)
    {
        HRESULT hr = S_OK;
        HRESULT hrStatus;
        GUID guidType;
        int status = 1;
        hr = pEvent->GetStatus(&hrStatus);
        if (FAILED(hr))
        {
            status = 0;
            if (m_cb) m_cb(status);
            return hr;
        }

        hr = pEvent->GetExtendedType(&guidType);
        if (FAILED(hr))
        {
            status = 0;
        }
        if (guidType == MF_CAPTURE_ENGINE_INITIALIZED)
        {
            status = 1;//EngineInitialized
        }
        else if (guidType == MF_CAPTURE_ENGINE_PREVIEW_STARTED)
        {
            status = 2; //PreviewStarted
        }
        else if (guidType == MF_CAPTURE_ENGINE_PREVIEW_STOPPED)
        {
            status = 3; //PreviewStopped
        }

        if (m_cb) m_cb(status);
        return hr;
    }

  • Ensuite on crée une fabrique de classe IMFCaptureEngineClassFactory, afin de pouvoir créer une instance de notre moteur de capture (IMFCaptureEngine).

  • MF_CAPTURE_ENGINE_D3D_ENGINE, indique au moteur, d’allouer les frames vidéos en utilisant DirectX et donc d’utiliser l’accélération matérielle pour l’affichage.

  • Pour ce faire on utilise une instance de l’interface IMFAttributes , créée par la méthode MFCreateAttributes.  

  • Ensuite, on crée une instance du moteur pFactory->CreateInstance (…)

  • Enfin on initialise le moteur m_pEngine->Initialize(..)en lui donnant :

    • un pointeur sur la classe de rappel Win32CaptureEventCallBack
    • un attribut correspondant au rendu DirectX
    • un pointeur IUnknown* pUnk, correspondant à la caméra qui sera utilisée.

Démarrer la prévisualisation

Une fois que le moteur est initialisé, il est alors possible de démarrer la prévisualisation.

HRESULT Win32CaptureManager::MFStartPreview(DWORD rotation)
{
    CComPtr<IMFCapturePreviewSink> pPreview;
    m_rotation = rotation;
    if (m_pEngine == nullptr) return S_FALSE;

    HRESULT hr = S_OK;
    CComPtr<IMFCaptureSink> pcaptureSink;
    hr = m_pEngine->GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_PREVIEW, &pcaptureSink);
    if (FAILED(hr)) return hr;

    hr = pcaptureSink->QueryInterface(IID_PPV_ARGS(&pPreview));
    if (FAILED(hr)) return hr;

    hr = pPreview->SetRenderHandle(m_hwndPreview);
    if (FAILED(hr)) return hr;

    
    CComPtr<IMFCaptureSource> pSource;

    hr = m_pEngine->GetSource(&pSource);
    if (FAILED(hr)) return hr;
    
    if (m_pProcAmp == nullptr)
    {
        hr = pSource->GetCaptureDeviceSource(MF_CAPTURE_ENGINE_DEVICE_TYPE_VIDEO, &m_pMediaSource);
        if (FAILED(hr)) return hr;

        hr = m_pMediaSource->QueryInterface(IID_PPV_ARGS(&m_pProcAmp));
        if (FAILED(hr)) return hr;

    }
    

    CComPtr<IMFMediaType> pMediaType;
    //CComPtr<IMFMediaType> pMediaType2;

    hr = pSource->GetCurrentDeviceMediaType((DWORD) MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_VIDEO_PREVIEW, &pMediaType);
    if (FAILED(hr)) return hr;
    
    
    /*hr = CloneVideoMediaType(pMediaType, MFVideoFormat_ARGB32, &pMediaType2);
    if (FAILED(hr)) return hr;

    hr = pMediaType2->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE);    
    if (FAILED(hr)) return hr;
    */
    
    DWORD dwSinkStreamIndex;
    hr = pPreview->AddStream((DWORD) MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_VIDEO_PREVIEW, pMediaType, nullptr, &dwSinkStreamIndex);
    if (FAILED(hr)) return hr;

    hr=pPreview->SetRotation(dwSinkStreamIndex, rotation);
    if (FAILED(hr)) return hr;
    
    hr = m_pEngine->StartPreview();
    return hr;
}

Vous aurez également remarqué que nous instancions un pointeur sur le périphérique
pSource->GetCaptureDeviceSource(IMFMediaSource) et ceci afin de pouvoir récupérer une instance de la classe IAMVideoProcAmp (m_pMediasource->QueryInterface()), qui nous permettra de modifier les paramètres de la caméra, tels que le contraste, la luminosité, la saturation, etc..

Modifier les propriétés de la caméra

Pour modifier par exemple, le contraste, la luminosité et la saturation, il suffit d’utiliser la méthode Set de l’interface IAMVideoProcAmp avec le bon paramètre, comme illustré dans le code suivant :

HRESULT Win32CaptureManager::MFSetContrast(long value)
{
    
    
    HRESULT hr = S_OK;
    if (m_pProcAmp == nullptr) return E_POINTER;
    long lValue, lFlags;
    hr = m_pProcAmp->Get(VideoProcAmp_Contrast, &lValue, &lFlags);
    if (FAILED(hr)) return hr;
    return    m_pProcAmp->Set(VideoProcAmp_Contrast, value, lFlags);        
}

HRESULT Win32CaptureManager::MFSetBrightness(long value)
{

    HRESULT hr = S_OK;
    if (m_pProcAmp == nullptr) return E_POINTER;
    long lValue, lFlags;
    hr = m_pProcAmp->Get(VideoProcAmp_Brightness, &lValue, &lFlags);
    if (FAILED(hr)) return hr;
    return    m_pProcAmp->Set(VideoProcAmp_Brightness, value, lFlags);
}

HRESULT Win32CaptureManager::MFSetSaturation(long value)
{

    HRESULT hr = S_OK;
    if (m_pProcAmp == nullptr) return E_POINTER;
    long lValue, lFlags;
    hr = m_pProcAmp->Get(VideoProcAmp_Saturation, &lValue, &lFlags);
    if (FAILED(hr)) return hr;
    return    m_pProcAmp->Set(VideoProcAmp_Saturation, value, lFlags);
}

Pour récupérer les propriétés il faut utiliser la méthode GetRange de la même interface.

HRESULT Win32CaptureManager::MFGetContrast(CAPABILITIES *contrast)
{
    HRESULT hr = S_OK;
    if (m_pProcAmp == nullptr) return E_POINTER;
    
    return m_pProcAmp->GetRange(VideoProcAmp_Contrast,
                              &contrast->Minimum,
                              &contrast->Maximum,
                              &contrast->Step,
                              &contrast->Default,
                              &contrast->Caps);
    
}

HRESULT Win32CaptureManager::MFGetBrightness(CAPABILITIES *brightness)
{
    HRESULT hr = S_OK;
    if (m_pProcAmp == nullptr) return E_POINTER;

    return m_pProcAmp->GetRange(VideoProcAmp_Brightness,
        &brightness->Minimum,
        &brightness->Maximum,
        &brightness->Step,
        &brightness->Default,
        &brightness->Caps);
}

HRESULT Win32CaptureManager::MFGetSaturation(CAPABILITIES *caps)
{
    HRESULT hr = S_OK;
    if (m_pProcAmp == nullptr) return E_POINTER;

    return m_pProcAmp->GetRange(VideoProcAmp_Saturation,
        &caps->Minimum,
        &caps->Maximum,
        &caps->Step,
        &caps->Default,
        &caps->Caps);
}

et ainsi de suite pour les autres propriétés.

Création du Wrapper .NET C# (FRDPE.MediaFoundation.Wrapper.dll)

Le wrapper .NET n’est pas indispensable. Il est là uniquement pour simplifier l’utilisation de notre librairie C/C++, dans le contexte d’un client Windows Forms ou WPF.

Nous avons une classe FRDPE.Win32.Import.MediaFoundation, qui :

  • importe sous forme de méthodes statiques, toutes les APIs de notre librairies C/C++
  • définit les signatures des délégués qui seront utilisés plus tard comme méthodes de rappels invoquées par le composant C/C++

namespace FRDPE.Win32.Import
{
    public static class MediaFoundation
    {
        
        [StructLayout(LayoutKind.Sequential)]
        public class Capabilities
        {
            public Int32 Minimum;
            public Int32 Maximum;
            public Int32 Step;
            public Int32 Default;
            public Int32 Caps;
        }

            public delegate void InitializeCompletedHandler(int status);
            public delegate void EnumDeviceHandler( [Out,MarshalAs(UnmanagedType.LPWStr)] String device);
            
            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFInitialize(IntPtr hwndPreview, IntPtr cb,UInt32 numDevice);
        
            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFEnumDevices(IntPtr cb,[In,MarshalAs(UnmanagedType.SysUInt)] ref UIntPtr count);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFStartPreview(uint rotation);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFStopPreview();

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFSetContrast(long contrast);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFGetContrast([Out,MarshalAs(UnmanagedType.LPStruct)] Capabilities contrast);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFSetBrightness(long brightness);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFGetBrightness([Out, MarshalAs(UnmanagedType.LPStruct)] Capabilities brightness);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFSetSaturation(long value);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFGetSaturation([Out, MarshalAs(UnmanagedType.LPStruct)] Capabilities caps);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFSetHue(long value);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFGetHue([Out, MarshalAs(UnmanagedType.LPStruct)] Capabilities caps);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFSetSharpness(long value);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFGetSharpness([Out, MarshalAs(UnmanagedType.LPStruct)] Capabilities caps);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFSetColorEnable(long value);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFGetColorEnable([Out, MarshalAs(UnmanagedType.LPStruct)] Capabilities caps);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFSetGamma(long value);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFGetGamma([Out, MarshalAs(UnmanagedType.LPStruct)] Capabilities caps);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFSetWhiteBalance(long value);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFGetWhiteBalance([Out, MarshalAs(UnmanagedType.LPStruct)] Capabilities caps);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFSetGain(long value);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFGetGain([Out, MarshalAs(UnmanagedType.LPStruct)] Capabilities caps);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFSetBacklightCompensation(long value);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 MFGetBacklightCompensation([Out, MarshalAs(UnmanagedType.LPStruct)] Capabilities caps);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 GetErrorMessage(UInt32 hr, IntPtr message);

            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]
            public static extern UInt32 GetErrorMessageSize(UInt32 hr);
            
    };

Ensuite, on définit une classe C# FRDPE.MediaFoundation.Wrapper.MediaCapture, qui servira de façade entre l’Interface utilisateur et la librairie C/C+.
Cette classe n’est pas forcement utile, mais elle simplifiera l’utilisation de la librairie C/C++ dans les projets Windows Forms et WPF.

Par exemple, si je prends la méthode MFEnumDevices qui prend comme paramètre :

  • un pointeur sur une méthode de rappel : CBENUMDEVICES*cb
  • un pointeur sur un entier non signé : UINT32*count, qui contiendra le nombre de caméras disponibles
  • et qui retourne un HRESULT.

typedef void(CALLBACK CBENUMDEVICES)(const WCHAR* friendlyname);
extern "C" WIN32MEDIACAPTURE_API HRESULT MFEnumDevices(CBENUMDEVICES *cb, UINT32 *count);

qui se traduit en C# par :

public delegate void EnumDeviceHandler([Out, MarshalAs(UnmanagedType.LPWStr)] String device);
            [DllImport("Win32MediaCapture.dll", CallingConvention = CallingConvention.Cdecl)]

que l’appel se fait comme cela :

public UInt32 EnumDevices()
        {
            IntPtr Callback=Marshal.GetFunctionPointerForDelegate(new Import.MediaFoundation.EnumDeviceHandler((String devicename)=>
                {

                    if (m_SyncronisationContext != null)
                    {
                        m_SyncronisationContext.Post(new System.Threading.SendOrPostCallback((o) =>
                        {

                            if (OnEnumDevices != null)
                            {
                                OnEnumDevices(this, devicename);
                            }

                        }), null);
                    }
                }));
                
                UIntPtr ptr=new UIntPtr();                
                Helper.CheckAndThrowException("EnumDevice Error", Import.MediaFoundation.MFEnumDevices(Callback, ref ptr));
                return ptr.ToUInt32();
        }

Il est alors préférable que l’appelant puisse l’appeler comme suit :

m_mediaCapture = new MF.MediaCapture(picMediaCapture.Handle);
                    m_mediaCapture.OnEnumDevices += (asender, deviceName) =>
                        {
                            lstEnumDevices.Items.Add(deviceName);
                        };
                    m_CountDevice = m_mediaCapture.EnumDevices();   

Code complet de la classe FRDPE.MediaFoundation.Wrapper.MediaCapture :

namespace FRDPE.MediaFoundation.Wrapper
{
    public class MediaCapture
    {
        IntPtr m_HwndPreview;
        SynchronizationContext m_SyncronisationContext;
        public delegate void InitializeCompletedEventHandler(Object sender,InitializeCompletedEventArgs e);
        public event InitializeCompletedEventHandler OnInitializationCompleted;

        public delegate void EnumDeviceHandler(Object sender, String device);
        public event EnumDeviceHandler OnEnumDevices;
        public SynchronizationContext Dispatcher {
            get { return m_SyncronisationContext; }
            set { m_SyncronisationContext=value;}
        }
        public class InitializeCompletedEventArgs:EventArgs
        {
            public AsyncStatus Status;
            public InitializeCompletedEventArgs(AsyncStatus status)
            {
                Status=status;
            }
        }
        
        public MediaCapture(IntPtr hwndPreview)
        {
            Helper.CheckPointer(hwndPreview);
            m_HwndPreview = hwndPreview;
            m_SyncronisationContext = SynchronizationContext.Current;
            
        }
        public UInt32 EnumDevices()
        {
            IntPtr Callback=Marshal.GetFunctionPointerForDelegate(new Import.MediaFoundation.EnumDeviceHandler((String devicename)=>
                {

                    if (m_SyncronisationContext != null)
                    {
                        m_SyncronisationContext.Post(new System.Threading.SendOrPostCallback((o) =>
                        {

                            if (OnEnumDevices != null)
                            {

                                OnEnumDevices(this, devicename);
                            }

                        }), null);
                    }
                }));
                
                UIntPtr ptr=new UIntPtr();
                
                Helper.CheckAndThrowException("EnumDevice Error", Import.MediaFoundation.MFEnumDevices(Callback, ref ptr));

                return ptr.ToUInt32();
        }
        public void InitializeAsync(UInt32 numDevice)
        {
            try
            {
                IntPtr callback = Marshal.GetFunctionPointerForDelegate(new
                    FRDPE.Win32.Import.MediaFoundation.InitializeCompletedHandler((int statusInfo) =>
                {
                    
                    if (m_SyncronisationContext!=null)
                    {
                        m_SyncronisationContext.Post(new System.Threading.SendOrPostCallback((o) =>
                        {

                            if (OnInitializationCompleted!=null)
                            {
                                AsyncStatus status = (AsyncStatus)statusInfo;                                
                                OnInitializationCompleted(this, new InitializeCompletedEventArgs(status));
                            }

                        }), null);
                    }
                    
                    
                }));
                Helper.CheckPointer(callback);
                Helper.CheckAndThrowException("Could not initialize MediaFoundation",
                    Import.MediaFoundation.MFInitialize(m_HwndPreview, callback, numDevice));
                
            }
            catch(Exception)
            {
                throw;
            }            
        }
        public void StartPreviewAsync(uint rotation)
        {
            Helper.CheckAndThrowException("Could not Start Preview", Import.MediaFoundation.MFStartPreview(rotation));

        }
        public void StopPreviewAsync()
        {
            Helper.CheckAndThrowException("Could not Stop Preview", Import.MediaFoundation.MFStopPreview());

        }
       
        public void SetProperty(VideoProcAmpProperty property,long value)
        {
            switch (property)
            {
                case VideoProcAmpProperty.VideoProcAmp_Brightness:
                    Helper.CheckAndThrowException("Could not set the brightness", Import.MediaFoundation.MFSetBrightness(value));
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Contrast:
                    Helper.CheckAndThrowException("Could not set the contrast", Import.MediaFoundation.MFSetContrast(value));
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Hue:
                    Helper.CheckAndThrowException("Could not set the Hue", Import.MediaFoundation.MFSetHue(value));
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Saturation:
                    Helper.CheckAndThrowException("Could not set the saturation", Import.MediaFoundation.MFSetSaturation(value));
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Sharpness:
                    Helper.CheckAndThrowException("Could not set the Sharpness", Import.MediaFoundation.MFSetSharpness(value));
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Gamma:
                    Helper.CheckAndThrowException("Could not set the gamma", Import.MediaFoundation.MFSetGamma(value));
                    break;
                case VideoProcAmpProperty.VideoProcAmp_ColorEnable:
                    Helper.CheckAndThrowException("Could not set the Color", Import.MediaFoundation.MFSetColorEnable(value));
                    break;
                case VideoProcAmpProperty.VideoProcAmp_WhiteBalance:
                    Helper.CheckAndThrowException("Could not set the WhiteBalance", Import.MediaFoundation.MFSetWhiteBalance(value));
                    break;
                case VideoProcAmpProperty.VideoProcAmp_BacklightCompensation:
                    Helper.CheckAndThrowException("Could not set the Backlight Compensation", Import.MediaFoundation.MFSetBacklightCompensation(value));
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Gain:
                    Helper.CheckAndThrowException("Could not set the gain", Import.MediaFoundation.MFSetGain(value));
                    break;
                default:
                    break;
            }
        }
        public FRDPE.Win32.Import.MediaFoundation.Capabilities GetProperty(VideoProcAmpProperty property)
        {
            FRDPE.Win32.Import.MediaFoundation.Capabilities Caps = new FRDPE.Win32.Import.MediaFoundation.Capabilities();
            UInt32 hr=0;
            String ErrorMessage=String.Empty;
            switch (property)
            {
                case VideoProcAmpProperty.VideoProcAmp_Brightness:
                    ErrorMessage="Could not get the brightness";
                    hr=Import.MediaFoundation.MFGetBrightness(Caps);                    
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Contrast:
                    ErrorMessage="Could not get the contrast";
                    hr=Import.MediaFoundation.MFGetContrast(Caps);            
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Hue:
                    ErrorMessage="Could not get the hue";
                    hr= Import.MediaFoundation.MFGetHue(Caps);
                    
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Saturation:
                    ErrorMessage="Could not get the saturation";
                    hr = Import.MediaFoundation.MFGetSaturation(Caps);
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Sharpness:
                    ErrorMessage="Could not get the Sharpness";
                    hr=Import.MediaFoundation.MFGetSharpness(Caps);
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Gamma:
                    ErrorMessage="Could not get the gamma";
                    hr=Import.MediaFoundation.MFGetGamma(Caps);
                    break;
                case VideoProcAmpProperty.VideoProcAmp_ColorEnable:
                    ErrorMessage="Could not get the color";
                    hr=Import.MediaFoundation.MFGetColorEnable(Caps);
                    break;
                case VideoProcAmpProperty.VideoProcAmp_WhiteBalance:
                    
                    ErrorMessage="Could not get the white balance";
                    hr=Import.MediaFoundation.MFGetWhiteBalance(Caps);
                    break;
                case VideoProcAmpProperty.VideoProcAmp_BacklightCompensation:
                    ErrorMessage="Could not get the Backlight Compensation";
                    hr=Import.MediaFoundation.MFGetBacklightCompensation(Caps);
                    break;
                case VideoProcAmpProperty.VideoProcAmp_Gain:
                    ErrorMessage="Could not get the Gain";
                    hr=Import.MediaFoundation.MFGetGain(Caps);
                    break;
                default:
                    break;
            }
            if (hr == 0x80070490)
            {
                //Not supported
                return null;
            }
            else
            {
                Helper.CheckAndThrowException(ErrorMessage,hr);
            }

            return Caps;
        }
        public enum VideoProcAmpProperty
        {
            VideoProcAmp_Brightness    = 0,
            VideoProcAmp_Contrast    = ( VideoProcAmp_Brightness + 1 ) ,
            VideoProcAmp_Hue    = ( VideoProcAmp_Contrast + 1 ) ,
            VideoProcAmp_Saturation    = ( VideoProcAmp_Hue + 1 ) ,
            VideoProcAmp_Sharpness    = ( VideoProcAmp_Saturation + 1 ) ,
            VideoProcAmp_Gamma    = ( VideoProcAmp_Sharpness + 1 ) ,
            VideoProcAmp_ColorEnable    = ( VideoProcAmp_Gamma + 1 ) ,
            VideoProcAmp_WhiteBalance    = ( VideoProcAmp_ColorEnable + 1 ) ,
            VideoProcAmp_BacklightCompensation    = ( VideoProcAmp_WhiteBalance + 1 ) ,
            VideoProcAmp_Gain    = ( VideoProcAmp_BacklightCompensation + 1 )
        };
        public enum AsyncStatus
        {
            Error=0,
            EngineInitialized,
            PreviewStarted,            
            PreviewStopped            
        };
    }
    
}

Ce qu’il faut juste noter ici :

  • Le constructeur prend un pointeur IntPtr qui sera un HANDLE (HWDN) de fenêtre.
  • Qu’on convertit les délégués en pointeurs de fonctions à l’aide de la méthode statique GetFunctionPointerForDelegate, de la classe Marshal.
  • Une fois rappelé par la librairie C/C++, on invoque un évènement auquel se serait abonné le client Windows Forms ou WPF, tout prenant la précaution d’usage, de bien se synchroniser avec le thread appelant à l’aide de l’objet SynchronisationContext.
  • Qu’on utilise une la méthode CheckAndThrowException(), pour vérifier et lever une exception en fonction du HRESULT retourné.

Note : pour de plus amples informations concernant l’interopérabilité entre C/C++ et C# via l’attribut DLLImport, reportez vous à mon précédant billet.

Une dernière classe nommée Helper, permet de :

  • vérifier la validité des pointeurs,
  • vérifier la validité des HRESULTs
  • de lever des exceptions, avec le bon message d’erreur associé au HRESULT

Code complet de la classe Helper

namespace FRDPE.MediaFoundation
{
    public static class Helper
    {
        /// <summary>
        /// Check the Pointer
        /// </summary>
        /// <param name="ptr"></param>
        public static void CheckPointer(IntPtr ptr)
        {
            unsafe
            {
                if (ptr.ToPointer() == null)
                {
                    throw new ArgumentNullException();
                }
            }
        }
        public static IntPtr GetFunctionPointeurFromInitializeCompletedHandler(MediaCapture mediaCapture)
        {
            return Marshal.GetFunctionPointerForDelegate(new FRDPE.Win32.Import.MediaFoundation.InitializeCompletedHandler((int statusInfo) =>
                    {
                        var s = statusInfo;
                        if (mediaCapture!=null)
                        {
                            if (mediaCapture.Dispatcher!=null)
                            {
                                mediaCapture.Dispatcher.Post(new System.Threading.SendOrPostCallback((o) =>
                                    {
                                        
                                        
                                    }), statusInfo);
                            }
                        }
                    }));

        }
        public static String FormatErrorMessage(String Message,UInt32 hr)
        {
            return String.Format("{0} : 0x{1:X}L", Message, hr);
        }
        public static Boolean FAILED(UInt32 hr)
        {
            return (hr > 0);
        }
        public static Boolean SUCCEEDED(UInt32 hr)
        {
            return (hr == 0);
        }
        public static void CheckAndThrowException(String message,UInt32 hr)
        {
            if (Helper.FAILED(hr))
            {
                String InternalMessage;
                Int32 size = 0;
                IntPtr ptr = IntPtr.Zero ;
                try
                {
                    size = (Int32)FRDPE.Win32.Import.MediaFoundation.GetErrorMessageSize(hr);

                    ptr = Marshal.AllocHGlobal(size);

                    UInt32 errorHR = FRDPE.Win32.Import.MediaFoundation.GetErrorMessage(hr, ptr);
                    if (Helper.FAILED(errorHR))
                    {
                        throw new ApplicationException(Helper.FormatErrorMessage("Catastrophic failure : No pointer !!!", errorHR));
                    }

                    InternalMessage = Marshal.PtrToStringUni(ptr);
                    
                }
                finally
                {
                    Marshal.FreeCoTaskMem(ptr);
                }
                

                throw new ApplicationException(Helper.FormatErrorMessage(String.Format("{0} : Internal Message : {1}",message,InternalMessage), hr));
            }
        }
    }
}

Interface Utilisateur Windows Forms ou WPF.

C’est la partie la plus simple, il suffit d’ajouter notre assembly FRDPE.MediaFoundation.Wrapper.dll en tant que référence.

Ensuite pour démarrer la prévisualisation de la vidéo, il suffit tout simplement :

  1. D’instancier notre classe MediaCapture en lui passant un HANDLE de fenêtre.
  2. De s’abonner à l’évènement OnInializationCompleted,
  3. D’initialiser la capture InitializeAsync(m_numDevice) avec le bon index de caméra
  4. Une fois initialisé, de démarrer la capture StartPreviewAsync()

private void cmdStartPreview_Click(object sender, EventArgs e)
        {
            if (m_CountDevice == 0) return;

            try
            {
                IntPtr ptr = picMediaCapture.Handle;                
                m_mediaCapture = new MF.MediaCapture(ptr);
                m_mediaCapture.OnInitializationCompleted += (asender, ae) =>
                    {
                        if (ae.Status==MF.MediaCapture.AsyncStatus.EngineInitialized)
                        {                           
                            try
                            {
                                m_mediaCapture.StartPreviewAsync(m_ConfigPreview.Rotation);
                                
                            }
                            catch(Exception ex)
                            {
                                MessageBox.Show(ex.Message);
                            }
                            
                        }
                        else if(ae.Status==MF.MediaCapture.AsyncStatus.PreviewStarted)
                        {
                            try
                            {

                                Brigtness = m_mediaCapture.GetProperty(MF.MediaCapture.VideoProcAmpProperty.VideoProcAmp_Brightness);
                                Contrast = m_mediaCapture.GetProperty(MF.MediaCapture.VideoProcAmpProperty.VideoProcAmp_Contrast);
                                Saturation = m_mediaCapture.GetProperty(MF.MediaCapture.VideoProcAmpProperty.VideoProcAmp_Saturation);
                                Hue = m_mediaCapture.GetProperty(MF.MediaCapture.VideoProcAmpProperty.VideoProcAmp_Hue);
                                Sharpness = m_mediaCapture.GetProperty(MF.MediaCapture.VideoProcAmpProperty.VideoProcAmp_Sharpness);
                                Gamma = m_mediaCapture.GetProperty(MF.MediaCapture.VideoProcAmpProperty.VideoProcAmp_Gamma);
                                ColorEnable = m_mediaCapture.GetProperty(MF.MediaCapture.VideoProcAmpProperty.VideoProcAmp_ColorEnable);
                                BacklightCompensation = m_mediaCapture.GetProperty(MF.MediaCapture.VideoProcAmpProperty.VideoProcAmp_BacklightCompensation);
                                Gain = m_mediaCapture.GetProperty(MF.MediaCapture.VideoProcAmpProperty.VideoProcAmp_Gain);
                                WhiteBalance = m_mediaCapture.GetProperty(MF.MediaCapture.VideoProcAmpProperty.VideoProcAmp_WhiteBalance);
                            }
                            catch(Exception ex)
                            {
                                MessageBox.Show(ex.Message);
                                
                            }
                            finally
                            {
                                FormatTrackBar();
                            }

                            
                        }
                        else if (ae.Status == MF.MediaCapture.AsyncStatus.PreviewStopped)
                        {
                            MessageBox.Show("Preview Stopped");
                        }
                        else if (ae.Status == MF.MediaCapture.AsyncStatus.Error)
                        {
                            MessageBox.Show("Engine Error");
                        }
                    };

                m_mediaCapture.InitializeAsync(m_numDevice);

                
            }
            catch (Exception ex)
            {

                MessageBox.Show(ex.Message);
            }
            

        }

Pour visualiser la vidéo il nous faut un Handle de fenêtre. Avec les Windows Form,s c’est pas trop compliqué, il suffit tout simplement comme dans l’exemple fournit, d’ajouter un PictureBox qui possède la propriété Handle. D’ailleurs tous les objets qui possèderaient cette propriété, comme l’objet Form lui même, sont de bons candidats.

image

Vous noterez également dans le code associé à cet article, qu’il est possible de modifier certaines propriétés de la caméra, comme, le contraste, la luminosité, la saturation, Hue, Gamma, Sharpness, la rotation, etc..

Si vous souhaitez utiliser cette librairie avec WPF, qui n’a plus réellement de notion de Handle associé aux objets, il suffira d’utiliser l’objet WindowsFormsHost, comme illustré dans l’extrait XAML suivant :

<Window x:Class="ClientWPFMediaCapture.MainWindow"
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="86*"/>
            <ColumnDefinition Width="431*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <WindowsFormsHost Margin="0" Grid.Column="1">
            <wf:PictureBox Width="100" Height="100" x:Name="pctMediaCapture"></wf:PictureBox>
        </WindowsFormsHost>
        <Button Content="Preview" HorizontalAlignment="Left" Height="19" Margin="6,14,0,0" VerticalAlignment="Top" Width="57" Click="cmdStartPreview_Click"/>
    </Grid>
</Window>

Télécharger le code

Eric