在 Windows 上捕获和显示实时摄像机内容

Capturing and displaying live camera content on Windows

我正在开发一个 Windows 应用程序,它能够显示高质量的视频源,录制它或从中拍照,并在以后编辑它们(最高 4K,在不久的将来可能是 8K ). 我目前有一个工作产品,使用 WPF (C#)。为了捕获和显示视频,我使用了 AForge.NET 库。

我的问题是应用程序真的很慢,主要的性能损失来自视频渲染。显然,做到这一点的唯一方法是从 AForge 库中进行回调,在每次可用时提供一个新框架。然后将该帧作为图像放置在 Image 元素内。我相信您可以看到性能下降的原因,尤其是对于高分辨率图像。

我使用 WPF 和这些庞大的库的经验让我重新思考我想要如何进行总体编程;我不想制作糟糕的软件,因为速度慢而占用大家的时间(我参考 Handmade 网络 了解更多关于“为什么?”的信息。

问题是,相机捕捉和显示在 WPF C# 中非常糟糕,但我似乎没有比其他任何地方更好的地方(在 Windows 上)。我可以选择主要使用 C++ 和 DirectShow。这是一个不错的解决方案,但在性能方面感觉已经过时,并且是建立在微软的 COM 系统之上的,我宁愿避免这种情况。有使用 Direct3D 使用硬件进行渲染的选项,但 DirectShow 和 Direct3D 不能很好地协同工作。

我研究了其他应用程序是如何实现这一点的。 VLC 使用 DirectShow,但这只能说明 DirectShow 存在较大延迟。我认为这是因为 VLC 并非用于实时目的。 OBS studio 使用 QT 使用的任何东西,但我无法找到他们是如何做到的。 OpenCV 抓取帧并将它们 blits 到屏幕上,效率不高,但这对 OpenCV 观众来说已经足够了。 最后,来自 Windows 的集成网络摄像头应用程序。 出于某种原因,该应用程序能够实时录制和回放,而不会对性能造成太大影响。我无法弄清楚他们是如何做到这一点的,也没有找到任何其他解决方案可以达到与该工具相当的结果。

TLDR; 所以我的问题是:我将如何有效地捕获和渲染相机流,最好是硬件加速;是否可以在 Windows 上执行此操作而无需通过 Directshow;最后,当我希望它们实时处理 4K 素材时,我是否要求很多商品设备?

我还没有发现任何人以满足我需要的方式来做这件事;这让我同时感到绝望和内疚。我宁愿不为这个问题打扰 Whosebug。

非常感谢您对此主题的一般性回答或建议。

这是一个完整的可重现示例代码,它使用 GDI+ 进行渲染并使用 MediaFoundation 捕获视频。它应该在 visual studio 上开箱即用,并且不应由于使用 unique_ptr 和 CComPtr 进行自动内存管理而出现任何类型的内存泄漏。此外,您的相机将使用此代码输出其默认视频格式。如果需要,您始终可以使用以下设置视频格式:https://docs.microsoft.com/en-us/windows/win32/medfound/how-to-set-the-video-capture-format

#include <windows.h>
#include <mfapi.h>
#include <iostream>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <dshow.h>
#include <dvdmedia.h>
#include <gdiplus.h>
#include <atlbase.h>
#include <thread>
#include <vector>

#pragma comment(lib, "mfplat")
#pragma comment(lib, "mf")
#pragma comment(lib, "mfreadwrite")
#pragma comment(lib, "mfuuid")
#pragma comment(lib, "gdiplus")

void BackgroundRecording(HWND hWnd, CComPtr<IMFSourceReader> pReader, int videoWidth, int videoHeight) {
    DWORD streamIndex, flags;
    LONGLONG llTimeStamp;

    Gdiplus::PixelFormat pixelFormat = PixelFormat24bppRGB;
    Gdiplus::Graphics* g = Gdiplus::Graphics::FromHWND(hWnd, FALSE);

    while (true) {
        CComPtr<IMFSample> pSample;

        HRESULT hr = pReader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &streamIndex, &flags, &llTimeStamp, &pSample);
        if (!FAILED(hr)) {
            if (pSample != NULL) {
                CComPtr<IMFMediaBuffer> pBuffer;
                hr = pSample->ConvertToContiguousBuffer(&pBuffer);
                if (!FAILED(hr)) {
                    DWORD length;
                    hr = pBuffer->GetCurrentLength(&length);
                    if (!FAILED(hr)) {
                        unsigned char* data;
                        hr = pBuffer->Lock(&data, NULL, &length);
                        if (!FAILED(hr)) {
                            std::unique_ptr<unsigned char[]> reversedData(new unsigned char[length]);
                            int counter = length - 1;
                            for (int i = 0; i < length; i += 3) {
                                reversedData[i] = data[counter - 2];
                                reversedData[i + 1] = data[counter - 1];
                                reversedData[i + 2] = data[counter];
                                counter -= 3;
                            }
                            std::unique_ptr<Gdiplus::Bitmap> bitmap(new Gdiplus::Bitmap(videoWidth, videoHeight, 3 * videoWidth, pixelFormat, reversedData.get()));
                            g->DrawImage(bitmap.get(), 0, 0);
                        }
                    }
                }
            }
        }
    }
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);
        FillRect(hdc, &ps.rcPaint, (HBRUSH)(COLOR_WINDOW + 1));
        EndPaint(hwnd, &ps);
    }
    break;
    case WM_CLOSE:
    {
        DestroyWindow(hwnd);
    }
    break;
    case WM_DESTROY:
    {
        PostQuitMessage(0);
    }
    break;
    default:
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
        break;
    }
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) {
    HRESULT hr = MFStartup(MF_VERSION);

    Gdiplus::GdiplusStartupInput gdiplusStartupInput;
    ULONG_PTR gdiplusToken;
    GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

    CComPtr<IMFSourceReader> pReader = NULL;
    CComPtr<IMFMediaSource> pSource = NULL;
    CComPtr<IMFAttributes> pConfig = NULL;
    IMFActivate** ppDevices = NULL;

    hr = MFCreateAttributes(&pConfig, 1);
    if (FAILED(hr)) {
        std::cout << "Failed to create attribute store" << std::endl;
    }

    hr = pConfig->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
    if (FAILED(hr)) {
        std::cout << "Failed to request capture devices" << std::endl;
    }

    UINT32 count = 0;
    hr = MFEnumDeviceSources(pConfig, &ppDevices, &count);
    if (FAILED(hr)) {
        std::cout << "Failed to enumerate capture devices" << std::endl;
    }

    hr = ppDevices[0]->ActivateObject(IID_PPV_ARGS(&pSource));
    if (FAILED(hr)) {
        std::cout << "Failed to connect camera to source" << std::endl;
    }

    hr = MFCreateSourceReaderFromMediaSource(pSource, pConfig, &pReader);
    if (FAILED(hr)) {
        std::cout << "Failed to create source reader" << std::endl;
    }

    for (unsigned int i = 0; i < count; i++) {
        ppDevices[i]->Release();
    }
    CoTaskMemFree(ppDevices);

    CComPtr<IMFMediaType> pType = NULL;
    DWORD dwMediaTypeIndex = 0;
    DWORD dwStreamIndex = 0;
    hr = pReader->GetNativeMediaType(dwStreamIndex, dwMediaTypeIndex, &pType);
    LPVOID representation;
    pType->GetRepresentation(AM_MEDIA_TYPE_REPRESENTATION, &representation);
    GUID subType = ((AM_MEDIA_TYPE*)representation)->subtype;
    BYTE* pbFormat = ((AM_MEDIA_TYPE*)representation)->pbFormat;
    GUID formatType = ((AM_MEDIA_TYPE*)representation)->formattype;
    int videoWidth = ((VIDEOINFOHEADER2*)pbFormat)->bmiHeader.biWidth;
    int videoHeight = ((VIDEOINFOHEADER2*)pbFormat)->bmiHeader.biHeight;

    WNDCLASS wc = { };
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = L"Window";
    RegisterClass(&wc);
    HWND hWnd = CreateWindowExW(NULL, L"Window", L"Window", WS_OVERLAPPEDWINDOW, 0, 0, videoWidth, videoHeight, NULL, NULL, hInstance, NULL);
    ShowWindow(hWnd, nCmdShow);

    std::thread th(BackgroundRecording, hWnd, pReader, videoWidth, videoHeight);
    th.detach();

    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    pSource->Shutdown();
    Gdiplus::GdiplusShutdown(gdiplusToken);
    return 0;
}

您的问题是关于几种技术的组合:视频捕获、视频演示以及如何将两者连接在一起。

在 Windows 上有两个与视频相关的 API(如果我们不考虑古老的 VfW):DirectShow 和 Media Foundation。 API 都有基础层,这些层大部分是共享的,因此 DirectShow 和 Media Foundation 都提供相似的视频捕获功能和性能。两个 API 都为您提供良好的视频捕获延迟,相当低。就目前的情况而言,不推荐使用 DirectShow,因为 API 已经过时了,而且大部分都是废弃的技术。同时,您可能会发现 DirectShow 的文档更好、用途更广,并且提供了数量级更好的补充材料和第三方软件项目。您提到了一些库,它们都建立在上述技术之一(VfW、DirectShow、Media Foundation)之上,实现质量不如原始操作系统API。

实际上,您可以使用这两种技术中的任何一种来捕获视频,最好使用 Media Foundation 作为当前技术。

在我看来,您问题中最重要的部分是如何组织视频渲染。在性能方面,利用硬件加速是必不可少的,在这种情况下,您的应用程序基于什么技术构建以及视频可用的集成选项是什么很重要 presentation/embedding。对于 .NET 桌面应用程序,您可能会对将 Direct3D 11/12 与 .NET 混合或使用 MediaPlayerElement 控件感兴趣,并研究如何将视频帧注入其中。如上所述,即使第三方库可用,您也不应期望它们以适当的方式解决问题。您有兴趣至少了解视频管道中的数据流。

那么您遇到了如何连接视频捕获(不是视频硬件加速)和硬件加速视频渲染的问题。这里可以有多种解决方案,但重要的是 DirectShow 对硬件加速的支持是有限的,并且在 Direct3D 9 停止了它的发展,这在今天听起来已经过时了。这是告别这项 - 毫无疑问 - 一项出色技术的另一个原因。您有兴趣尽快研究将捕获的视频内容放入 Direct3D 11/Direct3D 12/Direct2D 的选项,并利用标准的当前技术进行以下处理。实际技术可能取决于:它可以是 Media Foundation、Direct3D 11/12 或提到的 MediaPlayerElement 控件,以及一些其他选项,如 Direct2D,它们也不错。在获得出色或至少合理的性能的过程中,您有兴趣尽量减少对第三方库的使用——尽管它们很流行——即使它们的标题中有流行语。

4K实时画面可以实时采集和处理,但是一般要么你有专业的视频采集硬件,要么内容是压缩的,你应该用硬件加速解压。

我发现似乎是我目前拥有的软件的唯一解决方案。

为了提高性能,我当然需要硬件加速,但问题是没有太多选项与 WPF 兼容。我发现 Direct3D9 可以完成这项工作,尽管它已经过时了。可以在 D3D11 或更高版本中完成所有操作并与 D3D9 表面共享结果,但我选择一直使用 D3D9。

为了捕获自身,我现在使用 MediaFoundation iself 而不是 DirectShow 或任何捕获库。这似乎运作良好,并且可以轻松访问音频和视频组合。

回调收集视频帧并将它们写入 D3D9 纹理,该纹理又用作像素着色器(= 片段着色器)的输入,并渲染为矩形。这样做的原因是为了能够从相机的本机 NV12 格式(其他格式也可以)进行格式转换。

如果有人对如何更详细地完成此操作感兴趣,请随时在对此答案的评论中提问。那可以节省很多时间:)

长话短说;博士: WPF 只允许 D3D9 内容,我使用 MediaFoundation 进行捕获。