ScreenCapture:通过DirectX 库进行屏幕捕获

这篇具有很好参考价值的文章主要介绍了ScreenCapture:通过DirectX 库进行屏幕捕获。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

具有音频混合功能的 DirectX 硬件屏幕捕获和编码。 H264/H265/VP80/VP90/FLAC/MP3。

以硬件方式捕获视频和屏幕截图。

介绍

有很多关于它的东西。 这是一个简单的单头文件,硬件加速。 如果使用 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 设备:

HRESULT CreateDirect3DDevice(IDXGIAdapter1* g)
{
    HRESULT hr = S_OK;

    // 支持的驱动程序类型
    D3D_DRIVER_TYPE DriverTypes[] =
    {
        D3D_DRIVER_TYPE_HARDWARE,
        D3D_DRIVER_TYPE_WARP,
        D3D_DRIVER_TYPE_REFERENCE,
    };
    UINT NumDriverTypes = ARRAYSIZE(DriverTypes);

    // 支持的功能级别
    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;

    // 创建设备
    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))
        {
            // 设备创建成功,无需循环
            break;
        }
    }
    if (FAILED(hr))
        return hr;

    return S_OK;
}

我们要创建输出的桌面副本,然后:

bool Prepare(UINT Output = 0)
{
    // 获取 DXGI 设备
    CComPtr<IDXGIDevice> lDxgiDevice;
    lDxgiDevice = device;
    if (!lDxgiDevice)
        return 0;

    // 获取 DXGI 适配器
    CComPtr<IDXGIAdapter> lDxgiAdapter;
    auto hr = lDxgiDevice->GetParent(
        __uuidof(IDXGIAdapter),
        reinterpret_cast<void**>(&lDxgiAdapter));

    if (FAILED(hr))
        return 0;

    lDxgiDevice = 0;

    // 获取输出
    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;

    // 创建桌面副本
    hr = lDxgiOutput1->DuplicateOutput(
        device,
        &lDeskDupl);

    if (FAILED(hr))
        return 0;

    lDxgiOutput1 = 0;

    // 创建 GUI 绘图纹理
    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;

    // 创建 CPU 访问纹理
    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)
{
    // ID3D11Texture2D 的 QI
    CComPtr<ID3D11Texture2D> lAcquiredDesktopImage;
    if (!lDesktopResource)
        return 0;
    auto hr = lDesktopResource->QueryInterface(IID_PPV_ARGS(&lAcquiredDesktopImage));
    if (!lAcquiredDesktopImage)
        return 0;
    lDesktopResource = 0;

    // 将图像复制到 GDI 绘图纹理中
    context->CopyResource(lGDIImage, lAcquiredDesktopImage);

    // 将光标图像绘制到 GDI 绘图纹理中
    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);
        }
    }

    // 将图像复制到 CPU 访问纹理中
    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”数据输入媒体基金会的接收器写入器。

音频捕捉

您将使用 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;
}

如果录音设备是通过 loopback 的播放设备,则必须确保播放了某些内容,否则 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;

        // 查看有多少可用的缓冲区空间。
        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();
    }
};

如果您有音频,则视频会与之同步。

使用库

#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 之一。
AUDIO_ENCODING_FORMAT -> MFAudioFormat_MP3 或 MFAudioFormat_FLAC 或 MFAudioFormat_AAC 之一。 MP3 和 AAC 仅支持 44100/48000 2 通道输出。
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 捕获光标
rx -> 如果不是 {0},则仅捕获此特定矩形
hWnd -> 如果不是 {0},则仅捕获此 HWND。如果 HWND 为 0 且 rx = {0},则捕获整个屏幕
ad -> 如果不是 0,则指定如果您有超过 1 个适配器,您要捕获哪个适配器
nOutput -> 要捕获的监视器的索引。 0 是第一个监视器。对于多个监视器,这指定了监视器。
EndMS -> 如果不为 0,则库在 EndMs 毫秒被捕获时停止。否则,您必须通过将“MustEnd”设置为 true 来停止库。
MustEnd -> 设置为 true 以使库停止捕获
暂停 -> 如果为真,则暂停捕获
如果要捕获到缓冲区,则必须将“f”参数留空并使用 Streamer 参数。只要您返回 S_OK,这就会调用您的回调。如果您使用 ASF 容器,则无需执行任何操作。如果您想使用 MP4 流,则必须准备流示例描述(请参阅此帖子)。您可以使用它通过 HTTP 流式传输您的桌面。

捕获帧

您可以使用“Framer”回调,而不是捕获压缩视频。 只要您返回 S_FALSE,这将返回您请求的分辨率的原始 RGBA 倒置数组。 一旦你返回 S_OK,函数就会返回。文章来源地址https://www.toymoban.com/news/detail-444726.html

到了这里,关于ScreenCapture:通过DirectX 库进行屏幕捕获的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 第11课 利用windows API捕获桌面图像并通过FFmpeg分享

    在上一章,我们已经实现了一对一音视频对话功能。在实际应用中,我们常需要把自己的电脑桌面分享给他人以实现桌面共享功能,这种功能在视频会议、在线教学等场景中很常见,这种功能如何实现呢?这节课我们就来解决这个问题。 1.备份demo9并修改demo9为demo11。 2.在fm

    2024年02月03日
    浏览(24)
  • 6.3.5 利用Wireshark进行协议分析(五)----捕获并分析ICMP报文

    6.3.5 利用Wireshark进行协议分析(五)----捕获并分析ICMP报文 一、捕获ICMP报文 打开Wireshark,选择网络接口并点击开始按钮。分组列表面板不断刷新抓渠道的数据包,为了过滤出我们所要分析的ICMP报文,我们在过滤框中输入icmp,为了生成icmp报文,我们需要运行系统自带的ping程

    2024年02月16日
    浏览(32)
  • STM32 通过PWM输出一个方波并通过定时器输入捕获模式测量方波的周期(cubeMX+keil配置)

    前言:本文章用cubeMX和keil来进行代码编写,实现STM32的相应功能 本文章使用的STM32核心板是STM32H743VIT6,如果使用的是其他的核心板操作过程类似,可以尝试使用此教程。 (1)首先点击左侧的Timers  (2)选择一个定时器配置PWM 我选择的是TIM5,如上图 点击TIM5 Mode的配置如下

    2024年02月19日
    浏览(40)
  • HarmonyOS实战开发-通过screenshot模块实现屏幕截图 。

    本示例展示全屏截图和屏幕局部截图。 本示例通过screenshot模块实现屏幕截图 ,通过window模块实现隐私窗口切换,通过display模块查询当前隐私窗口。 使用说明: 点击右上角图标打开弹窗,选择截屏,展示全屏截图;选择局部截屏,选择截屏区域,点击右下角完成,展示局部

    2024年04月13日
    浏览(26)
  • 如何用Python进行屏幕录制?

    关于屏幕录制这个功能需求,之前用过基于ffmpeg的Capture录屏软件,但是fps拉高以后会变得很卡,声音也同样出现卡顿。也自己尝试过在python中调用ffmpeg的库函数,效果也不尽人意。网络上下载了几款录屏软件,不是要收费就是下载到捆绑软件或广告很是心累,因此想借此机会

    2024年02月01日
    浏览(21)
  • Ubuntu系统中如何进行屏幕截图

    前言:我的环境是双系统(ubuntu20.04),但应该无论是什么版本的ubuntu都可以实现。 方法: 1.快捷键截图: 在设置里找到键盘快捷键,找到截图目录,就可以看到有关截图的快捷键(可以自己手动更改——单击选项即可) 一般使用 shift+print (print为windows系统中截图的快捷键)进

    2024年02月12日
    浏览(25)
  • 通过X11获取屏幕截图并转为opencv Mat

    #include bits/stdc++.h #include \\\"opencv2/core.hpp\\\" #include \\\"opencv2/highgui.hpp\\\" #include \\\"opencv2/opencv.hpp\\\" #include \\\"opencv2/videoio.hpp\\\" #include X11/Xlib.h //-lX11 #include X11/Xutil.h #include X11/Xmd.h #include X11/Xatom.h using namespace std; using namespace cv; Mat getScreenShot() {     Display *dis=XOpenDisplay((char *)0);     Screen *scr = X

    2024年02月15日
    浏览(27)
  • android 通过adb shell命令旋转Android屏幕朝向

    注意: 默认0有的为横向,有的为纵向 纵向返回结果: cur 的值 宽 短 x 高 长 init=1080x1920 420dpi cur=1080x1920 app=1080x1794 rng=1080x1017-1794x1731 横向返回结果: cur 的值 宽 长 x 高 短 init=1080x1920 420dpi cur=1920x1080 app=1794x1080 rng=1080x1017-1794x1731

    2024年02月11日
    浏览(59)
  • android 通过adb shell命令旋转Android屏幕朝向方向

    注意: 默认0有的为横向,有的为纵向 纵向返回结果: cur 的值 宽 短 x 高 长 init=1080x1920 420dpi cur=1080x1920 app=1080x1794 rng=1080x1017-1794x1731 横向返回结果: cur 的值 宽 长 x 高 短 init=1080x1920 420dpi cur=1920x1080 app=1794x1080 rng=1080x1017-1794x1731

    2024年02月06日
    浏览(43)
  • [unity]如何通过代码获取UI宽高和屏幕宽高

    1.获取UI宽高 首先,使用GetComponentRectTransform().sizeDelta获取,但是这样会有问题,会跟锚点设置有关,改变设置后获取不对 只适用于MiddleCenter 所以又看了API,可以使用GetComponentRectTransform().rect获取 打印如下:  2.获取屏幕宽高 宽度: UnityEngine.Screen.width 高度: UnityEngine.Screen.

    2024年02月17日
    浏览(28)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包