ScreenCapture:支持 HDR 的单头文件 DirectX 库





5.00/5 (18投票s)
支持音频混合的 DirectX 硬件屏幕捕获和编码。支持 H264/H265/VP80/VP90/FLAC/MP3。支持 HDR。
引言
有很多关于它的资料。这是一个简单的、单头的、硬件加速的库。如果您使用的是 Windows 8 或更高版本,您可以轻松地将其包含在您的项目中。
要求
- Windows 8 或更高版本
视频捕获
我们需要借助 DXGI 来枚举我们的适配器和监视器的数量。
static void GetAdapters(std::vector<CComPtr<IDXGIAdapter1>>& a)
{
    CComPtr<IDXGIFactory1> df;
    CreateDXGIFactory1(__uuidof(IDXGIFactory1),(void**)&df);
    a.clear();
    if (!df)
        return;
    int L = 0;
    for (;;)
    {
        CComPtr<IDXGIAdapter1> lDxgiAdapter;
        df->EnumAdapters1(L, &lDxgiAdapter);
        if (!lDxgiAdapter)
            break;
        L++;
        a.push_back(lDxgiAdapter);
    }
}
然后,我们将使用其中一个或默认的 DirectX 11 设备来实例化一个 DirectX 11 设备。
HRESULT CreateDirect3DDevice(IDXGIAdapter1* g)
{
    HRESULT hr = S_OK;
    // Driver types supported
    D3D_DRIVER_TYPE DriverTypes[] =
    {
        D3D_DRIVER_TYPE_HARDWARE,
        D3D_DRIVER_TYPE_WARP,
        D3D_DRIVER_TYPE_REFERENCE,
    };
    UINT NumDriverTypes = ARRAYSIZE(DriverTypes);
    // Feature levels supported
    D3D_FEATURE_LEVEL FeatureLevels[] =
    {
        D3D_FEATURE_LEVEL_11_0,
        D3D_FEATURE_LEVEL_10_1,
        D3D_FEATURE_LEVEL_10_0,
        D3D_FEATURE_LEVEL_9_3,
        D3D_FEATURE_LEVEL_9_2,
        D3D_FEATURE_LEVEL_9_1
    };
    UINT NumFeatureLevels = ARRAYSIZE(FeatureLevels);
    D3D_FEATURE_LEVEL FeatureLevel;
    // Create device
    for (UINT DriverTypeIndex = 0; DriverTypeIndex < NumDriverTypes; ++DriverTypeIndex)
    {
        hr = D3D11CreateDevice(g, DriverTypes[DriverTypeIndex], 
             nullptr, D3D11_CREATE_DEVICE_VIDEO_SUPPORT, FeatureLevels, NumFeatureLevels,
             D3D11_SDK_VERSION, &device, &FeatureLevel, &context);
        if (SUCCEEDED(hr))
        {
            // Device creation success, no need to loop anymore
            break;
        }
    }
    if (FAILED(hr))
        return hr;
    return S_OK;
}
然后,我们想创建输出的桌面复制。
bool Prepare(UINT Output = 0)
{
// Get DXGI device
    CComPtr<IDXGIDevice> lDxgiDevice;
    lDxgiDevice = device;
    if (!lDxgiDevice)
        return 0;
    // Get DXGI adapter
    CComPtr<IDXGIAdapter> lDxgiAdapter;
    auto hr = lDxgiDevice->GetParent(
        __uuidof(IDXGIAdapter),
        reinterpret_cast<void**>(&lDxgiAdapter));
    if (FAILED(hr))
        return 0;
    lDxgiDevice = 0;
    // Get output
    CComPtr<IDXGIOutput> lDxgiOutput;
    hr = lDxgiAdapter->EnumOutputs(Output, &lDxgiOutput);
    if (FAILED(hr))
        return 0;
    lDxgiAdapter = 0;
    DXGI_OUTPUT_DESC lOutputDesc;
    hr = lDxgiOutput->GetDesc(&lOutputDesc);
    // QI for Output 1
    CComPtr<IDXGIOutput1> lDxgiOutput1;
    lDxgiOutput1 = lDxgiOutput;
    if (!lDxgiOutput1)
        return 0;
    lDxgiOutput = 0;
    // Create desktop duplication
    hr = lDxgiOutput1->DuplicateOutput(
        device,
        &lDeskDupl);
    if (FAILED(hr))
        return 0;
    lDxgiOutput1 = 0;
    // Create GUI drawing texture
    lDeskDupl->GetDesc(&lOutputDuplDesc);
    D3D11_TEXTURE2D_DESC desc = {};
    desc.Width = lOutputDuplDesc.ModeDesc.Width;
    desc.Height = lOutputDuplDesc.ModeDesc.Height;
    desc.Format = lOutputDuplDesc.ModeDesc.Format;
    desc.ArraySize = 1;
    desc.BindFlags = D3D11_BIND_FLAG::D3D11_BIND_RENDER_TARGET;
    desc.MiscFlags = D3D11_RESOURCE_MISC_GDI_COMPATIBLE;
    desc.SampleDesc.Count = 1;
    desc.SampleDesc.Quality = 0;
    desc.MipLevels = 1;
    desc.CPUAccessFlags = 0;
    desc.Usage = D3D11_USAGE_DEFAULT;
    hr = device->CreateTexture2D(&desc, NULL, &lGDIImage);
    if (FAILED(hr))
        return 0;
    if (lGDIImage == nullptr)
        return 0;
    // Create CPU access texture
    desc.Width = lOutputDuplDesc.ModeDesc.Width;
    desc.Height = lOutputDuplDesc.ModeDesc.Height;
    desc.Format = lOutputDuplDesc.ModeDesc.Format;
    desc.ArraySize = 1;
    desc.BindFlags = 0;
    desc.MiscFlags = 0;
    desc.SampleDesc.Count = 1;
    desc.SampleDesc.Quality = 0;
    desc.MipLevels = 1;
    desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE;
    desc.Usage = D3D11_USAGE_STAGING;
    hr = device->CreateTexture2D(&desc, NULL, &lDestImage);
    if (FAILED(hr))
        return 0;
    if (lDestImage == nullptr)
        return 0;
    return 1;
}
为了获取屏幕截图,我们进行循环。
hr = cap.lDeskDupl->AcquireNextFrame(
    0,
    &lFrameInfo,
    &lDesktopResource);
if (hr == DXGI_ERROR_WAIT_TIMEOUT)
    hr = S_OK;
if (FAILED(hr))
    break;
if (lDesktopResource && !cap.Get(lDesktopResource, dp.Cursor, 
                                 dp.rx.right && dp.rx.bottom ? &dp.rx : 0))
    break;
get() 方法将返回包含可选的光标(已裁剪)的位图。
bool Get(IDXGIResource* lDesktopResource,bool Curs,RECT* rcx = 0)
{
    // QI for ID3D11Texture2D
    CComPtr<ID3D11Texture2D> lAcquiredDesktopImage;
    if (!lDesktopResource)
        return 0;
    auto hr = lDesktopResource->QueryInterface(IID_PPV_ARGS(&lAcquiredDesktopImage));
    if (!lAcquiredDesktopImage)
        return 0;
    lDesktopResource = 0;
    // Copy image into GDI drawing texture
    context->CopyResource(lGDIImage, lAcquiredDesktopImage);
    // Draw cursor image into GDI drawing texture
    CComPtr<IDXGISurface1> lIDXGISurface1;
    lIDXGISurface1 = lGDIImage;
    if (!lIDXGISurface1)
        return 0;
    CURSORINFO lCursorInfo = { 0 };
    lCursorInfo.cbSize = sizeof(lCursorInfo);
    auto lBoolres = GetCursorInfo(&lCursorInfo);
    if (lBoolres == TRUE)
    {
        if (lCursorInfo.flags == CURSOR_SHOWING && Curs)
        {
            auto lCursorPosition = lCursorInfo.ptScreenPos;
//                auto lCursorSize = lCursorInfo.cbSize;
            HDC  lHDC;
            lIDXGISurface1->GetDC(FALSE, &lHDC);
            DrawIconEx(
                lHDC,
                lCursorPosition.x,
                lCursorPosition.y,
                lCursorInfo.hCursor,
                0,
                0,
                0,
                0,
                DI_NORMAL | DI_DEFAULTSIZE);
            lIDXGISurface1->ReleaseDC(nullptr);
        }
    }
    // Copy image into CPU access texture
    context->CopyResource(lDestImage, lGDIImage);
    // Copy from CPU access texture to bitmap buffer
    D3D11_MAPPED_SUBRESOURCE resource;
    UINT subresource = D3D11CalcSubresource(0, 0, 0);
    hr = context->Map(lDestImage, subresource, D3D11_MAP_READ_WRITE, 0, &resource);
    if (FAILED(hr))
        return 0;
    auto sz = lOutputDuplDesc.ModeDesc.Width
        * lOutputDuplDesc.ModeDesc.Height * 4;
    auto sz2 = sz;
    buf.resize(sz);
    if (rcx)
    {
        sz2 = (rcx->right - rcx->left) * (rcx->bottom - rcx->top) * 4;
        buf.resize(sz2);
        sz = sz2;
    }
    UINT lBmpRowPitch = lOutputDuplDesc.ModeDesc.Width * 4;
    if (rcx)
        lBmpRowPitch = (rcx->right - rcx->left) * 4;
    UINT lRowPitch = std::min<UINT>(lBmpRowPitch, resource.RowPitch);
    BYTE* sptr = reinterpret_cast<BYTE*>(resource.pData);
    BYTE* dptr = buf.data() + sz - lBmpRowPitch;
    if (rcx)
        sptr += rcx->left * 4;
    for (size_t h = 0; h < lOutputDuplDesc.ModeDesc.Height; ++h)
    {
        if (rcx && h < (size_t)rcx->top)
        {
            sptr += resource.RowPitch;
            continue;
        }
        if (rcx && h >= (size_t)rcx->bottom)
            break;
        memcpy_s(dptr, lBmpRowPitch, sptr, lRowPitch);
        sptr += resource.RowPitch;
        dptr -= lBmpRowPitch;
    }
    context->Unmap(lDestImage, subresource);
    return 1;
}
之后,您可以将“buf”数据馈送到 Media Foundation 的 Sink Writer。
音频捕获
您将使用IAudioClient 来获取 IAudioCaptureClient,以便在单独的线程中录制音频。
void ThreadLoopCapture()
{
    UINT64 up, uq;
    while (Capturing)
    {
        if (hEv)
            WaitForSingleObject(hEv, INFINITE);
        if (!Capturing)
            break;
        auto hr = cap->GetBuffer(&pData, &framesAvailable, &flags, &up, &uq);
        if (FAILED(hr))
            break;
        if (framesAvailable == 0)
            continue;
        auto ThisAudioBytes = framesAvailable * wfx.Format.nChannels * 
                                                wfx.Format.wBitsPerSample/8 ;
        AudioDataX->PushX((const char*)pData, ThisAudioBytes);
        cap->ReleaseBuffer(framesAvailable);
    }
    CapturingFin1 = true;
}
如果录制设备是通过环回的播放设备,您必须确保有声音播放,否则 Core Audio API 将录制不到任何内容。因此,我们必须播放静音。
void PlaySilence(REFERENCE_TIME rt)
{
    // ns
    rt /= 10000;
    // in SR , 1000 ms
    //  ?    , rt ms
    auto ns = (wfx.Format.nSamplesPerSec * rt);
    ns /= 1000;
    while (Capturing)
    {
        if (!ren)
            break;
        Sleep((DWORD)(rt / 2));
        if (!Capturing)
            break;
        // See how much buffer space is available.
        UINT32 numFramesPadding = 0;
        auto hr = ac2->GetCurrentPadding(&numFramesPadding);
        if (FAILED(hr))
            break;
        auto numFramesAvailable = ns - numFramesPadding;
        if (!numFramesAvailable)
            continue;
        BYTE* db = 0;
        hr = ren->GetBuffer((UINT32)numFramesAvailable, &db);
        if (FAILED(hr))
            break;
        auto bs = numFramesAvailable * wfx.Format.nChannels * wfx.Format.wBitsPerSample / 8;
        memset(db, 0,(size_t) bs);
        ren->ReleaseBuffer((UINT32)numFramesAvailable, 0); //AUDCLNT_BUFFERFLAGS_SILENT
    }
    CapturingFin2 = true;
}
当有多个音频流时,您必须将它们混合到单个缓冲区中。这是通过我自己的 REBUFFER 和 MIXBUFFER 来完成的。
struct REBUFFER
{
    std::recursive_mutex m;
    std::vector<char> d;
    AHANDLE Has = CreateEvent(0, TRUE, 0, 0);
    MIXBUFFER<float> mb;
    void FinMix(size_t sz, float* A = 0)
    {
        mb.Fin(sz / sizeof(float), A);
    }
    size_t PushX(const char* dd, size_t sz, float* A = 0, float V = 1.0f)
    {
        REBUFFERLOCK l(m);
        auto s = d.size();
        d.resize(s + sz);
        if (dd)
            memcpy(d.data() + s, dd, sz);
        else
            memset(d.data() + s, 0, sz);
        char* a1 = d.data();
        a1 += s;
        mb.Set((float*)a1);
        mb.count = 1;
        SetEvent(Has);
        float* b = (float*)(d.data() + s);
        if (V > 1.01f || V < 0.99f)
        {
            auto st = sz / sizeof(float);
            for (size_t i = 0; i < st; i++)
                b[i] *= V;
        }
        if (A)
        {
            *A = Peak<float>(b, sz / sizeof(float));
        }
        return s + sz;
    }
    size_t Av()
    {
        REBUFFERLOCK l(m);
        return d.size();
    }
    size_t PopX(char* trg, size_t sz, DWORD wi = 0, bool NR = false)
    {
        if (wi)
            WaitForSingleObject(Has, wi);
        REBUFFERLOCK l(m);
        if (sz >= d.size())
            sz = d.size();
        if (sz == 0)
            return 0;
        if (trg)
            memcpy(trg, d.data(), sz);
        if (NR == false)
            d.erase(d.begin(), d.begin() + sz);
        if (d.size() == 0)
            ResetEvent(Has);
        return sz;
    }
    void Clear()
    {
        REBUFFERLOCK l(m);
        d.clear();
    }
};
如果存在音频,视频将与其同步。
HDR 支持
当您的显示器支持 HDR 时,会发生以下情况:
- lDeskDupl->- GetDesc(&lOutputDuplDesc);返回的描述中,格式为- DXGI_FORMAT_R16G16B16A16_FLOAT,这与 GDI 不兼容。
- 无法绘制光标,因此 Cursor参数被忽略。
- Media Foundation 无法创建 HDR 视频。因此,您必须安装Turbo Play 并使用我自己的 Media Foundation 库,该库可以使用 NVIDIA Encoder 创建真正的 HDR-10 视频。如果您安装了 Turbo Play,在安装目录中有一个文件 nvh64.dll。以管理员身份运行 regsvr32,我的过滤器将为您注册,以便与屏幕捕获一起使用。
使用库
#include "stdafx.h"
#include "capture.hpp"
#include <iostream>
int wmain()
{
    CoInitializeEx(0, COINIT_APARTMENTTHREADED);
    MFStartup(MF_VERSION);
    std::cout << "Capturing screen for 10 seconds...";
    DESKTOPCAPTUREPARAMS dp;
    dp.f = L"capture.mp4";
    dp.EndMS = 10000;
    DesktopCapture(dp);
    std::cout << "Done.\r\n";
    return 0;
}
其中 DESKTOPCAPTUREPARAMS 定义如下:
struct DESKTOPCAPTUREPARAMS
{
    bool HasVideo = 1;
    bool HasAudio = 1;
    std::vector<std::tuple<std::wstring, std::vector<int>>> AudioFrom;
    GUID VIDEO_ENCODING_FORMAT = MFVideoFormat_H264;
    GUID AUDIO_ENCODING_FORMAT = MFAudioFormat_MP3;
    std::wstring f;
    void* cb = 0;
    std::function<HRESULT(const BYTE* d, size_t sz,void* cb)> Streamer;
    std::function<HRESULT(const BYTE* d, size_t sz,void* cb)> Framer;
    std::function<void(IMFAttributes* a)> PrepareAttributes;
    int fps = 25;
    int NumThreads = 0;
    int Qu = -1;
    int vbrm = 0;
    int vbrq = 0;
    int BR = 4000;
    int NCH = 2;
    int SR = 44100;
    int ABR = 192;
    bool Cursor = true;
    RECT rx = { 0,0,0,0 };
    HWND hWnd = 0;
    IDXGIAdapter1* ad = 0;
    UINT nOutput = 0;
    unsigned long long StartMS = 0; // 0, none
    unsigned long long EndMS = 0; // 0, none
    bool MustEnd = false;
    bool Pause = false;
};
其中
- HasVideo = 1-> 您正在捕获视频。如果设置了此项,则输出文件必须是 MP4 或 ASF,无论您是否有音频。
- HasAudio = 1-> 您正在捕获音频。如果设置了此项但您没有视频,则输出文件必须是 MP3 或 FLAC。
- AudioFrom= 一个包含您想要捕获的音频设备的向量。每个元素是一个元组,包含设备的唯一 ID(来自枚举,请参阅- VISTAMIXERS::EnumVistaMixers())以及您想要从中录制的通道向量。
该库还可以通过环回从播放设备(如您的扬声器)录制。您可以指定多个录制源,库会将它们全部混合到最终的音频流中。
- VIDEO_ENCODING_FORMAT->- MFVideoFormat_H264、- MFVideoFormat_HEVC、- MFVideoFormat_VP90、- MFVideoFormat_VP80中的一个。HDR 使用 HEVC。
- AUDIO_ENCODING_FORMAT->- MFAudioFormat_MP3、- MFAudioFormat_FLAC或- MFAudioFormat_AAC中的一个。MP3 和 AAC 只支持 44100/48000 双通道输出。
- f-> 目标文件名(仅音频为 MP3/FLAC,否则为 MP4/ASF)。
- fps-> 每秒帧数。
- NumThreads-> 视频编码器的线程数,默认为 0。范围为 0-16。
- Qu->- 如果 >= 0且- <= 0,质量与速度视频因子。
- vbrm和- vbrq-> 如果为- 2,则- vbrq是- 0到- 100之间的质量值(BR 被忽略)。
- BR-> 视频比特率(KBps),默认为 4000。如果- vbrm为- 2,则- BR被忽略。
- NCH-> 音频输出通道数。
- SR-> 音频输出采样率。
- ABR-> MP3 的音频比特率(Kbps)。
- Cursor-> true 表示捕获光标。HDR 时忽略。
- rx-> 如果不为 {0},则只捕获此特定矩形。
- hWnd-> 如果不为 {0},则只捕获此- HWND。如果- HWND为- 0且- rx = {0},则捕获整个屏幕。
- ad-> 如果不为- 0,则指定您想要捕获的适配器(如果您有多个适配器)。
- nOutput-> 要捕获的监视器的索引。- 0是第一个监视器。对于多个监视器,这指定了要捕获的监视器。
- EndMS-> 如果不为- 0,则库将在捕获了- EndMs毫秒后停止。否则,您必须通过将“- MustEnd”设置为- true来停止库。
- MustEnd-> 设置为- true以便库停止捕获。
- Pause-> 如果为- true,则捕获暂停。
如果您想捕获到缓冲区,必须将“f”参数留空并使用 Streamer 参数。这将调用您的回调函数,只要您返回 S_OK。如果您使用 ASF 容器,则无需执行任何操作。如果您想使用 MP4 流,则必须准备流样本描述(请参阅此帖子)。您可以使用此功能通过 HTTP 流式传输您的桌面。
捕获帧
除了捕获压缩视频外,您还可以使用“Framer”回调。只要您返回 S_FALSE,它就会以您请求的分辨率返回一个原始 RGBA 倒置数组(或者如果是 HDR,则返回一个半浮点 64 位数组)。一旦您返回 S_OK,函数就会返回。
历史
- 2024 年 3 月 20 日:添加了 HDR 支持
- 2021 年 4 月 2 日:支持流式捕获、帧捕获
- 2020 年 1 月 18 日:首次发布

