Directshow(SDK)学习笔记九_视频捕捉 选择自 dankes 的 Blog

本文深入探讨了DirectShow中视频捕捉的各项技术细节,包括视频捕捉Graph的构建、选择捕捉设备、预览视频、捕捉视频流并保存到文件、控制CaptureGraph、配置视频捕捉设备等内容。

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

主要包括下面内容

1关于视频捕捉(About Video Capture in Dshow) 2选择一个视频捕捉设备(Select capture device) 3预览视频(Previewing Video) 4如何捕捉视频流并保存到文件(Capture video to File) 5将设备从系统中移走时的事件通知(Device remove Notify) 6如何控制Capture Graph(Controlling Capture Graph) 7如何配置一个视频捕捉设备 8从静止图像pin中捕捉图片

1关于视频捕捉(About Video Capture in Dshow)

1视频捕捉Graph的构建 一个能够捕捉音频或者视频的graph图都称之为捕捉graph图。捕捉graph图比一般的文件回放graph图要复杂许多,dshow提供了一个Capture Graph Builder COM组件使得捕捉graph图的生成更加简单。Capture Graph Builder提供了一个ICaptureGraphBuilder2接口,这个接口提供了一些方法用来构建和控制捕捉graph。 首先创建一个Capture Graph Builder对象和一个graph manger对象,然后用filter graph manager 作参数,调用ICaptureGraphBuilder2::SetFiltergraph来初始化Capture Graph Builder。2视频捕捉的设备 现在许多新的视频捕捉设备都采用的是WDM驱动方法,在WDM机制中,微软提供了一个独立于硬件设备的驱动,称为类驱动程序。驱动程序的供应商提供的驱动程序称为minidrivers。Minidrivers提供了直接和硬件打交道的函数,在这些函数中调用了类驱动。 在directshow的filter图表中,任何一个WDM捕捉设备都是做为一个WDM Video Capture 过滤器(Filter)出现。WDM Video Capture过滤器根据驱动程序的特征构建自己的filter

下面是陆其明的一篇有关于dshow和硬件的文章,可以拿来参考一下

//陆文章开始 大家知道,为了提高系统的稳定性,Windows操作系统对硬件操作进行了隔离;应用程序一般不能直接访问硬件。DirectShow Filter工作在用户模式(User mode,操作系统特权级别为Ring 3),而硬件工作在内核模式(Kernel mode,操作系统特权级别为Ring 0),那么它们之间怎么协同工作呢?

DirectShow解决的方法是,为这些硬件设计包装Filter;这种Filter能够工作在用户模式下,外观、控制方法跟普通Filter一样,而包装Filter内部完成与硬件驱动程序的交互。这样的设计,使得编写DirectShow应用程序的开发人员,从为支持硬件而需做出的特殊处理中解脱出来。DirectShow已经集成的包装Filter,包括Audio Capture Filter(qcap.dll)、VfW Capture Filter(qcap.dll,Filter的Class Id为CLSID_VfwCapture)、TV Tuner Filter(KSTVTune.ax,Filter的Class Id为CLSID_CTVTunerFilter)、Analog Video Crossbar Filter(ksxbar.ax)、TV Audio Filter(Filter的Class Id为CLSID_TVAudioFilter)等;另外,DirectShow为采用WDM驱动程序的硬件设计了KsProxy Filter(Ksproxy.ax,)。我们来看一下结构图:

图1 从上图中,我们可以看出,Ksproxy.ax、Kstune.ax、Ksxbar.ax这些包装Filter跟其它普通的DirectShow Filter处于同一个级别,可以协同工作;用户模式下的Filter通过Stream Class控制硬件的驱动程序minidriver(由硬件厂商提供的实现对硬件控制功能的DLL);Stream Class和minidriver一起向上层提供系统底层级别的服务。值得注意的是,这里的Stream Class是一种驱动模型,它负责调用硬件的minidriver;另外,Stream Class的功能还在于协调minidriver之间的工作,使得一些数据可以直接在Kernel mode下从一个硬件传输到另一个硬件(或同一个硬件上的不同功能模块),提高了系统的工作效率。(更多的关于底层驱动程序的细节,请读者参阅Windows DDK。)

下面,我们分别来看一下几种常见的硬件。 VfW视频采集卡。这类硬件在市场上已经处于一种淘汰的趋势;新生产的视频采集卡一般采用WDM驱动模型。但是,DirectShow为了保持向后兼容,还是专门提供了一个包装Filter支持这种硬件。和其他硬件的包装Filter一样,这种包装Filter的创建不是像普通Filter一样使用CoCreateInstance,而要通过系统枚举,然后BindToObject。

音频采集卡(声卡)。声卡的采集功能也是通过包装Filter来实现的;而且现在的声卡大部分都有混音的功能。这个Filter一般有几个Input pin,每个pin都代表一个输入,如Line In、Microphone、CD、MIDI等。值得注意的是,这些pin代表的是声卡上的物理输入端子,在Filter Graph中是永远不会连接到其他Filter上的。声卡的输出功能,可以有两个Filter供选择:DirectSound Renderer Filter和Audio Renderer (WaveOut) Filter。注意,这两个Filter不是上述意义上的包装Filter,它们能够同硬件交互,是因为它们使用了API函数:前者使用了DirectSound API,后者使用了waveOut API。这两个Filter的区别,还在于后者输出音频的同时不支持混音。(顺便说明一下,Video Renderer Filter能够访问显卡,也是因为使用了GDI、DirectDraw或Direct3D API。)如果你的机器上有声卡的话,你可以通过GraphEdit,在Audio Capture Sources目录下看到这个声卡的包装Filter。

WDM驱动的硬件(包括视频捕捉卡、硬件解压卡等)。这类硬件都使用Ksproxy.ax这个包装Filter。Ksproxy.ax实现了很多功能,所以有“瑞士军刀”的美誉;它还被称作为“变色龙Filter”,因为该Filter上定义了统一的接口,而接口的实现因具体的硬件驱动程序而异。在Filter Graph中,Ksproxy Filter显示的名字为硬件的Friendly name(一般在驱动程序的.inf文件中定义)。我们可以通过GraphEdit,在WDM Streaming开头的目录中找到本机系统中安装的WDM硬件。因为KsProxy.ax能够代表各种WDM的音视频设备,所以这个包装Filter的工作流程有点复杂。这个Filter不会预先知道要代表哪种类型的设备,它必须首先访问驱动程序的属性集,然后动态配置Filter上应该实现的接口。当Ksproxy Filter上的接口方法被应用程序或其他Filter调用时,它会将调用方法以及参数传递给驱动程序,由驱动程序最终完成指定功能。除此以外,WDM硬件还支持内核流(Kernel Streaming),即内核模式下的数据传输,而无需经过到用户模式的转换。因为内核模式与用户模式之间的相互转换,需要花费很大的计算量。如果使用内核流,不仅可以避免大量的计算,还避免了内核数据与主机内存之间的拷贝过程。在这种情况下,用户模式的Filter Graph中,即使pin之间是连接的,也不会有实际的数据流动。典型的情况,如带有Video Port Pin的视频捕捉卡,Preview时显示的图像就是在内核模式下直接传送到显卡的显存的。所以,你也休想在VP Pin后面截获数据流。

讲到这里,我想大家应该对DirectShow对硬件的支持问题有了一个总体的认识。对于应用程序开发人员来说,这方面的内容不用研究得太透,而只需作为背景知识了解一下就好了。其实,大量繁琐的工作DirectShow已经帮我们做好了。 //陆其明文章结束

Direcshow中视频捕捉的Filter Pin的种类 捕捉Filter一般都有两个或多个输出pin,他们输出的媒体类型都一样,比如预览pin和捕捉pin,因此根据媒体类型就不能很好的区别这些pin。此时就要根据pin的功能来区别每个pin了,每个pin都有一个GUID,称为pin的种类。 如果想仔细的了解pin的种类,请看后面的相关内容Working with Pin Categories。对于大多数的应用来说,ICaptureGraphBuilder2提供了一些函数可以自动确定pin的种类。 预览pin和捕捉pin 视频捕捉Filter都提供了预览和捕捉的输出pin,预览pin用来将视频流在屏幕上显示,捕捉pin用来将视频流写入文件。 预览pin和输出pin有下面的区别: 1 为了保证捕捉pin对视频桢流量,预览pin必要的时候可以停止。 2 经过捕捉pin的视频桢都有时间戳,但是预览pin的视频流没有时间戳。 预览pin的视频流之所以没有时间戳的原因在于filter图表管理器在视频流里加一个很小的latency,如果捕捉时间被认为就是render时间的话,视频renderFilter就认为视频流有一个小小的延迟,如果此时render filter试图连续播放的时候,就会丢桢。去掉时间戳就保证了视频桢来了就可以播放,不用等待,也不丢桢。 预览pin的种类GUID为PIN_CATEGORY_PREVIEW 捕捉pin的种类GUID为PIN_CATEGORY_CAPTURE Video Port pin Video Port是一个介于视频设备(TV)和视频卡之间的硬件设备。同过Video Port,视频数据可以直接发送到图像卡上,通过硬件的覆盖,视频可以直接在屏幕显示出来。Video Port就是连接两个设备的。 使用Video Port的最大好处是,不用CPU的任何工作,视频流直接写入内存中。当然它也有下面的缺点drawbacks: 略 如果捕捉设备使用了Video Port,捕捉Filter就用一个video port pin代替预览pin。 video port pin的种类GUID为PIN_CATEGORY_VIDEOPORT 一个捕捉filter至少有一个Capture pin,另外,它可能有一个预览pin 和一个video port pin ,或者两者都没有,也许filter有很多的capture pin,和预览pin,每一个pin都代表一种媒体类型,因此一个filter可以有一个视频capture pin,视频预览pin,音频捕捉pin,音频预览pin。 Upstream WDM Filters 在捕捉Filter之上,WDM设备可能需要额外的filters,下面就是这些filter TV Tuner Filter TV Audio Filter. Analog Video Crossbar Filter 尽管这些都是一些独立的filter,但是他们可能代表的是同一个硬件设备,每个filter都控制设备的不同函数,这些filter通过pin连接起来,但是在pin中没有数据流动。因此,这些pin 的连接和媒体类型无关。他们使用一个GUID值来定义一个给定设备的minidriver,例如:TV tuner Filter 和video capture filter都支持同一种medium。 在实际应用中,如果你使用ICaptureGraphBuilder2来创建你的capture graphs,这些filters就会自动被添加到你的graph中。更多的详细资料,可以参考WDM Class Driver Filters

2选择一个视频捕捉设备(Select capture device)

如何选择一个视频捕捉设备,可以采用系统设备枚举,详细资料参见Using the System Device Enumerator 。enumerator可以根据filter的种类返回一个设备的monikers。Moniker是一个com对象,可以参见IMoniker的SDK。 对于捕捉设备,下面两种类是相关的。 CLSID_AudioInputDeviceCategory 音频设备 CLSID_VideoInputDeviceCategory 视频设备 下面的代码演示了如何枚举一个视频捕捉设备

  ICreateDevEnum *pDevEnum = NULL;   IEnumMoniker *pEnum = NULL; // Create the System Device Enumerator.   HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL,   CLSCTX_INPROC_SERVER, IID_ICreateDevEnum,   reinterpret_cast<void**>(&pDevEnum));   if (SUCCEEDED(hr))   {   //创建一个枚举器,枚举视频设备   hr = pDevEnum->CreateClassEnumerator( CLSID_VideoInputDeviceCategory,   &pEnum, 0);   }

IEnumMoniker接口pEnum返回一个IMoniker接口的列表,代表一系列的moniker,你可以显示所有的设备,然后让用户选择一个。 采用IMoniker::BindToStorage方法,返回一个IPropertyBag接口指针。然后调用IPropertyBag::Read读取moniker的属性。下面看看都包含什么属性 1 FriendlyName 是设备的名字 2 Description 属性仅仅适用于DV和D-VHS/MPEG摄象机,如果这个属性可用,这个属性更详细的描述了设备的资料 3DevicePath 这个属性是不可读的,但是每个设备都有一个独一无二的。你可以用这个属性来区别同一个设备的不同实例 下面的代码演示了如何显示遍历设备的名称 ,接上面的代码

HWND hList; // Handle to the list box.   IMoniker *pMoniker = NULL;   while (pEnum->Next(1, &pMoniker, NULL) == S_OK)   {   IPropertyBag *pPropBag;   hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag,   (void**)(&pPropBag));   if (FAILED(hr))   {   pMoniker->Release();   continue; // Skip this one, maybe the next one will work.   }   // Find the description or friendly name.   VARIANT varName;   VariantInit(&varName);   hr = pPropBag->Read(L"Description", &varName, 0);   if (FAILED(hr))   {   hr = pPropBag->Read(L"FriendlyName", &varName, 0);   }   if (SUCCEEDED(hr))   {   // Add it to the application's list box.   USES_CONVERSION;   (long)SendMessage(hList, LB_ADDSTRING, 0,   (LPARAM)OLE2T(varName.bstrVal));   VariantClear(&varName);   }   pPropBag->Release();   pMoniker->Release();   }

如果用户选中了一个设备调用IMoniker::BindToObject为设备生成filter,然后将filter加入到graph中。

IBaseFilter *pCap = NULL;   hr = pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&pCap);   if (SUCCEEDED(hr))   {   hr = m_pGraph->AddFilter(pCap, L"Capture Filter");   } 3预览视频(Previewing Video)

为了创建可以预览视频的graph,可以调用下面的代码

ICaptureGraphBuilder2 *pBuild; // Capture Graph Builder   // Initialize pBuild (not shown). IBaseFilter *pCap; // Video capture filter.   /* Initialize pCap and add it to the filter graph (not shown). */ hr = pBuild->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video,   pCap, NULL, NULL);

4如何捕捉视频流并保存到文件(Capture video to File)

1 将视频流保存到AVI文件 下面的图表显示了graph图

图2 AVI Mux filter接收从capture pin过来的视频流,然后将其打包成AVI流。音频流也可以连接到AVI Mux Filter上,这样mux filter就将视频流和视频流合成AVI流。File writer将AVI流写入到文件中。 可以像下面这样构建graph图

IBaseFilter *pMux;   hr = pBuild->SetOutputFileName(   &MEDIASUBTYPE_Avi, // Specifies AVI for the target file.   L"C://Example.avi", // File name.   &pMux, // Receives a pointer to the mux.   NULL); // (Optional) Receives a pointer to the file sink.

第一个参数表明文件的类型,这里表明是AVI,第二个参数是制定文件的名称。对于AVI文件,SetOutputFileName函数会创建一个AVI mux Filter 和一个 File writer Filter ,并且将两个filter添加到graph图中,在这个函数中,通过File Writer Filter 请求IFileSinkFilter接口,然后调用IFileSinkFilter::SetFileName方法,设置文件的名称。然后将两个filter连接起来。第三个参数返回一个指向 AVI Mux的指针,同时,它也通过第四个参数返回一个IFileSinkFilter参数,如果你不需要这个参数,你可以将这个参数设置成NULL。 然后,你应该调用下面的函数将capture filter 和AVI Mux连接起来。

hr = pBuild->RenderStream(   &PIN_CATEGORY_CAPTURE, // Pin category.   &MEDIATYPE_Video, // Media type.   pCap, // Capture filter.   NULL, // Intermediate filter (optional).   pMux); // Mux or file sink filter.   // Release the mux filter.   pMux->Release();

第5个参数就是使用的上面函数返回的pMux指针。 当捕捉音频的时候,媒体类型要设置为MEDIATYPE_Audio,如果你从两个不同的设备捕捉视频和音频,你最好将音频设置成主流,这样可以防止两个数据流间drift,因为avi mux filter为同步音频,会调整视频的播放速度的。为了设置master 流,调用IConfigAviMux::SetMasterStream方法,可以采用如下的代码:

IConfigAviMux *pConfigMux = NULL;   hr = pMux->QueryInterface(IID_IConfigAviMux, (void**)&pConfigMux);   if (SUCCEEDED(hr))   {   pConfigMux->SetMasterStream(1);   pConfigMux->Release();   } SetMasterStream的参数指的是数据流的数目,这个是由调用RenderStream的次序决定的。例如,如果你调用RenderStream首先用于视频流,然后是音频,那么视频流就是0,音频流就是1。 添加编码filter

  IBaseFilter *pEncoder;   /* Create the encoder filter (not shown). */   // Add it to the filter graph.   pGraph->AddFilter(pEncoder, L"Encoder); /* Call SetOutputFileName as shown previously. */ // Render the stream.   hr = pBuild->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video,     pCap, pEncoder, pMux);   pEncoder->Release();

2将视频流保存成wmv格式的文件 为了将视频流保存成并编码成windows media video (WMV)格式的文件,将capture pin连到WM ASF Writer filter。

图3 构建graph图最简单的方法就是将在ICaptureGraphBuilder2::SetOutputFileName方法中指定MEDIASUBTYPE_Asf的filter。如下

 IBaseFilter* pASFWriter = 0;   hr = pBuild->SetOutputFileName(   &MEDIASUBTYPE_Asf, // Create a Windows Media file.   L"C://VidCap.wmv", // File name.   &pASFWriter, // Receives a pointer to the filter.   NULL); // Receives an IFileSinkFilter interface pointer (optional).   参数MEDIASUBTYPE_Asf 告诉graph builder,要使用wm asf writer作为文件接收器,于是,pbuild 就创建这个filter,将其添加到graph图中,然后调用IFileSinkFilter::SetFileName来设置输出文件的名字。第三个参数用来返回一个ASF writer指针,第四个参数用来返回文件的指针。 在将任何pin连接到WM ASF Writer之前,一定要对WM ASF Writer进行一下设置,你可以同过WM ASF Writer的IConfigAsfWriter接口指针来进行设置。

 IConfigAsfWriter *pConfig = 0;   hr = pASFWriter->QueryInterface(IID_IConfigAsfWriter, (void**)&pConfig);   if (SUCCEEDED(hr))   {   // Configure the ASF Writer filter.   pConfig->Release();   }

然后调用ICaptureGraphBuilder2::RenderStream将capture Filter 和 ASF writer连接起来。

 hr = pBuild->RenderStream(   &PIN_CATEGORY_CAPTURE, // Capture pin.   &MEDIATYPE_Video, // Video. Use MEDIATYPE_Audio for audio.   pCap, // Pointer to the capture filter.   0,   pASFWriter); // Pointer to the sink filter (ASF Writer).

3保存成自定义的文件格式 如果你想将文件保存成自己的格式,你必须有自己的 file writer。看下面的代码

 IBaseFilter *pMux = 0;   IFileSinkFilter *pSink = 0;   hr = pBuild->SetOutputFileName(   &CLSID_MyCustomMuxFilter, //自己开发的Filter   L"C://VidCap.avi", &pMux, &pSink);

4如何将视频流保存进多个文件 当你将视频流保存进一个文件后,如果你想开始保存第二个文件,这时,你应该首先将graph停止,然后通过IFileSinkFilter::SetFileName改变 File Writer 的文件名称。注意,IFileSinkFilter指针你可以在SetOutputFileName时通过第四个参数返回的。 看看保存多个文件的代码吧

 IBaseFilter *pMux;   IFileSinkFilter *pSink   hr = pBuild->SetOutputFileName(&MEDIASUBTYPE_Avi, L"C://YourFileName.avi",     &pMux, &pSink);   if (SUCCEEDED(hr))   {   hr = pBuild->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video,     pCap, NULL, pMux);  if (SUCCEEDED(hr))   {   pControl->Run();   /* Wait awhile, then stop the graph. */   pControl->Stop();   // Change the file name and run the graph again.   pSink->SetFileName(L"YourFileName02.avi", 0);   pControl->Run();   }   pMux->Release();   pSink->Release();   }

5组合视频的捕捉和预览 如果想组建一个既可以预览视频,又可以将视频保存成文件的graph,只需要两次调用ICaptureGraphBuilder2::RenderStream即可。代码如下:

 // Render the preview stream to the video renderer.   hr = pBuild->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video,   pCap, NULL, NULL); // Render the capture stream to the mux.   hr = pBuild->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video,   pCap, NULL, pMux);

在上面的代码中,graph builder 其实隐藏了下面的细节。 1 如果capture Filter既有preview pin 也有captrue pin,那么RenderStream仅仅将两个pin和render filter接起来。如下图

图4 2如果caprture Filter只有一个capture pin,那么Capture Graph Builder就采用一个Smart Tee Filter将视频流分流,graph图如下

图5

5如何控制Capture Graph(Controlling Capture Graph)

Filter图表管理器可以通过IMediaControl接口控制整个graph的运行,停止和暂停。但是当一个graph有捕捉和预览两个数据流的时候,如果我们想单独的控制其中的一个数据流话,我们可以通过ICaptureGraphBuilder2::ControlStream 。 下面讲一下如何来单独控制捕捉和预览数据流。 1 控制捕捉视频流 下面的代码,让捕捉数据流在graph开始运行1秒后开始,允运行4秒后结束。

 // Control the video capture stream.   REFERENCE_TIME rtStart = 1000 0000, rtStop = 5000 0000;   const WORD wStartCookie = 1, wStopCookie = 2; // Arbitrary values.   hr = pBuild->ControlStream(   &PIN_CATEGORY_CAPTURE, // Pin category.   &MEDIATYPE_Video, // Media type.   pCap, // Capture filter.   &rtStart, &rtStop, // Start and stop times.   wStartCookie, wStopCookie // Values for the start and stop events.   );   pControl->Run();

第一个参数表明需要控制的数据流,一般采用的是pin种类GUID, 第二个参数表明了媒体类型。 第三个参数指明了捕捉的filter。如果想要控制graph图中的所有捕捉filter,第二个和第三个参数都要设置成NULL。 第四和第五个参数表明了流开始和结束的时间,这是一个相对于graph开始的时间。 只有你调用IMediaControl::Run以后,这个函数才有作用。如果graph正在运行,这个设置立即生效。 最后的两个参数用来设置当数据流停止,开始能够得到的事件通知。对于任何一个运用此方法的数据流,graph当流开始的时候,会发送EC_STREAM_CONTROL_STARTED通知,在流结束的时候,要发送EC_STREAM_CONTROL_STOPPED通知。wStartCookie和wStopCookie是作为第二个参数的。 看看事件通知处理过程吧

 while (hr = pEvent->GetEvent(&evCode, &param1, &param2, 0), SUCCEEDED(hr))   {   switch (evCode)   {   case EC_STREAM_CONTROL_STARTED:   // param2 == wStartCookie   break;  case EC_STREAM_CONTROL_STOPPED:   // param2 == wStopCookie   break;     }   pEvent->FreeEventParams(evCode, param1, param2);   }

ControlStream还定义了一些特定的值来表示开始和停止的时间。 MAXLONGLONG 从不开始,只有在graph停止的时候才停止 NULL, 立即开始和停止 例如,下面的代码立即停止捕捉流。

 pBuild->ControlStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap,   0, 0, // Start and stop times.   wStartCookie, wStopCookie); 2控制预览视频流 只要给ControlStream第一个参数设置成PIN_CATEGORY_PREVIEW就可以控制预览pin,整个函数的使用和控制捕捉流一样,但是唯一区别是在这里你没法设置开始和结束时间了,因为预览的视频流没有时间戳,因此你必须使用NULL或者MAXLONGLONG。例子

 Use NULL to start the preview stream:   pBuild->ControlStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pCap,   NULL, // Start now.   0, // (Don't care.)   wStartCookie, wStopCookie);   Use MAXLONGLONG to stop the preview stream:   pBuild->ControlStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, pCap,   0, // (Don't care.)   MAXLONGLONG, // Stop now.   wStartCookie, wStopCookie);

3关于数据流的控制 Pin的缺省的行为是传递sample,例如,如果你对PIN_CATEGORY_CAPTURE使用了ControlStream,但是对于PIN_CATEGORY_PREVIEW没有使用该函数,因此,当你run graph的时候,preview 流会立即运行起来,而capture 流则要等到你设置的时间运行。

6如何配置一个视频捕捉设备

1显示VFW驱动的视频设备对话框 如果视频捕捉设备采用的仍然是VFW方式的驱动程序,则必须支持下面三个对话框,用来设置视频设备。 1 Video Source 用来选择视频输入设备并且调整设备的设置,比如亮度和对比度。 2Video Format 用来设置桢的大小和位 3Video Display 用来设置视频的显示参数 为了显示上面的三个对话框,你可以do the following 1 停止graph。 2向捕捉filter请求IAMVfwCaptureDialogs接口,如果成功,表明设备支持VFW驱动。 3调用IAMVfwCaptureDialogs::HasDialog来检查驱动程序是否支持你请求的对话框,如果支持,返回S_OK,否则返回S_FALSE。注意不要用SUCCEDED宏。 4如果驱动支持该对话框,调用IAMVfwCaptureDialogs::ShowDialog显示该对话框。 5 重新运行graph 代码如下

 pControl->Stop(); // Stop the graph.   // Query the capture filter for the IAMVfwCaptureDialogs interface.   IAMVfwCaptureDialogs *pVfw = 0;   hr = pCap->QueryInterface(IID_IAMVfwCaptureDialogs, (void**)&pVfw);   if (SUCCEEDED(hr))   {   // Check if the device supports this dialog box.   if (S_OK == pVfw->HasDialog(VfwCaptureDialog_Source))   {   // Show the dialog box.   hr = pVfw->ShowDialog(VfwCaptureDialog_Source, hwndParent);   }   }   pControl->Run();

2 调整视频的质量 WDM驱动的设备支持一些属性可以用来调整视频的质量,比如亮度,对比度,饱和度,等要向调整视频的质量,do the following 1 从捕捉filter上请求IAMVideoProcAmp接口 2 对于你想调整的任何一个属性,调用IAMVideoProcAmp::GetRange可以返回这个属性赋值的范围,缺省值,最小的增量值。IAMVideoProcAmp::Get返回当前正在使用的值。VideoProcAmpProperty枚举每个属性定义的标志。 3调用IAMVideoProcAmp::Set来设置这个属性值。设置属性的时候,不用停止graph。 看看下面的代码是如何调整视频的质量的

HWND hTrackbar; // Handle to the trackbar control.   // Initialize hTrackbar (not shown). // Query the capture filter for the IAMVideoProcAmp interface.   IAMVideoProcAmp *pProcAmp = 0;   hr = pCap->QueryInterface(IID_IAMVideoProcAmp, (void**)&pProcAmp);   if (FAILED(hr))   {   // The device does not support IAMVideoProcAmp, so disable the control.   EnableWindow(hTrackbar, FALSE);   }   else   {   long Min, Max, Step, Default, Flags, Val;  // Get the range and default value.   hr = m_pProcAmp->GetRange(VideoProcAmp_Brightness, &Min, &Max, &Step,   &Default, &Flags);   if (SUCCEEDED(hr))   {   // Get the current value.   hr = m_pProcAmp->Get(VideoProcAmp_Brightness, &Val, &Flags);   }   if (SUCCEEDED(hr))   {   // Set the trackbar range and position.   SendMessage(hTrackbar, TBM_SETRANGE, TRUE, MAKELONG(Min, Max));   SendMessage(hTrackbar, TBM_SETPOS, TRUE, Val);   EnableWindow(hTrackbar, TRUE);   }   else   {   // This property is not supported, so disable the control.   EnableWindow(hTrackbar, FALSE);   }   }

3调整视频输出格式 我们知道视频流可以有多种输出格式,一个设备可以支持16-bit RGB, 32-bit RGB, and YUYV,在每一种格式下,设备还可以调整视频桢的大小。 在WDM驱动设备上,IAMStreamConfig 接口用来报告设备输出视频的格式的,VFW设备,可以采用对话框的方式来设置,参见前面的内容。 捕捉Filter的捕捉pin和预览pin都支持IAMStreamConfig 接口,可以通过ICaptureGraphBuilder2::FindInterface获得IAMStreamConfig接口。

 IAMStreamConfig *pConfig = NULL;   hr = pBuild->FindInterface(   &PIN_CATEGORY_PREVIEW, // Preview pin.   0, // Any media type.   pCap, // Pointer to the capture filter.   IID_IAMStreamConfig, (void**)&pConfig);

设备还支持一系列的媒体类型,对于每一个媒体类型,设备都要支持一系列的属性,比如,桢的大小,图像如何缩放,桢率的范围等。 通过IAMStreamConfig::GetNumberOfCapabilities获得设备所支持的媒体类型的数量。这个方法返回两个值,一个是媒体类型的数量,二是属性所需结构的大小。 这个结构的大小很重要,因为这个方法是用于视频和音频的,视频采用的是VIDEO_STREAM_CONFIG_CAPS结构,音频用AUDIO_STREAM_CONFIG_CAPS结构。 通过函数IAMStreamConfig::GetStreamCaps来枚举媒体类型,要给这个函数传递一个序号作为参数,这个函数返回媒体类型和相应的属性结构体。看代码把

 int iCount = 0, iSize = 0;   hr = pConfig->GetNumberOfCapabilities(&iCount, &iSize); // Check the size to make sure we pass in the correct structure.   if (iSize == sizeof(VIDEO_STREAM_CONFIG_CAPS)   {   // Use the video capabilities structure.   for (int iFormat = 0; iFormat < iCount; iFormat++)   {   VIDEO_STREAM_CONFIG_CAPS scc;   AM_MEDIA_TYPE *pmtConfig;   hr = pConfig->GetStreamCaps(iFormat, &pmtConfig, (BYTE*)&scc);   if (SUCCEEDED(hr))   {   /* Examine the format, and possibly use it. */   // Delete the media type when you are done.   hr = pConfig->SetFormat(pmtConfig);//重新设置视频格式   DeleteMediaType(pmtConfig);   }   }

你可以调用IAMStreamConfig::SetFormat设置新的媒体类型 hr = pConfig->SetFormat(pmtConfig); 如果pin没有连接,当连接的时候就试图用新的格式,如果pin已经在连接了,它就会用的新的媒体格式重新连接。在任何一种情况下,下游的filter都有可能拒绝新的媒体格式。 在SetFormat前你可以修改VIDEO_STREAM_CONFIG_CAPS结构来重新设置媒体类型。 例如: 如果GetStreamCaps返回的是24-bit RGB format,桢的大小是320 x 240 像素,你可以通过检查媒体类型的major type,subtpye,和format等值

 if ((pmtConfig.majortype == MEDIATYPE_Video) &&   (pmtConfig.subtype == MEDIASUBTYPE_RGB24) &&   (pmtConfig.formattype == FORMAT_VideoInfo) &&   (pmtConfig.cbFormat >= sizeof (VIDEOINFOHEADER)) &&   (pmtConfig.pbFormat != NULL))   {   VIDEOINFOHEADER *pVih = (VIDEOINFOHEADER*)pmtConfig.pbFormat;   // pVih contains the detailed format information.   LONG lWidth = pVih->bmiHeader.biWidth;   LONG lHeight = pVih->bmiHeader.biHeight;   }

VIDEO_STREAM_CONFIG_CAPS结构里包含了该媒体类型的视频长度和宽度的最大值和最小值,还有递增的幅度值,就是每次调整视频size的幅度,例如,设备可能返回如下的值 ? MinOutputSize: 160 x 120 ? MaxOutputSize: 320 x 240 ? OutputGranularityX: 8 pixels (horizontal step size) ? OutputGranularityY: 8 pixels (vertical step size) 这样你可以在(160, 168, 176, ... 304, 312, 320) 范围内设置宽度,在 (120, 128, 136, ... 104, 112, 120).设置高度值,

图6 如果想设置新的值,直接修改在GetStreamCaps函数中返回的值即可,

 pVih->bmiHeader.biWidth = 160;   pVih->bmiHeader.biHeight = 120;   pVih->bmiHeader.biSizeImage = DIBSIZE(pVih->bmiHeader);

然后将媒体类型传递给SetFormat函数,就可修改视频格式了。

7将设备从系统中移走时的事件通知(Device remove Notify)

如果用户将一个graph正在使用的即插即用型的设备从系统中去掉,filter图表管理器就会发送一个EC_DEVICE_LOST事件通知,如果该设备又可以使用了,filter图表管理器就发送另外的一个EC_DEVICE_LOST通知,但是先前组建的捕捉filter graph图就没法用了,用户必须重新组建graph图。 当系统中有新的设备添加时,dshow是不会发送任何通知的,所以,应用程序如果想要知道系统中何时添加新的设备,应用程序可以监控WM_DEVICECHANGE消息。

8从静止图像pin中捕捉图片

有些照相机,摄像头除了可以捕获视频流以外还可以捕获单张的,静止的图片。通常,静止的图片的质量要比流的质量要高。摄像头一般都一个按钮来触发,或者是支持软件触发。支持输出静态图片的摄像头一般都要提供一个静态图片pin,这个pin的种类是PIN_CATEGORY_STILL。 从设备中获取静态图片,我们一般推荐使用windows Image Acquisition (WIA) APIs。当然,你也可以用dshow来获取图片。 在graph运行的时候利用IAMVideoControl::SetMode来触发静态的pin。代码如下

 pControl->Run(); // Run the graph.   IAMVideoControl *pAMVidControl = NULL;   hr = pCap->QueryInterface(IID_IAMVideoControl, (void**)&pAMVidControl);   if (SUCCEEDED(hr))   {   // Find the still pin.   IPin *pPin = 0;   hr = pBuild->FindPin(pCap, PINDIR_OUTPUT, &PIN_CATEGORY_STILL, 0,   FALSE, 0, &pPin);   if (SUCCEEDED(hr))   {   hr = pAMVidControl->SetMode(pPin, VideoControlFlag_Trigger);   pPin->Release();   }   pAMVidControl->Release();   }

首先向capture Filter 请求IAMVideoContol,如果支持该接口,就调用ICaptureGraphBuilder2::FindPin请求指向静止pin 的指针,然后调用pin的put_Mode方法。 根据不同的摄像头,你可能静态pin连接前要render 该pin。 捕捉静态图片常用的filter是Sample Grabber filter,Sample Grabber使用了一个用户定义的回调汗水来处理图片。关于这个filter的详细用法,参见Using the Sample Grabber.。 下面的例子假设静态pin传递的是没有压缩的RGB图片。首先定义一个类,从ISampleGrabberCB继承。

 // Class to hold the callback function for the Sample Grabber filter.   class SampleGrabberCallback : public ISampleGrabberCB   {   // Implementation is described later.   } // Global instance of the class.   SampleGrabberCallback g_StillCapCB;

然后将捕捉filter的静态pin连接到Sample Grabber,将Sample Grabber连接到Null Renderer filter。Null Renderer仅仅是将她接收到的sample丢弃掉。实际的工作都是在回调函数里进行,连接Null Renderer 仅仅是为了给Sample Grabber's 输出pin上连接点东西。具体见下面的代码

  // Add the Sample Grabber filter to the graph.   IBaseFilter *pSG_Filter;   hr = CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER,   IID_IBaseFilter, (void**)&pSG_Filter);   hr = pGraph->AddFilter(pSG_Filter, L"SampleGrab"); // Add the Null Renderer filter to the graph.   IBaseFilter *pNull;   hr = CoCreateInstance(CLSID_NullRendere, NULL, CLSCTX_INPROC_SERVER,   IID_IBaseFilter, (void**)&pNull);   hr = pGraph->AddFilter(pSG_Filter, L"NullRender");

然后通过RenderStream将still pin ,sample grabber ,null Renderer连接起来

 hr = pBuild->RenderStream(   &PIN_CATEGORY_STILL, // Connect this pin ...   &MEDIATYPE_Video, // with this media type ...   pCap, // on this filter ...   pSG_Filter, // to the Sample Grabber ...   pNull); // ... and finally to the Null Renderer.

然后调用ISampleGrabber指针,来通过这个指针可以分配内存。

  // Configure the Sample Grabber.   ISampleGrabber *pSG;   hr = pSG_Filter->QueryInterface(IID_ISampleGrabber, (void**)&pSG);   pSG->SetOneShot(FALSE);   pSG->SetBufferSamples(TRUE);

设置你的回调对象

 pSG->SetCallback(&g_StillCapCB, 0); // 0 = Use the SampleCB callback   method

获取静态pin和sample grabber之间连接所用的媒体类型

 // Store the media type for later use.   AM_MEDIA_TYPE g_StillMediaType;   hr = pSG->GetConnectedMediaType(&g_StillMediaType);   pSG->Release();

媒体类型包含一个BITMAPINFOHEADER结构来定义图片的格式,在程序退出前一定要释放媒体类型

 // On exit, remember to release the media type.   FreeMediaType(g_StillMediaType);

看看下面的回调类吧。这个类从ISampleGrabber接口派生,但是它没有保持引用计数,因为应用程序在堆上创建这个对象,在整个graph的生存周期它都存在。 所有的工作都在BufferCB函数里完成,当有一个新的sample到来的时候,这个函数就会被sample Grabber调用到。在下面的例子里,bitmap被写入到一个文件中

 class SampleGrabberCallback : public ISampleGrabberCB   {   public:   // Fake referance counting.   STDMETHODIMP_(ULONG) AddRef() { return 1; }   STDMETHODIMP_(ULONG) Release() { return 2; }  STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject)   {   if (NULL == ppvObject) return E_POINTER;   if (riid == __uuidof(IUnknown))   {   *ppvObject = static_cast<IUnknown*>(this);   return S_OK;   }   if (riid == __uuidof(ISampleGrabberCB))   {   *ppvObject = static_cast<ISampleGrabberCB*>(this);   return S_OK;   }   return E_NOTIMPL;   }  STDMETHODIMP SampleCB(double Time, IMediaSample *pSample)   {   return E_NOTIMPL;   }  STDMETHODIMP BufferCB(double Time, BYTE *pBuffer, long BufferLen)   {   if ((g_StillMediaType.majortype != MEDIATYPE_Video) ||   (g_StillMediaType.formattype != FORMAT_VideoInfo) ||   (g_StillMediaType.cbFormat < sizeof(VIDEOINFOHEADER)) ||   (g_StillMediaType.pbFormat == NULL))   {   return VFW_E_INVALIDMEDIATYPE;   }   HANDLE hf = CreateFile("C://Example.bmp", GENERIC_WRITE,   FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, 0, NULL);   if (hf == INVALID_HANDLE_VALUE)   {   return E_FAIL;   }   long cbBitmapInfoSize = g_StillMediaType.cbFormat - SIZE_PREHEADER;   VIDEOINFOHEADER *pVideoHeader =   (VIDEOINFOHEADER*)g_StillMediaType.pbFormat;  BITMAPFILEHEADER bfh;   ZeroMemory(&bfh, sizeof(bfh));   bfh.bfType = 'MB'; // Little-endian for "MB".   bfh.bfSize = sizeof( bfh ) + BufferLen + cbBitmapInfoSize;   bfh.bfOffBits = sizeof( BITMAPFILEHEADER ) + cbBitmapInfoSize;     // Write the file header.   DWORD dwWritten = 0;   WriteFile( hf, &bfh, sizeof( bfh ), &dwWritten, NULL );   WriteFile(hf, HEADER(pVideoHeader), cbBitmapInfoSize, &dwWritten, NULL);     WriteFile( hf, pBuffer, BufferLen, &dwWritten, NULL );   CloseHandle( hf );   return S_OK;  }   };

作者Blog:http://blog.csdn.net/dankes/  

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

数字治理作为数字经济时代政府治理现代化的重要方向,强调利用数字技术优化政府组织运行机制、促进政务数据共享、提升公共服务效率,实现由传统行政管理向数据驱动型治理转变 2014年,国家启动“信息惠民国家试点城市建设”,选择80个城市开展试点,核心内容包括:建设政务数据平台、推动数据共享、推动“一网通办”等,是我国数字治理实践的重要探索 本文借鉴刘奥龙等(2026)的研究思路和方法,将信息惠民国家试点城市建设作为数字治理的准自然实验,衡量各城市政府数字治理,整理形成地级市信息惠民政策试点DID数据,并进一步构建省份数字治理程度指标数据,为数字治理经济效应研究提供数据支持 具体整理过程如下: 1.城市政府数字治理:采用信息惠民国家试点城市冲击来衡量各城市政府数字治理,城市若成为信息惠民国家试点城市取值为1, 表明城市推行政府数字治理,否则为0 2.省份数字治理程度:选择各省份内信息惠民国家试点城市数量占省内所有城市的比值来衡量省份整体推进数字治理的程度 一、数据介绍 数据名称:政府数字治理程度_省+地级市 数据范围:省份、地级市 时间范围:2000-2025年 样本数量:省份806条;地级市7722条 数据来源:国家发展改革委 数据说明:含信息惠民试点城市名单、省份数字治理程度、地级市DID明细数据等 二、数据指标 年份 省份 省份代码 所属地域 省内城市总数量 当年实施试点城市数量 数字治理程度 年份 省份 城市 省份代码 城市代码 所属地域 胡焕庸线 “信息惠民”试点时间 Treat Post DID 三、参考文献 [1]刘奥龙,马亦凡,高娜娜.打破创新藩篱:数字治理能否推动区域协同创新[J].山西财经大学学报,2026,48(3):26-38.
内容概要:本文以有源中点箝位(ANPC)三电平并网逆变器为研究对象,提出并构建了一套融合双极性倍频脉宽调制(DPWMA)、正负序分离锁相控制与电网电压前馈的一体化高性能并网控制策略。通过深入分析ANPC三电平拓扑在开关损耗均衡、中点电位可控性及输出谐波低等方面的结构优势,确立了其作为大功率高质量并网系统的硬件基础。在此基础上,DPWMA调制策略有效提升等效开关频率,显著降低输出电流电压的总谐波畸变率,优化稳态电能质量;正负序分离锁相技术精准剥离电网电压中的负序扰动分量,保障电网不平衡工况下的相位同步精度与并网电流对称性;电网电压前馈控制则通过前瞻性补偿机制,突破传统闭环控制的响应滞后瓶颈,大幅提升系统在电压骤变、畸变等动态扰动下的抗扰能力与动态响应速度。研究通过搭建完整的Simulink仿真模型,在稳态对称、电网不平衡及动态切换等多种工况下进行全面验证,结果表明该复合控制策略在电能质量、运行稳定性与工况适应性方面均具有显著优越性。; 适合人群:具备电力电子、自动控制或新能源并网等相关专业背景,从事科研、工程开发或处于研究生及以上学习阶段的专业技术人员。; 使用场景及目标:①应用于光伏发电、风力发电等新能源系统的大功率并网逆变器高性能控制设计;②解决电网电压不平衡、畸变等复杂非理想工况下的并网稳定性与电能质量问题;③提升工业级并网设备的动态响应速度与运行可靠性;④为相关领域的仿真建模、控制算法开发与性能优化提供系统性的技术参考与实现方案。; 阅读建议:建议结合文中所述的Simulink仿真模型进行实践操作,重点深入理解DPWMA调制的具体实现逻辑、正负序分离锁相环的设计原理以及前馈-反馈复合控制结构的集成方法,通过设置不同工况的对比仿真实验,直观体会各项关键技术对系统整体性能的提升作用。
内容概要:本文研究了离网光伏直流微网中功率供需失衡的抑制机制,重点探讨光伏最大功率点跟踪(MPPT)技术与锂离子电池储能系统在“削峰填谷”中的协同控制策略,并通过Simulink仿真平台搭建了完整的光伏-储能-负载系统模型进行验证。系统由光伏阵列、Boost升压电路、双向DC-DC变换器及锂电池储能单元构成,通过MPPT实时捕获光伏最大输出功率,同时利用储能系统的双向充放电能力平抑功率波动,实现能量的时空转移与动态平衡。研究涵盖系统建模、控制逻辑设计与多工况仿真分析,全面验证了该协同机制在应对光照强度变化和负载突变等不确定因素时的有效性与鲁棒性,显著提升了微网在离网条件下的能量自给能力和运行稳定性。; 适合人群:具备电力电子、新能源或自动控制基础知识,从事微电网、光伏发电或储能系统相关研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①掌握离网光伏直流微网的基本架构与能量管理原理;②学习MPPT算法与储能双向充放电控制的协同设计方法;③通过Simulink仿真复现并优化系统性能,应用于科研项目或工程原型开发。; 阅读建议:该资源以Simulink仿真实现为核心,注重理论与实践结合,建议读者在理解控制策略的基础上动手搭建模型,重点关注MPPT模块与储能控制模块的接口逻辑及参数整定,结合实际光照与负荷数据进行仿真测试以深化理解。
内容概要 本资源是一套完整可运行的 Qt Widgets 批量图片压缩桌面工具源码,基于 Qt5/C++ 从零开发,专为初学者设计,分步实现图片批量处理全套功能。工具支持多选单张图片、直接读取整个文件夹内所有 JPG/PNG 图像,可自定义输出图片分辨率、调节 JPG0~100 区间压缩质量,自带锁定宽高比防拉伸变形功能;批量处理完成后自动统计每张图片压缩前后文件体积,计算整体压缩缩小比例,直观展示压缩效果。 适用人群 Qt/C++ 零基础初学者,学习 QImage 图像绘图、文件目录遍历、UI 交互开发; 需要本地批量处理图片的办公、设计、自媒体从业者; 想要学习图片缩放、JPG 压缩、本地文件 IO、进度条交互的开发学习者。 使用场景 自媒体批量压缩配图,降低图片体积节省上传流量; 摄影、设计批量统一图片尺寸,批量轻量化相册图片; 程序开发学习:QFileDialog 文件选择、QDir 文件夹遍历、QImage 缩放保存、QSlider 参数联动、批量循环界面防卡顿、文件大小格式化转换全套 Qt 图像开发实战案例。 工具核心功能清单 双模式导入图片:手动多选单张图片 / 一键读取整个文件夹全部图片; 自定义输出宽高分辨率,支持锁定原始宽高比,避免图片拉伸变形; 滑块调节 JPG 压缩质量 0~100,平衡图片清晰度与文件占用大小; 自定义输出保存目录,批量生成压缩后的图片文件; 实时进度条展示处理进度,循环中刷新界面,程序不会假死卡顿; 自动统计每张图片压缩前后体积,换算 KB/MB 直观展示; 批量完成弹窗汇总:图片总数、成功数量、单张大小对比、整体压缩节省空间比例; 完整模块化代码,功能拆分清晰,每段代码附带详细注释,新手可分步拆解学习。 其他说明 开发环境:Qt Creator + Qt5.15 MSVC,Windows 平台可直接编译运行; 源码结构清晰,功能
内容概要:本文研究了基于有源中点箝位(ANPC)的三电平并网逆变器,提出了一种融合双极性倍频脉宽调制(DPWMA)、正负序分离锁相及电网电压前馈控制的一体化高性能并网控制策略。文章首先分析了ANPC三电平逆变器的拓扑结构及其在开关损耗均衡、输出波形质量、中点电位稳定等方面的优越性,奠定了高性能并网的硬件基础。随后,重点阐述了DPWMA调制策略如何通过等效倍频效应优化输出波形、降低谐波含量;正负序分离锁相技术如何在电网不平衡工况下精准提取正序分量,保障锁相精度与并网对称性;电网电压前馈控制如何提前补偿电网扰动,提升系统的动态响应与抗扰能力。通过Simulink仿真模型对稳态、电网不平衡及动态工况进行了全面验证,结果表明该复合控制策略能显著提升并网电能质量、稳定性和工况适应性,适用于新能源并网等大功率应用场景。; 适合人群:电力电子、新能源并网、自动化及相关领域的研究生、科研人员及从事逆变器控制算法开发的工程技术人员。; 使用场景及目标:① 研究三电平ANPC逆变器的拓扑优势与控制难点;② 掌握DPWMA调制、正负序分离、电网前馈等先进控制技术的原理与协同机制;③ 解决电网不平衡、动态扰动等复杂工况下的并网稳定性问题,提升系统电能质量和鲁棒性。; 阅读建议:建议结合文中提到的Simulink仿真模型进行实践验证,重点关注控制策略在不同工况下的波形响应与性能指标变化,深入理解各模块间的耦合关系与优化逻辑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值