You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Media Foundation与Direct3D9的C++录屏程序改造需求:实现特定应用(Microsoft Word)的实时视频帧捕获

实现特定窗口的实时视频帧捕获(基于Media Foundation与Direct3D9)

当然可以实现仅捕获特定应用窗口的实时视频帧!你的现有代码已经基于Media Foundation和Direct3D9搭建了完整的桌面捕获框架,只需要针对窗口捕获做几个关键修改即可。下面我会提供完整的修改后代码,并拆解核心改动点。

完整修改后代码

// Required Components
#pragma comment(lib, "mfreadwrite")
#pragma comment(lib, "mfplat")
#pragma comment(lib, "mfuuid")
#pragma comment(lib, "d3d9.lib")
#include <iostream>
#include <Windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <Mfreadwrite.h>
#include <mferror.h>
#include <d3d9.h>
#include <string>
#include <algorithm>

template<class T>
void SafeRelease(T **ppT)
{
    if (*ppT)
    {
        (*ppT)->Release();
        *ppT = NULL;
    }
}

#define REVERSE_IMAGE

// Format constants
const UINT32 VIDEO_FPS = 30;
const UINT64 VIDEO_FRAME_DURATION = 10 * 1000 * 1000 / VIDEO_FPS;
const UINT32 VIDEO_BIT_RATE = 800000000;
const GUID VIDEO_ENCODING_FORMAT = MFVideoFormat_H264;
const GUID VIDEO_INPUT_FORMAT = MFVideoFormat_RGB32;
const UINT32 VIDEO_FRAME_COUNT = 10 * VIDEO_FPS;

// 获取目标窗口句柄(可根据窗口标题、进程名等调整)
HWND GetTargetWindowHandle(const std::wstring& windowTitlePart)
{
    return FindWindowW(NULL, windowTitlePart.c_str());
    // 如果需要模糊匹配窗口标题,可使用EnumWindows遍历所有窗口进行匹配
    // 示例:匹配包含"Word"的窗口
    /*
    HWND targetHwnd = NULL;
    EnumWindows([](HWND hwnd, LPARAM lParam) -> BOOL {
        wchar_t title[256];
        GetWindowTextW(hwnd, title, sizeof(title)/sizeof(wchar_t));
        std::wstring windowTitle(title);
        std::wstring* targetPart = reinterpret_cast<std::wstring*>(lParam);
        if (windowTitle.find(*targetPart) != std::wstring::npos && IsWindowVisible(hwnd)) {
            *reinterpret_cast<HWND*>(lParam) = hwnd;
            return FALSE;
        }
        return TRUE;
    }, reinterpret_cast<LPARAM>(&targetHwnd));
    return targetHwnd;
    */
}

HRESULT InitializeDirect3D9(IDirect3DDevice9 **ppDevice, IDirect3DSurface9 **ppSurface, UINT32 &uiWidth, UINT32 &uiHeight, HWND targetHwnd)
{
    if (!targetHwnd)
        return E_INVALIDARG;

    // 获取目标窗口的尺寸(包含边框)
    RECT windowRect;
    GetWindowRect(targetHwnd, &windowRect);
    uiWidth = windowRect.right - windowRect.left;
    uiHeight = windowRect.bottom - windowRect.top;

    IDirect3D9 *d3d = NULL;
    d3d = Direct3DCreate9(D3D_SDK_VERSION);
    if (d3d == NULL)
        return E_POINTER;

    D3DPRESENT_PARAMETERS parameters = {0};
    parameters.Windowed = TRUE;
    parameters.BackBufferCount = 1;
    parameters.BackBufferHeight = uiHeight;
    parameters.BackBufferWidth = uiWidth;
    parameters.SwapEffect = D3DSWAPEFFECT_DISCARD;
    parameters.hDeviceWindow = NULL;

    HRESULT hr = d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, NULL, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &parameters, ppDevice);
    if (FAILED(hr))
    {
        SafeRelease(&d3d);
        return hr;
    }

    // 创建与窗口尺寸匹配的Offscreen Surface
    hr = (*ppDevice)->CreateOffscreenPlainSurface(uiWidth, uiHeight, D3DFMT_A8R8G8B8, D3DPOOL_SYSTEMMEM, ppSurface, nullptr);
    SafeRelease(&d3d);
    return hr;
}

HRESULT InitializeSinkWriter(IMFSinkWriter **ppWriter, DWORD *pStreamIndex, const UINT32 uiWidth, const UINT32 uiHeight)
{
    *ppWriter = NULL;
    *pStreamIndex = NULL;

    IMFSinkWriter *pSinkWriter = NULL;
    IMFMediaType *pMediaTypeOut = NULL;
    IMFMediaType *pMediaTypeIn = NULL;
    DWORD streamIndex;

    HRESULT hr = MFCreateSinkWriterFromURL(L"output.mp4", NULL, NULL, &pSinkWriter);

    // Set the output media type.
    if (SUCCEEDED(hr))
    {
        hr = MFCreateMediaType(&pMediaTypeOut);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetGUID(MF_MT_SUBTYPE, VIDEO_ENCODING_FORMAT);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetUINT32(MF_MT_AVG_BITRATE, VIDEO_BIT_RATE);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeOut->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
    }
    if (SUCCEEDED(hr))
    {
        hr = MFSetAttributeSize(pMediaTypeOut, MF_MT_FRAME_SIZE, uiWidth, uiHeight);
    }
    if (SUCCEEDED(hr))
    {
        hr = MFSetAttributeRatio(pMediaTypeOut, MF_MT_FRAME_RATE, VIDEO_FPS, 1);
    }
    if (SUCCEEDED(hr))
    {
        hr = MFSetAttributeRatio(pMediaTypeOut, MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
    }
    if (SUCCEEDED(hr))
    {
        hr = pSinkWriter->AddStream(pMediaTypeOut, &streamIndex);
    }

    // Set the input media type.
    if (SUCCEEDED(hr))
    {
        hr = MFCreateMediaType(&pMediaTypeIn);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeIn->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeIn->SetGUID(MF_MT_SUBTYPE, VIDEO_INPUT_FORMAT);
    }
    if (SUCCEEDED(hr))
    {
        hr = pMediaTypeIn->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
    }
    if (SUCCEEDED(hr))
    {
        hr = MFSetAttributeSize(pMediaTypeIn, MF_MT_FRAME_SIZE, uiWidth, uiHeight);
    }
    if (SUCCEEDED(hr))
    {
        hr = MFSetAttributeRatio(pMediaTypeIn, MF_MT_FRAME_RATE, VIDEO_FPS, 1);
    }
    if (SUCCEEDED(hr))
    {
        hr = MFSetAttributeRatio(pMediaTypeIn, MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
    }
    if (SUCCEEDED(hr))
    {
        hr = pSinkWriter->SetInputMediaType(streamIndex, pMediaTypeIn, NULL);
    }

    // Tell the sink writer to start accepting data.
    if (SUCCEEDED(hr))
    {
        hr = pSinkWriter->BeginWriting();
    }

    // Return the pointer to the caller.
    if (SUCCEEDED(hr))
    {
        *ppWriter = pSinkWriter;
        (*ppWriter)->AddRef();
        *pStreamIndex = streamIndex;
    }

    SafeRelease(&pSinkWriter);
    SafeRelease(&pMediaTypeOut);
    SafeRelease(&pMediaTypeIn);
    return hr;
}

HRESULT WriteFrame(IDirect3DDevice9 *pDevice, IDirect3DSurface9 *pSurface, IMFSinkWriter *pWriter, DWORD streamIndex, const LONGLONG &rtStart, const UINT32 uiWidth, const UINT32 uiHeight, HWND targetHwnd)
{
    if (!targetHwnd)
        return E_INVALIDARG;

    // 获取目标窗口的DC
    HDC hWndDC = GetDC(targetHwnd);
    if (!hWndDC)
        return E_HANDLE;

    // 创建兼容DC
    HDC hMemDC = CreateCompatibleDC(hWndDC);
    if (!hMemDC)
    {
        ReleaseDC(targetHwnd, hWndDC);
        return E_HANDLE;
    }

    // 创建兼容位图
    HBITMAP hBitmap = CreateCompatibleBitmap(hWndDC, uiWidth, uiHeight);
    if (!hBitmap)
    {
        DeleteDC(hMemDC);
        ReleaseDC(targetHwnd, hWndDC);
        return E_HANDLE;
    }

    // 选入位图到兼容DC
    HGDIOBJ oldBitmap = SelectObject(hMemDC, hBitmap);

    // 捕获窗口图像到兼容DC
    BitBlt(hMemDC, 0, 0, uiWidth, uiHeight, hWndDC, 0, 0, SRCCOPY);

    // 将位图数据转换为D3D Surface
    D3DLOCKED_RECT lockedRect;
    HRESULT hr = pSurface->LockRect(&lockedRect, NULL, 0);
    if (SUCCEEDED(hr))
    {
        BITMAPINFO bmi = {0};
        bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
        bmi.bmiHeader.biWidth = uiWidth;
        bmi.bmiHeader.biHeight = -uiHeight; // 负高度表示从上到下的像素顺序
        bmi.bmiHeader.biPlanes = 1;
        bmi.bmiHeader.biBitCount = 32;
        bmi.bmiHeader.biCompression = BI_RGB;

        GetDIBits(hMemDC, hBitmap, 0, uiHeight, lockedRect.pBits, &bmi, DIB_RGB_COLORS);
        pSurface->UnlockRect();
    }

    // 清理GDI资源
    SelectObject(hMemDC, oldBitmap);
    DeleteObject(hBitmap);
    DeleteDC(hMemDC);
    ReleaseDC(targetHwnd, hWndDC);

    if (FAILED(hr))
    {
        return hr;
    }

    D3DLOCKED_RECT rc;
    hr = pSurface->LockRect(&rc, NULL, 0);
    if (FAILED(hr))
    {
        return hr;
    }

    IMFSample *pSample = NULL;
    IMFMediaBuffer *pBuffer = NULL;

    const LONG cbWidth = 4 * uiWidth;
    const DWORD cbBuffer = cbWidth * uiHeight;
    BYTE *pData = NULL;

    // Create a new memory buffer.
    hr = MFCreateMemoryBuffer(cbBuffer, &pBuffer);

    // Lock the buffer and copy the video frame to the buffer.
    if (SUCCEEDED(hr))
    {
        hr = pBuffer->Lock(&pData, NULL, NULL);
    }
    if (SUCCEEDED(hr))
    {
#ifdef REVERSE_IMAGE
        for (int i = 0, j = uiHeight - 1; i < uiHeight; i++, j--)
            for (int k = 0; k < cbWidth; k++)
                pData[(i * cbWidth) + k] = ((BYTE *) rc.pBits)[(j * cbWidth) + k];
#else
        hr = MFCopyImage(pData, cbWidth, (BYTE*)rc.pBits, rc.Pitch, cbWidth, uiHeight);
#endif
    }
    if (pBuffer)
    {
        pBuffer->Unlock();
    }

    // Set the data length of the buffer.
    if (SUCCEEDED(hr))
    {
        hr = pBuffer->SetCurrentLength(cbBuffer);
    }

    // Create a media sample and add the buffer to the sample.
    if (SUCCEEDED(hr))
    {
        hr = MFCreateSample(&pSample);
    }
    if (SUCCEEDED(hr))
    {
        hr = pSample->AddBuffer(pBuffer);
    }

    // Set the time stamp and the duration.
    if (SUCCEEDED(hr))
    {
        hr = pSample->SetSampleTime(rtStart);
    }
    if (SUCCEEDED(hr))
    {
        hr = pSample->SetSampleDuration(VIDEO_FRAME_DURATION);
    }

    // Send the sample to the Sink Writer.
    if (SUCCEEDED(hr))
    {
        hr = pWriter->WriteSample(streamIndex, pSample);
    }

    hr = pSurface->UnlockRect();

    SafeRelease(&pSample);
    SafeRelease(&pBuffer);
    return hr;
}

void start_engine(){
    HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
    if (SUCCEEDED(hr))
    {
        hr = MFStartup(MF_VERSION);
        if (SUCCEEDED(hr))
        {
            // 获取目标窗口句柄,这里替换为你的Word窗口标题
            HWND targetHwnd = GetTargetWindowHandle(L"Document1 - Microsoft Word");
            if (!targetHwnd)
            {
                std::cout << "Failed to find target window!" << std::endl;
                MFShutdown();
                CoUninitialize();
                return;
            }

            UINT32 uiWidth = 0;
            UINT32 uiHeight = 0;
            IDirect3DDevice9 *pDevice = NULL;
            IDirect3DSurface9 *pSurface = NULL;
            hr = InitializeDirect3D9(&pDevice, &pSurface, uiWidth, uiHeight, targetHwnd);
            if (SUCCEEDED(hr))
            {
                IMFSinkWriter *pSinkWriter = NULL;
                DWORD stream;
                hr = InitializeSinkWriter(&pSinkWriter, &stream, uiWidth, uiHeight);
                if (SUCCEEDED(hr))
                {
                    LONGLONG rtStart = 0;
                    for (DWORD i = 0; i < VIDEO_FRAME_COUNT; ++i)
                    {
                        hr = WriteFrame(pDevice, pSurface, pSinkWriter, stream, rtStart, uiWidth, uiHeight, targetHwnd);
                        if (FAILED(hr))
                        {
                            break;
                        }
                        rtStart += VIDEO_FRAME_DURATION;
                        // 添加小延迟,避免CPU占用过高
                        Sleep(1000/VIDEO_FPS - 1);
                    }
                }
                if (SUCCEEDED(hr))
                {
                    hr = pSinkWriter->Finalize();
                }
                SafeRelease(&pSinkWriter);
            }
            SafeRelease(&pDevice);
            SafeRelease(&pSurface);
            MFShutdown();
        }
        CoUninitialize();
    }
}

int main()
{
    std::cout << "Window Capturing has started..." << std::endl;
    start_engine();
    std::cout << "Check: 'output.mp4' in the same directory." << std::endl;
    return 0;
}

关键改动说明

1. 新增窗口定位逻辑

  • 添加了GetTargetWindowHandle函数,支持通过精确窗口标题定位目标窗口
  • 注释中提供了模糊匹配的方案(比如匹配包含"Word"的窗口),适合窗口标题动态变化的场景(比如Word会显示当前文档名)

2. 修改Direct3D初始化流程

  • 不再依赖显示器分辨率,而是通过GetWindowRect获取目标窗口的实际尺寸
  • 创建与窗口尺寸完全匹配的Offscreen Surface,确保捕获的画面没有黑边或拉伸

3. 替换帧捕获实现

  • 使用GDI的BitBlt函数直接捕获目标窗口的图像数据,替代原有的全屏GetFrontBufferData
  • 将GDI位图转换为Direct3D Surface,确保后续编码流程兼容原有逻辑

4. 优化捕获性能

  • 在帧循环中添加了Sleep延迟,避免CPU占用过高,同时保证帧率稳定

使用注意事项

  • 确保目标窗口处于可见状态(最小化窗口无法捕获)
  • 如果需要捕获窗口的客户区(不含边框和标题栏),可以将GetWindowRect替换为GetClientRect,并配合ClientToScreen转换坐标
  • 若遇到窗口权限问题(比如UAC提升的程序),需要将你的捕获程序也以管理员权限运行

内容的提问来源于stack exchange,提问作者Muh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 21:13:14