问题描述
我正在开发一个 Windows 应用程序,该应用程序能够显示高质量的视频源、录制视频或从中拍摄照片,并在以后进行编辑(最高可达 4K,在不久的将来可能会达到 8K)。 我目前有一个工作产品,使用 WPF (C#)。为了捕获和显示视频,我使用了 AForge.NET 库。
我的问题是应用程序真的很慢,主要的性能影响来自视频渲染。显然,唯一的方法是从 AForge 库中进行回调,每次可用时提供一个新框架。然后将该框架作为图像放置在 Image
元素内。我相信您可以看到性能下降的来源,尤其是高分辨率图像。
我使用 WPF 和这些庞大的库的经验让我重新思考了我想如何编程;我不想制作因为速度慢而占用每个人时间的糟糕软件(我参考手工制作网络了解更多“为什么?”。
问题是,在 WPF C# 中,相机捕获和显示是地狱,但我似乎没有比其他任何地方更好(在 Windows 上)。我的一个选择是主要使用 C++ 和 DirectShow。这是一个不错的解决方案,但在性能方面感觉已经过时,并且建立在 Microsoft 的 COM 系统上,我更愿意避免使用它。可以选择使用 Direct3D 使用硬件进行渲染,但 DirectShow 和 Direct3D 不能很好地配合使用。
我研究了其他应用程序是如何实现这一点的。 VLC 使用 DirectShow,但这只能说明 DirectShow 存在较大的延迟。我认为这是因为 VLC 并非用于实时目的。 OBS 工作室使用 QT 使用的任何东西,但我无法找到他们是如何做到的。 OpenCV 抓取帧并将它们传送到屏幕上,根本没有效率,但这对于 OpenCV 观众来说已经足够了。 最后,来自 Windows 的集成网络摄像头应用程序。 出于某种原因,这个应用程序能够实时记录和回放,而不会对性能造成很大的影响。我无法弄清楚他们是如何做到这一点的,我也没有找到任何其他解决方案可以实现与该工具相当的结果。
TLDR; 所以我的问题是:我将如何有效地捕获和渲染相机流,最好是硬件加速;是否可以在不通过 Directshow 的情况下在 Windows 上执行此操作?最后,当我希望它们实时处理 4K 素材时,我是否会要求很多商品设备?
我没有发现任何人以足以满足我需求的方式这样做;这让我同时感到绝望和内疚。我宁愿不要因为这个问题打扰 StackOverflow。
非常感谢您提供有关此主题的一般性回答或建议。
解决方法
这是一个完整的可重现示例代码,它使用 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,&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);
}
}
}
}
}
}
}
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",WS_OVERLAPPEDWINDOW,videoWidth,hInstance,NULL);
ShowWindow(hWnd,nCmdShow);
std::thread th(BackgroundRecording,hWnd,pReader,videoHeight);
th.detach();
MSG msg = { };
while (GetMessage(&msg,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 作为当前技术。
在我看来,您问题中最重要的部分是如何组织视频渲染。在性能方面,利用硬件加速至关重要,在这种情况下,您的应用程序所构建的技术以及视频演示/嵌入的可用集成选项很重要。对于 .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 本身而不是 DirectShow 或任何捕获库。这似乎运行良好,并且可以轻松访问音频和视频组合。
回调收集视频帧并将它们写入 D3D9 纹理,该纹理又用作像素着色器(= 片段着色器)的输入,并渲染为矩形。这样做的原因是为了能够从相机的原生 NV12 格式(其他格式也可以)进行格式转换。
如果有人对如何更详细地完成此操作感兴趣,请随时在此答案的评论中提问。这可以节省很多时间:)
TL;博士: WPF 仅允许 D3D9 内容,我使用 MediaFoundation 进行捕获。