65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Windows 7 和 Windows 8 上捕获实时视频(来自网络摄像头)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (40投票s)

2014年5月23日

CPOL

17分钟阅读

viewsIcon

263686

downloadIcon

16363

使用 Media Foundation 从网络摄像头捕获实时视频的简单库

引言

大约一年前,我写了一篇关于使用 Media Foundation API 从网络摄像头捕获实时视频的短文 - 旧文章。许多开发人员关注了这篇文章并提出了一些建议。令我惊讶的是,这段代码竟然被用于 OpenCV 项目。这激发了我回顾代码的动力,我发现它的编码质量很差。因此,我决定重写这个项目,但过了一段时间,我决定写一篇新文章,因为新代码与旧代码不同。我认为最好有两篇不同的文章来解决这个问题。

背景

上一篇关于使用 Media Foundation 从网络摄像头捕获实时视频的文章,其目标是保持与另一个库 videoInput 的接口一致。然而,现在我认为使用另一个接口以获得更灵活的解决方案会更好。我已经决定在解决方案中包含选项,用于设置未压缩图像数据的图像格式、用于通过对象方式处理视频设备移除的接口、用于同步和异步读取模式的设置,以及将库的请求封装到对象实例和结果代码中。

为了开发最稳定的代码,我决定采用测试驱动开发策略。结果,我开发了 45 个测试来测试类和代码。 Media Foundation 的许多对象都被测试了很多次,方式各不相同。这使得代码更清晰、更稳定,并且易于验证,它还包含了一个用于检查代码执行结果的统一代码-结果枚举,包含 51 项。我还删除了上一篇文章中使用的线程,并简化了读写管道,这使得代码更稳定。

新项目与前一个项目有很大不同,我认为它将有助于解决其他开发人员遇到的问题。

Using the Code

库 `videoInput` 是用 Visual Studio 2012 编写的 - videoInputVS2012-static.zip。它可以作为一个 `static` 库使用,只需要将 `videoInput.lib` 和 `videoInput.h` 包含到新项目中即可。此项目的代码也可以下载 - videoInputVS2012-Source,并可以包含到任何项目中。

该项目包含 15 个类和接口

  • `videoInput` - 是一个类单例。这个类被设计成单例模式,便于管理资源。
  • `MediaFoundation` - 是一个单例类,负责管理 Media Foundation 资源的分配和释放。几乎所有的 Media Foundation 函数调用都来自这个类,并且这些调用的结果在这个类中被控制。
  • `VideoCaptureDeviceManager` - 是一个单例类,负责视频设备的分配、访问和释放。
  • `VideoCaptureDevice` - 是一个继承自 `IUnknow` 接口的类。它允许使用智能指针 `CComPtr` 来控制类的生命周期。这个类允许控制选定的视频捕获设备。
  • `VideoCaptureSession` - 是一个继承自 `IMFAsyncCallback` 的类。这个类用于处理由 `MediaEventGenerator` - `IMFMediaSession` 生成的事件。这个类处理视频捕获设备的事件并开始视频捕获。
  • `VideoCaptureSink` - 是一个继承自 `IMFSampleGrabberSinkCallback` 的类。这个类用于获取原始数据并将其写入缓冲区。
  • `IWrite` - 是一个类接口,用于在 `VideoCaptureSink` 中以唯一的方式使用不同类型的缓冲区来写入数据到缓冲区。
  • `IRead` - 是一个类接口,用于在 `VideoCaptureDevice` 中以唯一的方式使用不同类型的缓冲区来从缓冲区读取原始数据。
  • `IReadWriteBuffer` - 是一个类接口,它继承自 `IRead`、`IWrite` 和 `IUnknown`。这个类是不同类型缓冲区的基类,可以与智能指针 `CComPtr` 一起使用。
  • `ReadWriteBufferRegularAsync` - 是一个继承自 `IReadWriteBuffer` 的类。这个类使用 `critical section` 来阻止用户线程和 Media Foundation 内部线程对数据的写入-读取访问。但是,用户线程在进入 `critical section` 之前会检查 `readyToRead` 标志,如果它不准备好,用户线程将退出缓冲区而不读取数据。结果,用户线程不会被 Media Foundation 内部线程的写入操作阻塞。
  • `ReadWriteBufferRegularSync` - 是一个继承自 `public` `IReadWriteBuffer` 的类。这个类使用 `critical section` 来阻止用户线程和 Media Foundation 内部线程对数据的写入-读取访问。但是,用户线程在进入 `critical section` 之前会通过 `WaitForSingleObject` 被阻塞。结果,用户线程会等待 Media Foundation 内部线程大约 1 秒的事件对象,当事件到来时,用户线程开始从缓冲区读取,但如果事件没有到来,用户线程将以代码状态 `READINGPIXELS_REJECTED_TIMEOUT` 离开缓冲区。
  • `ReadWriteBufferFactory` - 是一个单例类,用于生产 `ReadWriteBufferRegularAsync` 或 `ReadWriteBufferRegularSync` 缓冲区。
  • `FormatReader` - 是一个用于读取 `MediaType` 格式数据的类。
  • `DebugPrintOut` - 是一个用于向控制台打印文本的类。
  • `CComMassivPtr` - 是一个用于处理具有 `IUnknow` 接口的对象数组的模板类。这个类允许封装调用 `SafeRelease()` 函数并控制这些对象的释放。

只需使用 `videoInput.h` 文件作为库的接口。其列表如下所示

//

#include <string>
#include <vector>
#include <guiddef.h>

using namespace std;

// Structure of info MediaType 
struct MediaType
{
    unsigned int MF_MT_FRAME_SIZE;
    unsigned int height;
    unsigned int width;         
    unsigned int MF_MT_YUV_MATRIX;
    unsigned int MF_MT_VIDEO_LIGHTING;
    unsigned int MF_MT_DEFAULT_STRIDE;
    unsigned int MF_MT_VIDEO_CHROMA_SITING;
    GUID MF_MT_AM_FORMAT_TYPE;
    wstring MF_MT_AM_FORMAT_TYPEName;
    unsigned int MF_MT_FIXED_SIZE_SAMPLES;
    unsigned int MF_MT_VIDEO_NOMINAL_RANGE;
    float MF_MT_FRAME_RATE_RANGE_MAX;
    float MF_MT_FRAME_RATE;
    float MF_MT_FRAME_RATE_RANGE_MIN;
    float MF_MT_PIXEL_ASPECT_RATIO;
    unsigned int MF_MT_ALL_SAMPLES_INDEPENDENT;
    unsigned int MF_MT_SAMPLE_SIZE;
    unsigned int MF_MT_VIDEO_PRIMARIES;
    unsigned int MF_MT_INTERLACE_MODE;
    GUID MF_MT_MAJOR_TYPE;
    wstring MF_MT_MAJOR_TYPEName;
    GUID MF_MT_SUBTYPE;
    wstring MF_MT_SUBTYPEName;    
};

// Stream structure
struct Stream
{
    std::vector<MediaType> listMediaType;
};

// Device info structure
struct Device
{
    wstring friendlyName;
    wstring symbolicName;
    std::vector<Stream> listStream;
};

// Structure for collecting info about one parametr of current video device
struct Parametr
{
    long CurrentValue;
    long Min;
    long Max;
    long Step;
    long Default; 
    long Flag;
    Parametr();
};

// Structure for collecting info about 17 parametrs of current video device
struct CamParametrs
{
        Parametr Brightness;
        Parametr Contrast;
        Parametr Hue;
        Parametr Saturation;
        Parametr Sharpness;
        Parametr Gamma;
        Parametr ColorEnable;
        Parametr WhiteBalance;
        Parametr BacklightCompensation;
        Parametr Gain;


        Parametr Pan;
        Parametr Tilt;
        Parametr Roll;
        Parametr Zoom;
        Parametr Exposure;
        Parametr Iris;
        Parametr Focus;
};

// Structure for defining color format for out data
struct CaptureVideoFormat
{
    enum VideoFormat
    {
        RGB24 = 0,
        RGB32 = 1,
        AYUV = 2
    };
};

// structure of defining of the type callback event
struct StopCallbackEvent
{
    enum CallbackEvent
    {
        STOP = 0,
        CAPTUREDEVICEREMOVED = 1
    };
};

// Interface for processing callback of the emergency stop - removing video device.
class IStopCallback
{
public:
    virtual void Invoke(StopCallbackEvent::CallbackEvent callbackEvent) = 0;
};

// Structure for defining mode of reading pixels
struct ReadMode
{
    enum Read
    {
        ASYNC = 0,
        SYNC = 1
    };
};

// Settings for setting up of video device.
struct DeviceSettings
{
    wstring symbolicLink;
    unsigned int indexStream;
    unsigned int indexMediaType;
};

// Settings for setting up mode of capturing raw pixels
struct CaptureSettings
{
    CaptureVideoFormat::VideoFormat videoFormat;
    IStopCallback *pIStopCallback;
    ReadMode::Read readMode;
};

// Structure for controlling reading raw pixels.
struct ReadSetting
{
    std::wstring symbolicLink;
    unsigned char *pPixels;
};

// Structure for controlling parameters of video device
struct CamParametrsSetting
{
    std::wstring symbolicLink;
    CamParametrs settings;
};

// Structure for defining result code of the working program
struct ResultCode
{
    enum Result
    {
        OK = 0,
        UNKNOWN_ERROR = 1,
        MEDIA_FOUNDATION_INITIALIZECOM_ERROR = 2,
        MEDIA_FOUNDATION_INITIALIZEMF_ERROR = 3,
        MEDIA_FOUNDATION_SHUTDOWN_ERROR = 4,
        MEDIA_FOUNDATION_ENUMDEVICES_ERROR = 5,
        MEDIA_FOUNDATION_CREATEATTRIBUTE_ERROR = 6,
        MEDIA_FOUNDATION_READFRIENDLYNAME_ERROR = 7,
        MEDIA_FOUNDATION_READSYMBOLICLINK_ERROR = 8,
        MEDIA_FOUNDATION_GETDEVICE_ERROR = 9,
        MEDIA_FOUNDATION_createPresentationDescriptor_ERROR = 10,
        MEDIA_FOUNDATION_GETTHEAMOUNTOFSTREAMS_ERROR = 11,
        MEDIA_FOUNDATION_GETSTREAMDESCRIPTORBYINDEX_ERROR = 12,
        MEDIA_FOUNDATION_ENUMMEDIATYPE_ERROR = 13,
        VIDEOCAPTUREDEVICEMANAGER_GETLISTOFDEVICES_ERROR = 14,
        MEDIA_FOUNDATION_SETSYMBOLICLINK_ERROR = 15,
        MEDIA_FOUNDATION_SETCURRENTMEDIATYPE_ERROR = 16,
        MEDIA_FOUNDATION_GETCURRENTMEDIATYPE_ERROR = 17,
        MEDIA_FOUNDATION_SELECTSTREAM_ERROR = 18,
        MEDIA_FOUNDATION_CREATESESSION_ERROR = 19,
        MEDIA_FOUNDATION_CREATEMEDIATYPE_ERROR = 20,
        MEDIA_FOUNDATION_SETGUID_ERROR = 21,
        MEDIA_FOUNDATION_SETUINT32_ERROR = 22,
        MEDIA_FOUNDATION_CREATESAMPLERGRABBERSINKACTIVE_ERROR = 23,
        MEDIA_FOUNDATION_CREATETOPOLOGY_ERROR = 24,
        MEDIA_FOUNDATION_CREATETOPOLOGYNODE_ERROR = 25,
        MEDIA_FOUNDATION_SETUNKNOWN_ERROR = 26,
        MEDIA_FOUNDATION_SETOBJECT_ERROR = 27,
        MEDIA_FOUNDATION_ADDNODE_ERROR = 28,
        MEDIA_FOUNDATION_CONNECTOUTPUTNODE_ERROR = 29,
        MEDIA_FOUNDATION_SETTOPOLOGY_ERROR = 30,
        MEDIA_FOUNDATION_BEGINGETEVENT_ERROR = 31,
        VIDEOCAPTUREDEVICEMANAGER_DEVICEISSETUPED = 32,
        VIDEOCAPTUREDEVICEMANAGER_DEVICEISNOTSETUPED = 33,
        VIDEOCAPTUREDEVICEMANAGER_DEVICESTART_ERROR = 34,
        VIDEOCAPTUREDEVICE_DEVICESTART_ERROR = 35,
        VIDEOCAPTUREDEVICEMANAGER_DEVICEISNOTSTARTED = 36,
        VIDEOCAPTUREDEVICE_DEVICESTOP_ERROR = 37,
        VIDEOCAPTURESESSION_INIT_ERROR = 38,
        VIDEOCAPTUREDEVICE_DEVICESTOP_WAIT_TIMEOUT = 39,
        VIDEOCAPTUREDEVICE_DEVICESTART_WAIT_TIMEOUT = 40,
        READINGPIXELS_DONE = 41,
        READINGPIXELS_REJECTED = 42,
        READINGPIXELS_MEMORY_ISNOT_ALLOCATED = 43,
        READINGPIXELS_REJECTED_TIMEOUT = 44,
        VIDEOCAPTUREDEVICE_GETPARAMETRS_ERROR = 45,
        VIDEOCAPTUREDEVICE_SETPARAMETRS_ERROR = 46,
        VIDEOCAPTUREDEVICE_GETPARAMETRS_GETVIDEOPROCESSOR_ERROR = 47,
        VIDEOCAPTUREDEVICE_GETPARAMETRS_GETVIDEOCONTROL_ERROR = 48,
        VIDEOCAPTUREDEVICE_SETPARAMETRS_SETVIDEOCONTROL_ERROR = 49,
        VIDEOCAPTUREDEVICE_SETPARAMETRS_SETVIDEOPROCESSOR_ERROR = 50
    };
};

class videoInput
{
public:

    // get static instance of the singleton
    static videoInput& getInstance();

    // filling list of the video devices. 
    ResultCode::Result getListOfDevices(vector<Device> &listOfDevices);

    // setting up selected video device and capture settings.
    ResultCode::Result setupDevice(DeviceSettings deviceSettings, 
                                   CaptureSettings captureSettings);

    // closing selected video device.
    ResultCode::Result closeDevice(DeviceSettings deviceSettings);

    // closing all setup video devices
    ResultCode::Result closeAllDevices();

    // reading raw data of pixels
    ResultCode::Result readPixels(ReadSetting readSetting);

    // getting parametrs of video device
    ResultCode::Result getParametrs(CamParametrsSetting &parametrs);

    // setting parametrs of video device
    ResultCode::Result setParametrs(CamParametrsSetting parametrs);

    // Setting of the state of outprinting info on consol
    ResultCode::Result setVerbose(bool state);

private:
    videoInput(void);
    ~videoInput(void);
    videoInput(const videoInput&);
    videoInput& operator=(const videoInput&);
}; 

库的接口已经变得简单,一些对原始数据的操作已经成为开发者的责任。 `videoInput` 的方法有以下用途:

  • `getListofDevices` - 用于填充活动视频设备列表的方法。事实上,在旧项目中,这个列表是在初始化时填充的,并且不会改变。现在,调用这个方法将生成一个活动视频设备的新列表,并且可以进行更改。每个设备包含 `friendlyName`、`symbolicLink` 和 `stream` 列表的 `string`。`friendlyName` `string` 用于显示设备的“可读”名称。`symbolicLink` `string` 是用于管理设备的唯一名称。
  • `setupDevice` - 用于设置设备并开始从设备捕获的方法。它有两个参数:`deviceSettings` 和 `captureSettings`。第一个用于通过 `symbolicLink`、`indexStream` 和 `indexMediaType` 将设备设置为选定模式。第二个用于设置捕获模式。它包括用于选择未压缩图像数据格式的 `videoFormat` - `RGB24`、`RGB32` 和 `AYUV`;`pIStopCallback` 是指向回调类接口的指针,当视频捕获设备移除时执行;`readMode` 是用于同步和异步读取原始数据的缓冲区类型的枚举。
  • `closeDevice` - 用于停止和关闭选定设备的方法。该设备由 `symbolicName` 定义。
  • `closeAllDevices` - 用于关闭所有已设置设备的工厂。
  • `readPixels` - 用于将数据从缓冲区读取到指针的方法。参数 `readSetting` 包含 `symbolicLink` 以访问设备,以及 `pPixels` 以保存指向使用的原始图像数据的指针。
  • `getParametrs` - 用于获取视频捕获设备参数的方法。
  • `setParametrs` - 用于设置视频捕获设备参数的方法。
  • `setVerbose` - 用于设置在控制台打印信息的模式的方法。

所有这些方法都返回 `ResultCode::Result` 枚举中的一个项。它允许控制每个方法的执行结果,并获取有关代码中几乎所有进程的信息。

我想强调这个库的三个特殊功能

  1. 捕获图像的分辨率通过选择设备的适当 `MediaType` 来定义。在 旧文章 中,分辨率是手动设置的。然而,这会导致一个错误,即可以设置任何分辨率。我决定新的解决方案应该更清晰地呈现视频捕获设备的限制。
  2. 可以选择三种颜色格式之一来捕获图像 - `RGB24`、`RGB32` 和 `AYUV`。这允许选择合适的格式。`RGB24` 和 `RGB32` 颜色格式外观相同,但后者更适合在具有 16 字节对齐的现代处理器上处理数据。
  3. 从缓冲区读取可以有两种方式之一:同步或异步。通过设置 `readMode` 模式来做出此选择。在 `ReadMode::ASYNC` 模式下,从缓冲区读取不会阻塞,如果数据未准备好,`readPixels()` 方法将返回结果代码 `ResultCode::READINGPIXELS_REJECTED`。在 `ReadMode::SYNC` 模式下,从缓冲区读取会被阻塞,如果数据未准备好,`readPixels()` 方法会阻塞用户线程 1 秒,然后返回结果代码 `ResultCode::READINGPIXELS_REJECTED_TIMEOUT`。

下一个代码列表展示了如何使用此库与 OpenCV 一起从网络摄像头捕获实时视频。

#include "stdafx.h"
#include "../videoInput/videoInput.h"
#include "include\opencv2\highgui\highgui_c.h"
#include "include\opencv2\imgproc\imgproc_c.h"

#pragma comment(lib, "../Debug/videoInput.lib")
#pragma comment(lib, "lib/opencv_highgui248d.lib")
#pragma comment(lib, "lib/opencv_core248d.lib")

int _tmain(int argc, _TCHAR* argv[])
{
    using namespace std;        
    vector<Device> listOfDevices;
    ResultCode::Result result = videoInput::getInstance().getListOfDevices(listOfDevices);
    DeviceSettings deviceSettings;
    deviceSettings.symbolicLink = listOfDevices[0].symbolicName;
    deviceSettings.indexStream = 0;
    deviceSettings.indexMediaType = 0;            
    CaptureSettings captureSettings;
    captureSettings.pIStopCallback = 0;
    captureSettings.readMode = ReadMode::SYNC;
    captureSettings.videoFormat = CaptureVideoFormat::RGB32;

    MediaType MT = listOfDevices[0].listStream[0].listMediaType[0];
    cvNamedWindow ("VideoTest", CV_WINDOW_AUTOSIZE);
    CvSize size = cvSize(MT.width, MT.height);
 
    IplImage* frame;
    frame = cvCreateImage(size, 8,4);
    
    ReadSetting readSetting;
    readSetting.symbolicLink = deviceSettings.symbolicLink;
    readSetting.pPixels = (unsigned char *)frame->imageData;
    result = videoInput::getInstance().setupDevice(deviceSettings, captureSettings);
    while(1)
    {
        ResultCode::Result readState = videoInput::getInstance().readPixels(readSetting);

        if(readState == ResultCode::READINGPIXELS_DONE)
        {            
            cvShowImage("VideoTest", frame);
        }
        else
            break;
                    
        char c = cvWaitKey(33);
 
        if(c == 27) 
            break;
    }

    result = videoInput::getInstance().closeDevice(deviceSettings);
  
    return 0;
}

最后,我想说的是,在这个项目中,我删除了重新排序 BGR 到 RGB 颜色和垂直翻转的代码。事实是,Windows 平台的这些特定功能可以通过多种方式得到弥补——例如,OpenCV 库默认以 BGR 颜色顺序处理,DirectX 和 OpenGL 支持 BGR 和 RGB 纹理,以及垂直翻转。我决定开发者可以根据自己的目的编写颜色重新排序和垂直翻转的代码。

我已将该项目代码发布在该网站的 Git 仓库上,任何人都可以通过链接 videoInput 克隆它。

更新至 VS2013Express for Desktop

我收到了许多关于使用这个库的问题,其中一些人对该库独立于我用于测试示例的 OpenCV 库表示怀疑。我理解这种怀疑,因为 OpenCV 包含了支持网络摄像头工作的代码。为了证明我的代码独立于 OpenCV 库(事实上,OpenCV 代码部分基于我的旧代码),我决定编写独立的代码来可视化网络摄像头视频。当然,这样的代码需要一些支持图像绘制的 Windows 框架——因此,需要一些可能导致新疑虑的依赖项。所以,我决定使用 OpenGL 进行可视化。我拥有丰富的 OpenGL 经验,我编写了一个包含所有所需代码(除 `videoInput.lib` 外)的单文件示例。

#define WIN32_LEAN_AND_MEAN    

#include <windows.h>

#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>
#include <gl/gl.h>
#include <memory>
#include <vector>
#include "../videoInput/videoInput.h"

#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "../videoInput/Debug/videoInput.lib")

#define GL_BGR                            0x80E0

/**************************
* Function Declarations
*
**************************/

LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
    WPARAM wParam, LPARAM lParam);
void EnableOpenGL(HWND hWnd, HDC *hDC, HGLRC *hRC);
void DisableOpenGL(HWND hWnd, HDC hDC, HGLRC hRC);

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPTSTR    lpCmdLine,
    _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    /* initialisation videoinput */
    using namespace std;
    vector<Device> listOfDevices;
    ResultCode::Result result = videoInput::getInstance().getListOfDevices(listOfDevices);
    if (listOfDevices.size() == 0)
        return -1;
    DeviceSettings deviceSettings;
    deviceSettings.symbolicLink = listOfDevices[0].symbolicName;
    deviceSettings.indexStream = 0;
    deviceSettings.indexMediaType = 0;
    CaptureSettings captureSettings;
    captureSettings.pIStopCallback = 0;
    captureSettings.readMode = ReadMode::SYNC;
    captureSettings.videoFormat = CaptureVideoFormat::RGB24;
    MediaType MT = listOfDevices[0].listStream[0].listMediaType[0];
    unique_ptr<unsigned char> frame(new unsigned char[3 * MT.width * MT.height]);

    ReadSetting readSetting;
    readSetting.symbolicLink = deviceSettings.symbolicLink;
    readSetting.pPixels = frame.get();
    result = videoInput::getInstance().setupDevice(deviceSettings, captureSettings);
    ResultCode::Result readState = videoInput::getInstance().readPixels(readSetting);
    
    /* check access to the video device */
    if (readState != ResultCode::READINGPIXELS_DONE)
        return -1;

    float halfQuadWidth = 0.75;
    float halfQuadHeight = 0.75;

    WNDCLASS wc;
    HWND hWnd;
    HDC hDC;
    HGLRC hRC;
    MSG msg;
    BOOL bQuit = FALSE;
    float theta = 0.0f;

    /* register window class */
    wc.style = CS_OWNDC;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = L"OpenGLWebCamCapture";
    RegisterClass(&wc);

    /* create main window */
    hWnd = CreateWindow(
        L"OpenGLWebCamCapture", L"OpenGLWebCamCapture Sample",
        WS_CAPTION | WS_POPUPWINDOW | WS_VISIBLE,
        0, 0, MT.width, MT.height,
        NULL, NULL, hInstance, NULL);

    /* enable OpenGL for the window */
    EnableOpenGL(hWnd, &hDC, &hRC);

    GLuint textureID;
    glGenTextures(1, &textureID);

    // "Bind" the newly created texture : all future texture functions will modify this texture
    glBindTexture(GL_TEXTURE_2D, textureID);

    // Give the image to OpenGL
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, MT.width, MT.height, 0, GL_BGR, GL_UNSIGNED_BYTE, frame.get());

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

    /* program main loop */
    while (!bQuit)
    {
        /* check for messages */
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            /* handle or dispatch messages */
            if (msg.message == WM_QUIT)
            {
                bQuit = TRUE;
            }
            else
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }
        else
        {
            /* OpenGL animation code goes here */

            ResultCode::Result readState = videoInput::getInstance().readPixels(readSetting);

            if (readState != ResultCode::READINGPIXELS_DONE)
                break;

            glClear(GL_COLOR_BUFFER_BIT);
            glLoadIdentity();

            glBindTexture(GL_TEXTURE_2D, textureID);

            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, MT.width, 
                    MT.height, GL_BGR, GL_UNSIGNED_BYTE, frame.get());

            glBegin(GL_QUADS);      // Beginning of drawing of Square.

            glTexCoord2f(0, 1);  glVertex2f(-halfQuadWidth, -halfQuadHeight);    // Bottom Left
            glTexCoord2f(1, 1);  glVertex2f(halfQuadWidth, -halfQuadHeight);    // Bottom right
            glTexCoord2f(1, 0);  glVertex2f(halfQuadWidth, halfQuadHeight);        // Top right
            glTexCoord2f(0, 0);  glVertex2f(-halfQuadWidth, halfQuadHeight);    // Top Left
            glEnd();

            SwapBuffers(hDC);

            Sleep(1);
        }
    }

    /* shutdown OpenGL */
    DisableOpenGL(hWnd, hDC, hRC);

    /* destroy the window explicitly */
    DestroyWindow(hWnd);

    /* release captured webcam */
    videoInput::getInstance().closeDevice(deviceSettings);

    return msg.wParam;
}

/********************
* Window Procedure
*
********************/

LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
    WPARAM wParam, LPARAM lParam)
{

    switch (message)
    {
    case WM_CREATE:
        return 0;
    case WM_CLOSE:
        PostQuitMessage(0);
        return 0;

    case WM_DESTROY:
        return 0;

    case WM_KEYDOWN:
        switch (wParam)
        {
        case VK_ESCAPE:
            PostQuitMessage(0);
            return 0;
        }
        return 0;

    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
}

/*******************
* Enable OpenGL
*
*******************/

void EnableOpenGL(HWND hWnd, HDC *hDC, HGLRC *hRC)
{
    PIXELFORMATDESCRIPTOR pfd;
    int iFormat;

    /* get the device context (DC) */
    *hDC = GetDC(hWnd);

    /* set the pixel format for the DC */
    ZeroMemory(&pfd, sizeof (pfd));
    pfd.nSize = sizeof (pfd);
    pfd.nVersion = 1;
    pfd.dwFlags = PFD_DRAW_TO_WINDOW |
        PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
    pfd.iPixelType = PFD_TYPE_RGBA;
    pfd.cColorBits = 24;
    pfd.cDepthBits = 16;
    pfd.iLayerType = PFD_MAIN_PLANE;
    iFormat = ChoosePixelFormat(*hDC, &pfd);
    SetPixelFormat(*hDC, iFormat, &pfd);

    /* create and enable the render context (RC) */
    *hRC = wglCreateContext(*hDC);
    wglMakeCurrent(*hDC, *hRC);

    glEnable(GL_TEXTURE_2D);            // Enable of texturing for OpenGL

}

/******************
* Disable OpenGL
*
******************/

void DisableOpenGL(HWND hWnd, HDC hDC, HGLRC hRC)
{
    wglMakeCurrent(NULL, NULL);
    wglDeleteContext(hRC);
    ReleaseDC(hWnd, hDC);
}

在开发新的文本示例时,我决定将代码替换为 Visual Studio 2013 Express for Desktop。因此,我重写了旧代码并将其上传到以下链接:

 

更新至 C++/CLI、C# 和 WPF

关于本文和代码的最新问题引起了我对类似任务的其他解决方案的关注。因此,我发现了一些作者建议使用 EmguCV 库来捕获 C# 语言平台上的网络摄像头实时视频的文章。我了解这些库。它们支持强大的算法来创建计算机视觉项目。我认为,如果项目只需要连接到网络摄像头,使用这些库并不是一个好的解决方案。此外,一些程序员更喜欢使用更简单的解决方案,这些解决方案可以集成到项目代码层面。

所以,我决定为我原来的 videoInput 库编写一个简单的 C++/CLI 包装器。我选择编写包装器是因为 C++/CLI 和 C++ 并非完全兼容。事实是,要完全实现 C++/CLI,我需要重写大约 90% 的代码,这会失去与 C++ 中其他代码的兼容性——这会导致编写新代码和新文章。另一方面,包装器代码使用了 videoInputVS2013Express_for_Desktop 的代码,并且原始 C++ 代码的任何改进都可以轻松地替换到 C++/CLI 包装器中。为了证明包装器在 C# 中有效,我编写了一个简单的 WPF 示例——结果显示在下一张图片中。

源代码和示例可以通过链接找到:下载 videoInputVS2013Express_for_DesktopC++CLI.zip

更新 C# 封送 C 包装器

C++/CLI 版本的 videoInput 允许将网络摄像头捕获的图像无缝集成到 C# 项目中。然而,过了一段时间,我发现了一个令人沮丧的事实——无法在 Visual Studio 2013 中为旧版本的 dotNet 编写 C++/CLI 解决方案。这意味着 Visual Studio 2013 的**平台工具集** **v120** for C++/CLI 只支持 dotNet 4.5,并且**不可能**将开发在 Visual Studio 2013 上的 C++/CLI 解决方案集成到 dotNet 3.5 及更早版本的项目中——我在开发用于旧的 dotNet 3.5 项目的网络摄像头 MediaFoundation 支持插件时遇到了这个问题。我决定不可能重新编译所有项目为 dotNet 4.5 来仅支持一个插件。我决定只有一个方法可以解决这个问题——为 C++ videoInput 库编写一个 C 包装器,并为 dotNet 3.5 插件编写封送代码。我编写了 C 库 USBCameraProxy 代码:

USBCameraProxy.h

#include <Windows.h>
#include <stdio.h>
#include "videoInput.h"

#define EXPORT extern "C" __declspec(dllexport)

// structure for storing information about resolution
typedef struct _Resolution
{
	int width;
	int height;
} Resolution, *PtrResolution;

// get amount of accessible web cams
EXPORT int __cdecl getDeviceCount();

// fill frendly name, symbolic name on web cam with the specific number, and get an amount of supported MediaTypes (resolutions)
EXPORT int __cdecl getDeviceInfo(int number, wchar_t *aPtrFriendlyName, wchar_t *aPtrSymbolicName);

// fill pointer on resolution data for web cam, which is defined by symbolic name and number of media Type. 
EXPORT int __cdecl getDeviceResolution(int number, wchar_t *aPtrSymbolicName, PtrResolution aPtrResolution);

// setup device, which is defined by symbolic name with the selected index of resolution, capture video format and read mode.
EXPORT int __cdecl setupDevice(wchar_t *aPtrSymbolicName, int aResolutionIndex, int aCaptureVideoFormat, int ReadMode);

// close all setuped devices.
EXPORT int __cdecl closeAllDevices();

// close device with the specific symbolic name.
EXPORT int __cdecl closeDevice(wchar_t *aPtrSymbolicName);

// fill pixel data, which is defined by pixel pointer by web cam, which is defined by symbolic name.
EXPORT int __cdecl readPixels(wchar_t *aPtrSymbolicName, unsigned char *aPtrPixels);

// set Camera parameters of web cam, which is defined by symbolic name.
EXPORT int __cdecl setCamParametrs(wchar_t *aPtrSymbolicName, CamParametrs *aPtrCamParametrs);

// get Camera parameters of web cam, which is defined by symbolic name.
EXPORT int __cdecl getCamParametrs(wchar_t *aPtrSymbolicName, CamParametrs *aPtrCamParametrs);

USBCameraProxy.cpp

#include "USBCameraProxy.h"

#include <algorithm>


EXPORT int __cdecl getDeviceCount()
{
	vector<device> listOfDevices;

	videoInput::getInstance().getListOfDevices(listOfDevices);

	return listOfDevices.size();
}

EXPORT int __cdecl getDeviceInfo(int number, wchar_t *aPtrFriendlyName, wchar_t *aPtrSymbolicName)
{
	int lresult = -1;

	do
	{
		if (number < 0)
			break;

		vector<device> listOfDevices;

		videoInput::getInstance().getListOfDevices(listOfDevices);

		if (listOfDevices.size() <= 0)
			break;

		wcscpy(aPtrFriendlyName, listOfDevices[number].friendlyName.c_str());

		wcscpy(aPtrSymbolicName, listOfDevices[number].symbolicName.c_str());
		
		lresult = listOfDevices[number].listStream[0].listMediaType.size();

	} while(false);

	return lresult;
}

EXPORT int __cdecl getDeviceResolution(int number, wchar_t *aPtrSymbolicName, PtrResolution aPtrResolution)
{
	int lresult = -1;

	do
	{
		if (number < 0)
			break;

		std::wstring lSymbolicName(aPtrSymbolicName);

		vector<device> listOfDevices;

		videoInput::getInstance().getListOfDevices(listOfDevices);

		if (listOfDevices.size() <= 0)
			break;

		auto lfindIter = std::find_if(
			listOfDevices.begin(),
			listOfDevices.end(),
			[lSymbolicName](Device lDevice)
		{
			return lDevice.symbolicName.compare(lSymbolicName) == 0;
		});
		
		if (lfindIter != listOfDevices.end())
		{
			aPtrResolution->height = (*lfindIter).listStream[0].listMediaType[number].height;

			aPtrResolution->width = (*lfindIter).listStream[0].listMediaType[number].width;

			lresult = 0;
		}

	} while (false);

	return lresult;
}

EXPORT int __cdecl setupDevice(wchar_t *aPtrSymbolicName, int aResolutionIndex, int aCaptureVideoFormat, int ReadMode)
{
	int lresult = -1;
	
	do
	{
		if (aResolutionIndex < 0)
			break;

		DeviceSettings deviceSettings;

		deviceSettings.symbolicLink = std::wstring(aPtrSymbolicName);

		deviceSettings.indexStream = 0;

		deviceSettings.indexMediaType = aResolutionIndex;


		CaptureSettings captureSettings;

		captureSettings.pIStopCallback = nullptr;

		captureSettings.videoFormat = CaptureVideoFormat::VideoFormat(aCaptureVideoFormat);// ::RGB24;

		captureSettings.readMode = ReadMode::Read(ReadMode);

		lresult = videoInput::getInstance().setupDevice(deviceSettings, captureSettings);


	} while (false);

	return lresult;
}

EXPORT int __cdecl closeAllDevices()
{
	return videoInput::getInstance().closeAllDevices();
}

EXPORT int __cdecl closeDevice(wchar_t *aPtrSymbolicName)
{

	DeviceSettings deviceSettings;

	deviceSettings.symbolicLink = std::wstring(aPtrSymbolicName);

	deviceSettings.indexStream = 0;

	deviceSettings.indexMediaType = 0;

	return videoInput::getInstance().closeDevice(deviceSettings);

}

EXPORT int __cdecl readPixels(wchar_t *aPtrSymbolicName, unsigned char *aPtrPixels)
{
	ReadSetting readSetting;

	readSetting.symbolicLink = std::wstring(aPtrSymbolicName);

	readSetting.pPixels = aPtrPixels;

	int lresult = videoInput::getInstance().readPixels(readSetting);

	return lresult;
}

EXPORT int __cdecl setCamParametrs(wchar_t *aPtrSymbolicName, CamParametrs *aPtrCamParametrs)
{
	CamParametrsSetting lCamParametrsSetting;

	lCamParametrsSetting.symbolicLink = std::wstring(aPtrSymbolicName);

	lCamParametrsSetting.settings = *aPtrCamParametrs;

	videoInput::getInstance().setParametrs(lCamParametrsSetting);

	return 0;
}

EXPORT int __cdecl getCamParametrs(wchar_t *aPtrSymbolicName, CamParametrs *aPtrCamParametrs)
{
	CamParametrsSetting lCamParametrsSetting;

	lCamParametrsSetting.symbolicLink = std::wstring(aPtrSymbolicName);

	videoInput::getInstance().getParametrs(lCamParametrsSetting);

	*aPtrCamParametrs = lCamParametrsSetting.settings;

	return 0;
}

结构体 `VideoInputWrap` 包含内部类和方法,用于在 VideoInputWrap.cs 中“反射” C 库到 C#。

    public struct VideoInputWrap
    {
        [StructLayout( LayoutKind.Sequential, Pack = 1 )]
        public class Resolution
        {
            public int width;
            public int height;

            public override string ToString()
            {
                return width.ToString() + " x " + height.ToString();
            }
        }

        // Structure for collecting info about one parametr of current video device
        [StructLayout(LayoutKind.Sequential)]
        public class Parametr
        {
            public int CurrentValue;

            public int Min;

            public int Max;

            public int Step;

            public int Default;

            public int Flag;
        };

        // Structure for collecting info about 17 parametrs of current video device
        [StructLayout(LayoutKind.Sequential)]
        public class CamParametrs
        {
            public Parametr Brightness;
            public Parametr Contrast;
            public Parametr Hue;
            public Parametr Saturation;
            public Parametr Sharpness;
            public Parametr Gamma;
            public Parametr ColorEnable;
            public Parametr WhiteBalance;
            public Parametr BacklightCompensation;
            public Parametr Gain;


            public Parametr Pan;
            public Parametr Tilt;
            public Parametr Roll;
            public Parametr Zoom;
            public Parametr Exposure;
            public Parametr Iris;
            public Parametr Focus;
        };

        const string DLLNAME = "USBCameraProxy.dll";

        [DllImport(DLLNAME)]
        public static extern int getDeviceCount();

        [DllImport(DLLNAME, CharSet = CharSet.Unicode)]
        public static extern int getDeviceInfo(
            int number, 
            StringBuilder aPtrFriendlyName, 
            StringBuilder aPtrSymbolicName
            );

        [DllImport(DLLNAME, CharSet = CharSet.Unicode)]
        public static extern int getDeviceResolution(
            int number,
            StringBuilder aPtrSymbolicName,
            [ Out] Resolution aPtrResolution
            );

        [DllImport(DLLNAME, CharSet = CharSet.Unicode)]
        public static extern int setupDevice(
            StringBuilder aPtrSymbolicName, 
            int aResolutionIndex, 
            int aCaptureVideoFormat, 
            int ReadMode
           );

        [DllImport(DLLNAME)]
        public static extern int closeAllDevices();

        [DllImport(DLLNAME, CharSet = CharSet.Unicode)]
        public static extern int closeDevice(
            StringBuilder aPtrSymbolicName
           );

        [DllImport(DLLNAME, CharSet = CharSet.Unicode)]
        public static extern int readPixels(
            StringBuilder aPtrSymbolicName,
            IntPtr aPtrPixels
           );

        [DllImport(DLLNAME, CharSet = CharSet.Unicode)]
        public static extern int getCamParametrs(
            StringBuilder aPtrSymbolicName,
            [Out] CamParametrs lpCamParametrs
           );

        [DllImport(DLLNAME, CharSet = CharSet.Unicode)]
        public static extern int setCamParametrs(
            StringBuilder aPtrSymbolicName,
            [In] CamParametrs lpCamParametrs
           );
    }

    public class Device
    {
        public IList<videoinputwrap.resolution> mResolutionList = new List<videoinputwrap.resolution>();

        public StringBuilder mFriendlyName = new StringBuilder(256);

        public StringBuilder mSymbolicName = new StringBuilder(256);

        public override string ToString()
        {
            return mFriendlyName.ToString();
        }

    }

C 包装器库的名称由 `const string DLLNAME = "USBCameraProxy.dll"` 定义。

封送结构体 `VideoInputWrap` 可以采用以下形式使用:

以下代码填充网络摄像头设备列表

    List<device> mDeviceList = new List<device>();
    
    StringBuilder aFriendlyName = new StringBuilder(256);

    StringBuilder aSymbolicName = new StringBuilder(256);
    
    var lcount = VideoInputWrap.getDeviceCount();

    if(lcount > 0)
    {
        for (int lindex = 0; lindex < lcount; ++lindex )
        {
            var lresult = VideoInputWrap.getDeviceInfo(lindex, aFriendlyName, aSymbolicName);

            if (lresult <= 0)
                continue;

            Device lDevice = new Device();

            lDevice.mFriendlyName.Append(aFriendlyName.ToString());

            lDevice.mSymbolicName.Append(aSymbolicName.ToString());

            for (var lResolutionIndex = 0; lResolutionIndex < lresult; ++lResolutionIndex)
            {
                VideoInputWrap.Resolution k = new VideoInputWrap.Resolution();

                var lResolutionResult = VideoInputWrap.getDeviceResolution(lResolutionIndex, aSymbolicName, k);

                if (lResolutionResult < 0)
                    continue;

                lDevice.mResolutionList.Add(k);
            }

            mDeviceList.Add(lDevice);
       }

    }

以下代码以指定的**分辨率**索引 `aResolutionIndex` **开始**从设备 `aDevice` **捕获**

    private IntPtr ptrImageBuffer = IntPtr.Zero;

    private DispatcherTimer m_timer = new DispatcherTimer();

    private StringBuilder mSymbolicName;

    private BitmapSource m_BitmapSource = null;

    public VideoInputWrap.CamParametrs m_camParametrs;

    private Device m_Device;
    
    public void startCapture(Device aDevice, int aResolutionIndex)
    {

        mSymbolicName = new StringBuilder(aDevice.mSymbolicName.ToString());

        int lbufferSize = (3 * aDevice.mResolutionList[aResolutionIndex].width * aDevice.mResolutionList[aResolutionIndex].height);

        ptrImageBuffer = Marshal.AllocHGlobal(lbufferSize);

        var lr = VideoInputWrap.setupDevice(aDevice.mSymbolicName, aResolutionIndex, 0, 1);

        m_camParametrs = new VideoInputWrap.CamParametrs();

        m_Device = aDevice;

        var lres = VideoInputWrap.getCamParametrs(aDevice.mSymbolicName, m_camParametrs);

        m_timer.Tick += delegate
        {
            var result = VideoInputWrap.readPixels(mSymbolicName, ptrImageBuffer);

            if (result != 41)
               m_timer.Stop();

            m_BitmapSource = FromNativePointer(ptrImageBuffer, aDevice.mResolutionList[aResolutionIndex].width,
            aDevice.mResolutionList[aResolutionIndex].height, 3);

            displayImage.Source = m_BitmapSource;

        };

        m_timer.Interval = new TimeSpan(0, 0, 0, 0, 1);

        m_timer.Start();
    }
    

    [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")]
    public static extern void CopyMemory(IntPtr Destination, IntPtr Source, uint Length);

    public static BitmapSource FromNativePointer(IntPtr pData, int w, int h, int ch)
    {
        PixelFormat format = PixelFormats.Default;

        if (ch == 1) format = PixelFormats.Gray8; //grey scale image 0-255
        if (ch == 3) format = PixelFormats.Bgr24; //RGB
        if (ch == 4) format = PixelFormats.Bgr32; //RGB + alpha


        WriteableBitmap wbm = new WriteableBitmap(w, h, 96, 96, format, null);
        CopyMemory(wbm.BackBuffer, pData, (uint)(w * h * ch));

        wbm.Lock();
        wbm.AddDirtyRect(new Int32Rect(0, 0, wbm.PixelWidth, wbm.PixelHeight));
        wbm.Unlock();

        return wbm;
    }

此解决方案运行良好,并允许与 dotNet 3.5 上的旧项目一起使用。可以在链接处下载 Visual Studio 2013 上的 C 包装器项目:USBCameraProxy.zip

 

更新版本以支持 x64 Windows 平台并改进多线程同步。

一些评论引起了我对 x64 平台编译问题的注意。事实是 x86 和 x64 的 `InterlockedIncrement` 函数支持不同——事实上,在 x64 上 `InterlockedIncrement` 没有 `long` 类型参数的重载,但在我的 x86 版本中我使用了这种类型。我收到建议将 `long` 类型替换为 `unsigned long` 类型。

然而,我决定做的不只是简单地添加 `unsigned` 关键字——我决定用 STL C++11 解决方案替换 `AddRef - Release`。

class VideoCaptureSink
{
...
std::atomic<unsigned long> refCount;
}
STDMETHODIMP_(ULONG) VideoCaptureSink::AddRef()
{
	return ++refCount;
}

STDMETHODIMP_(ULONG) VideoCaptureSink::Release()
{
	ULONG cRef = --refCount;

    if (cRef == 0)
    {
        delete this;
    }

    return cRef;
}

所以,您可以看到我有了相同的解决方案,它不依赖于 Windows SDK 的语义。我认为这是解决 Windows SDK 问题的一个更正确的解决方案。

在解决移植到 x64 平台的问题时,我决定通过用 STL C++11 解决方案替换 Windows SDK 代码来改进多线程同步。

这在 Windows SDK 上具有通用形式。

HANDLE syncEvent;

syncEvent = CreateEvent(NULL,FALSE,FALSE,NULL);

// in user thread
{

	dwWaitResult = WaitForSingleObject(syncEvent, 1000);

	if (dwWaitResult == WAIT_TIMEOUT)
	{
	
	
	}

}

// in MediaFoundation thread
{

	if(state)
	{
		SetEvent(syncEvent);
	}
	else
	{
		ResetEvent(syncEvent);
	}

}

这在 STL C++11 上具有通用形式。

std::condition_variable mConditionVariable;

std::mutex mMutex;

// in user thread
{

	std::unique_lock<std::mutex> lock(mMutex);

	auto lconditionResult = mConditionVariable.wait_for(lock, std::chrono::seconds(1));

	if (lconditionResult == std::cv_status::timeout)
	{
	
	
	}

}

// in MediaFoundation thread
{

	if(state)
	{
		mConditionVariable.notify_all();
	}

}

所以,正如您所见,新解决方案更加面向对象,看起来更漂亮。

我在 x86 和 x64 平台上测试了新解决方案,它运行良好。新版本可以通过以下链接下载 - videoInputVS2013Express_for_Desktop_x64.zip

 

更新支持多接收器输出。

本文所介绍代码的主要目标是提供一个简单而灵活的解决方案,用于从网络摄像头抓取原始图像数据。在许多项目中,图像都会根据特定目的进行处理。然而,作为代码基础的 Media Foundation 功能更强大。为了扩展库的功能,我决定扩展接口。我用新版本替换了旧的 `setupDevice` 方法

ResultCode::Result setupDevice(
        DeviceSettings deviceSettings, 
        CaptureSettings captureSettings);

使用新版本

ResultCode::Result setupDevice(
        DeviceSettings deviceSettings, 
        CaptureSettings captureSettings, 
        std::vector<TopologyNode> aNodesVector = std::vector<TopologyNode>(),
        bool aIsSampleGrabberEnable = true);

新版本的 `setupDevice` 方法反映了主要变化——添加了一个 `TopologyNode` 结构体向量。该结构体的视图如下:

struct IMFTopologyNode;

struct TopologyNode
{
	IMFTopologyNode* mPtrIMFTopologyNode;

	bool mConnectedTopologyNode;

	TopologyNode():
		mPtrIMFTopologyNode(nullptr),
		mConnectedTopologyNode(false)
	{}
};

此结构体是 Media Foundation 对象 `IMFTopologyNode` 接口的容器,它可以被“注入”到从网络摄像头捕获视频的管道中。

在旧版本的 videoInput 中,源和采样捕获器之间的连接或拓扑可以用以下模式表示:

 

拓扑有两个节点:源节点和接收器节点,Media Foundation 会通过添加必要的转换来解决它。此模式很简单,但在这种拓扑中,流仅定向到一个节点进行消耗。然而,开发者可能需要更灵活的解决方案,具有多个消耗节点——例如:抓取原始图像、在窗口上显示实时视频、向视频注入水印、将实时视频保存到视频文件、网络广播实时视频。这些类型的视频处理需要一个视频捕获库,该库可以将源流复制到多个接收器——多接收器输出。

在新版本的 videoInput 中,源和多个接收器之间的拓扑可以用以下模式表示:

Media Foundation 框架有一个集成的拓扑节点“TeeNode”,它允许将多个输出节点连接到一个源节点的输出引脚。新版本的 videoInput 有一个集成的、已连接的输出节点——Sample Graber Sink,它捕获分离的图像,但是可以连接许多额外的节点来获取网络摄像头的视频流。需要注意的是,注入的节点不仅可以是视频渲染器或视频文件写入器接收器等输出节点,它们还可以是处理图像的转换节点——例如,在图像上写水印、平滑图像、缩放图像等。

新版本的 videoInput 的使用示例可以在链接处找到:videoInputVS2013Express_for_Desktop_x64_multisink_version.zip

以下列表中的代码可以解释如何使用新方法。

std::vector<TopologyNode> aNodesVector;

auto lhresult = createOutputNode(lhwnd, aNodesVector);
		
if (FAILED(lhresult))
    break;

resultCode = videoInput::getInstance().setupDevice(deviceSettings, captureSettings, aNodesVector);

在此代码中,节点向量 `aNodesVector` 在 `createOutputNode` 函数中创建并填充,然后发送到 videoinput 库以注入 Media Foundation 拓扑。填充节点向量的代码在下一个列表中给出。

HRESULT createOutputNode(
	HWND aHWNDVideo,
	std::vector<TopologyNode> &aNodesVector)
{
	HRESULT lhresult = S_OK;

	CComPtrCustom<IMFMediaTypeHandler> lHandler;

	CComPtrCustom<IMFActivate> lRendererActivate;

	CComPtrCustom<IMFTopologyNode> aNodeMFT;

	CComPtrCustom<IMFTopologyNode> aNodeRender;

	do
	{
		WaterMarkInjectorMFT *lMFT = new WaterMarkInjectorMFT();

		lhresult = MFCreateTopologyNode(MF_TOPOLOGY_TRANSFORM_NODE, &aNodeMFT);

		if (FAILED(lhresult))
			break;

		lhresult = aNodeMFT->SetObject(lMFT);

		if (FAILED(lhresult))
			break;

		lhresult = MFCreateVideoRendererActivate(aHWNDVideo, &lRendererActivate);
		
		if (FAILED(lhresult))
			break;
		
		lhresult = MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE, &aNodeRender);

		if (FAILED(lhresult))
			break;

		lhresult = aNodeRender->SetObject(lRendererActivate);

		if (FAILED(lhresult))
			break;


		lhresult = MediaFoundation::getInstance().connectOutputNode(0, aNodeMFT, aNodeRender);

		if (FAILED(lhresult))
			break;

		TopologyNode lMFTTopologyNode;

		lMFTTopologyNode.mPtrIMFTopologyNode = aNodeMFT.Detach();

		lMFTTopologyNode.mConnectedTopologyNode = true;

		aNodesVector.push_back(lMFTTopologyNode);

		TopologyNode lRenderTopologyNode;

		lRenderTopologyNode.mPtrIMFTopologyNode = aNodeRender.Detach();

		lRenderTopologyNode.mConnectedTopologyNode = false;

		aNodesVector.push_back(lRenderTopologyNode);


	} while (false);
	
	return lhresult;
}

在此方法中,使用 `MFCreateVideoRendererActivate` 函数的参数 `HWND aHWNDVideo` 来获取渲染激活。渲染激活被设置为渲染节点 - `aNodeRender`。此外,在方法中创建了一个 Media Foundation 转换类 `WaterMarkInjectorMFT`,该类将水印注入实时视频。转换类被设置在转换节点 `aNodeMFT` 中。这些节点通过 `connectOutputNode` 方法连接,并推送到节点向量 `aNodesVector`。需要注意的是,`TopologyNode` 结构体有一个变量 `mConnectedTopologyNode`,它是一个标志,表示节点是否与 videoInput 的 `TeeNode` 连接。在此代码中,节点 `aNodeRender` 未与 TeeNode 连接,因为它已与 `aNodeMFT` 节点连接——变量 `mConnectedTopologyNode` 设置为 `false`。但是,`aNodeMFT` 未连接,并且必须与 videoInput 的 TeeNode 连接——变量 `mConnectedTopologyNode` 被设置为 `true`。结果,就可以编写自己的代码来使用 Media Foundation 框架并轻松地将其注入 videoInput 库。

最后,需要强调 `setupDevice` 方法中的最后一个参数 `bool aIsSampleGrabberEnable = true`。此参数是一个标志,用于启用内部采样捕获接收器与拓扑的 `TeeNode` 的内部连接。默认情况下是启用的,但我认为有些情况项目不需要处理原始图像数据,连接这样的接收器可能会浪费资源——因此,可以禁用它。事实上,许多原始图像操作可以替换为自定义的 Media Foundation 转换,并且此类解决方案将更灵活、更有效。

 

将水平镜像转换包含到采样捕获拓扑中。

这是一个简单的代码更新,用于解决某些网络摄像头上的底部向上捕获问题。事实是,某些网络摄像头的图像写入输出流的模式与 Windows 的默认模式不同——这会导致图像水平翻转。可以通过手动检查 MF_MT_DEFAULT_STRIDE 属性来检测此问题并由开发人员纠正。然而,我决定编写一个特定的转换并将其包含在视频捕获拓扑中。新代码可以在以下链接找到:videoInputVS2013Express_for_Desktop_bottom_up_detection_version.zip

 

 

 

关注点

我在 TDD 策略的基础上开发了这个项目。我认为这将帮助其他开发人员解决与此代码相关的某些问题。

历史

该项目位于 CodeProject 的 Git 仓库:videoInput

本文基于 旧文章

2014年12月6日 - 更新 Visual Studio 2013 Express for Desktop 项目。添加新的 OpenGL 可视化测试示例。

2014年12月11日 - 更新 C++/CLI、C#、WPF 项目

2015年4月10日 - 更新 C# 封送 C 包装器项目

2015年4月13日 - 更新版本以支持 x64 Windows 平台并改进多线程同步。

2015年5月11日 - 更新版本以支持多接收器输出。

2015年5月25日 - 更新版本以修复底部向上失真。
© . All rights reserved.