首页 > 其他分享 >SkeyeLive中DirectShow采集音视频流程及几种采集方式介绍

SkeyeLive中DirectShow采集音视频流程及几种采集方式介绍

时间:2023-02-24 15:32:52浏览次数:65  
标签:视频 DirectShow return FAILED hr 音视频 采集 NULL

前段时间SkeyeLive开放了DirectShow采集库,这个库底层采用DirectShow SDK的接口实现音视频的预览(播放)和采集;很多人可能还不太了解这个封装库的回调方式和之前的DShow线程采集方式有什么不同,或者说对DirectShow的采集流程还不太熟悉,下面我将就Windows平台下用使用DirectShow的过滤器(滤波器)进行流媒体开发的前端采集部分进行简要介绍,如果大家想深入的学习和探索,推荐大家去看看​​《Visual C++音频/视频处理技术及工程实践》​​这本书,第9章有详细的流程讲解。

一、枚举采集设备

使用采集设备前,需要首先确定系统已经安装的采集设备:视频、音频采集设备。系统设备枚举器为按类型枚举已注册在系统中的滤波器提供了统一的方法。而且它能够区分不同的硬件设备,即便是同一个滤波器支持它们。这对那些使用Windows驱动模型、KSProxy Filter的设备来说是非常有用的,系统设备枚举器对它们按不同的设备实例进行对待。

当利用系统设备枚举器查询设备的时候,系统设备枚举器为特定类型的设备(如音频捕获和视频压缩)生成了一张枚举表(Enumerator)。类型枚举器(Category Enumerator)为每个这种类型的设备返回一个Moniker,类型枚举器自动把每种即插即用的设备包含在内。

调用标准方法CoCreateInstance生成系统设备枚举器(Device Enumerator),类标识(CLSID)为CLSID_SystemDeviceEnum,方法如下:

CAMERA_LIST_T *CCameraDS::GetCameraList()
{
if (NULL != cameraList.pCamera || cameraList.count > 0)
{
return &cameraList;
}

if (NULL == cameraList.pCamera)
{
cameraList.count = 0;

// enumerate all video capture devices
CComPtr<ICreateDevEnum> pCreateDevEnum;
HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER,
IID_ICreateDevEnum, (void**)&pCreateDevEnum);

CComPtr<IEnumMoniker> pEm;
hr = pCreateDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEm, 0);
if (hr != NOERROR)
{
return &cameraList;
}

pEm->Reset();

CAMERA_INFO_T *pCameraList = cameraList.pCamera;
CAMERA_INFO_T *pCameraInfo = NULL;

ULONG cFetched;
IMoniker *pM = NULL;
while(hr = pEm->Next(1, &pM, &cFetched), hr==S_OK)
{
pCameraInfo = new CAMERA_INFO_T;
memset(pCameraInfo, 0x00, sizeof(CAMERA_INFO_T));

IPropertyBag *pBag=0;
hr = pM->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pBag);
if(SUCCEEDED(hr))
{
VARIANT var;
var.vt = VT_BSTR;
hr = pBag->Read(L"FriendlyName", &var, NULL); //还有其他属性,像描述信息等等...
if(hr == NOERROR)
{
//获取设备名称
WideCharToMultiByte(CP_ACP,0,var.bstrVal,-1,pCameraInfo->friendlyName, sizeof(pCameraInfo->friendlyName) ,"",NULL);
SysFreeString(var.bstrVal);
}
pBag->Release();
cameraList.count++;

{
pCameraList = cameraList.pCamera;

if (NULL == cameraList.pCamera) cameraList.pCamera = pCameraInfo;
else
{
while (NULL != pCameraList->pNext)
{
pCameraList = pCameraList->pNext;
}
pCameraList->pNext = pCameraInfo;
}
}
}

pM->Release();
}

pCreateDevEnum = NULL;
pEm = NULL;
}
return &cameraList;
}

这是视频设备枚举部分,当然音频也是一样的,只需要把CreateClassEnumerator函数的第一个参数换成CLSID_AudioInputDeviceCategory即可。 注意:调用ICreateDevEnum::CreateClassEnumerator方法生成类型枚举器,参数为用户想要得到的类的ID(CLSID),该方法返回一个IEnumMoniker接口指针。如果指定的类型是空的或不存在,则函数ICreateDevEnum::CreateClassEnumerator将返回S_FALSE而不是错误代码,同时IEnumMoniker指针也是空的,这就要求我们在调用CreateClassEnumerator的时候明确用S_OK进行比较而不使用宏SUCCEEDED;(扯远了...)而在SkeyeLive中还提供了另外一种枚举音频设备的方式,那就是采用DirectSound的枚举方式:DirectSoundCaptureEnumerate这个函数来实现的,需要注意,这个函数枚举出的设备GUID有可能是空的,设备名称可能表象为”声卡主设备驱动“,经测试:这个设备是不能用于采集,也是不存在的,枚举过程中应该丢弃。当然,其实DirectShow也是封装了底层的DirectSound的接口来实现的COM接口的统一封装。 (需要重点说明的是:枚举设备这一块不是DShow封装库中的代码,这是由我们EasyDarwin团队的Gavin大神之前的DShow采集部分代码中提供的(前身是EasyCamera_win),我只是鸠占鹊巢的给大家讲解,向大神致敬~~~~~~~哈哈哈!)

二、使用Capture Graph Builder进行音视频采集

这个为了节约篇幅,本文以视频采集为例子进行讲解,其实音频采集是一模一样的(这就是封装的好处,不用关心底层的实现细节); 1、创建GraphBuilder 使用DirectShow进行视频采集,首先,创建视频捕获Graph,DShow SDK提供的是Graph Builder接口是IgraphBuilder。不过针对捕获任务(Capture),还有另一个接口ICaptureGraphBuilder2针对采集捕获的增强型接口,这个接口可以提供视频捕获预览窗口的创建和使用,然后,再创建一个媒体控制器对视频预览的播放进行控制,代码如下:

//创建视频捕获Graph
HRESULT CCaptureVideo::CreateCaptureGraphBuilder()
{
HRESULT hr=NOERROR;

if(m_pGraphBuilder==NULL)
{
hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&m_pGraphBuilder);
if(FAILED(hr))
{
//ERR_DEBUG("CreateCaptureGraphBuilder Create m_pGraphBuilder Failed");
return hr;
}
}
if(m_pCaptureGraphBulid==NULL)
{
//// 创建ICaptureGraphBuilder2接口,即创建视频捕获窗
hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC,
IID_ICaptureGraphBuilder2, (void **) &m_pCaptureGraphBulid);
if (FAILED(hr))
{
//ERR_DEBUG("CreateCaptureGraphBuilder CaptureGraphBuilder2 Failed");
return hr;
}
//将捕获窗的视频属性设为定义好的视频窗
//给captureGrap builder指定一个图象filter,不能由于混合视频的呈现,video端口的管理
hr = m_pCaptureGraphBulid->SetFiltergraph(m_pGraphBuilder);
}

//此处可能存在问题是否为一个链路就一个m_pMediaCon媒体控制//QueryInterface
if(m_pMediaCon==NULL)
{
hr = m_pGraphBuilder->QueryInterface(IID_IMediaControl, (void **)&m_pMediaCon);
if (FAILED(hr))
{
//ERR_DEBUG("CreateCaptureGraphBuilder QueryInterface m_pMediaCon Failed");
return hr;
}
}

if(m_pMediaEvent==NULL)
{
hr = m_pGraphBuilder->QueryInterface(IID_IMediaEvent, (void **) &m_pMediaEvent);
if (FAILED(hr))
{
//ERR_DEBUG("CreateCaptureGraphBuilder QueryInterface m_pMediaEvent Failed");
return hr;
}
}

return hr;
}

2、枚举设备并连接设备 枚举设备我们在上一节已经讲过了,这里直接查询到自己需要捕获的设备名称,然后绑定到过滤器上即可:

//枚举设备并绑定设备
BOOL CCaptureVideo::BindToVideoDev(int deviceId, IBaseFilter **pFilter)
{
if (deviceId < 0)
{
return FALSE;
}

CComPtr<ICreateDevEnum> pCreateDevEnum;
HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER,
IID_ICreateDevEnum, (void**)&pCreateDevEnum);
if (hr != NOERROR)
{
//ERR_DEBUG("Instance DeviceEnum Failed");
return FALSE;
}
CComPtr<IEnumMoniker> pEm;
hr = pCreateDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,&pEm, 0);
if (hr != NOERROR)
{
//ERR_DEBUG("Enum VideoInputDeviceCategory Failed");
return FALSE;
}
pEm->Reset();
ULONG cFetched;
IMoniker *pM=NULL;

int index = 0;
while((( pEm->Next(1, &pM, &cFetched))==S_OK)&&( index <= deviceId))
{
IPropertyBag *pBag=NULL;
if (pM==NULL)
{
break;
}
hr = pM->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pBag);
if(pBag!=NULL)
{
VARIANT var;
var.vt = VT_BSTR;
hr = pBag->Read(L"FriendlyName", &var, NULL);
if (hr == NOERROR)
{
if (index == deviceId)
{
//将视频设备绑定到基础过滤器上
pM->BindToObject(0, 0, IID_IBaseFilter, (void**)pFilter);
}
SysFreeString(var.bstrVal);
pBag->Release();
}

}
pM->Release();
index++;
}
return TRUE;
}

3、采集参数的设置

采集前需要对要采集的视频格式、图像质量进行设置,如视频的分辨率、帧率和数据格式,图像的亮度、色度和饱和度参数设置等。当然,我们这里只针对视频的宽高,帧率和数据格式进行了设置,如果大家还想进行更多的设置,可以使用OleCreatePropertyFrame函数以属性页的方式对视频属性和图像参数进行配置和修改。

HRESULT CCaptureVideo::SetVideoSize(int nPreview,CString strRGBBytes,int nFrameRate,int iWidth , int iHeight)
{
HRESULT hr=E_FAIL;
if(m_pCaptureGraphBulid==NULL)
return hr;

IAMStreamConfig *pAMStreamConfig=NULL;
if(nPreview==0)
{
hr = m_pCaptureGraphBulid->FindInterface(&PIN_CATEGORY_PREVIEW,&MEDIATYPE_Video,
m_pBaseFilter,IID_IAMStreamConfig,(void **)&pAMStreamConfig);
}
else
{
hr = m_pCaptureGraphBulid->FindInterface(&PIN_CATEGORY_CAPTURE,&MEDIATYPE_Video,
m_pBaseFilter,IID_IAMStreamConfig,(void **)&pAMStreamConfig);
}

if(FAILED( hr ))
{
SAFE_RELEASE(pAMStreamConfig);
return hr;
}
//得到视频格式大小
AM_MEDIA_TYPE *pmt;
pAMStreamConfig->GetFormat(&pmt);

//设置视频格式
pmt->majortype = MEDIATYPE_Video;
pmt->subtype = GetMediaTypeGuid(strRGBBytes);


VIDEOINFOHEADER *pvih = reinterpret_cast<VIDEOINFOHEADER *>(pmt->pbFormat);
//设置回去
int nDefualWidth = pvih->bmiHeader.biWidth;
int nDefualHeight = pvih->bmiHeader.biHeight;

pvih->bmiHeader.biWidth = iWidth;
pvih->bmiHeader.biHeight = iHeight;
pvih->bmiHeader.biSizeImage = pmt->lSampleSize = iWidth*iHeight*pvih->bmiHeader.biPlanes*pvih->bmiHeader.biBitCount/8;
pvih->AvgTimePerFrame = (LONGLONG)(10000000/nFrameRate);

hr = pAMStreamConfig->SetFormat(pmt);
if(FAILED(hr))
{
//如果设置失败可以选用默认的,但运用之后,小屏幕初始化时会出现闪动的情况
pvih->bmiHeader.biWidth = nDefualWidth;
pvih->bmiHeader.biHeight = nDefualHeight;
pvih->bmiHeader.biSizeImage = pmt->lSampleSize = nDefualWidth*nDefualHeight*pvih->bmiHeader.biPlanes*pvih->bmiHeader.biBitCount/8;
hr = pAMStreamConfig->SetFormat(pmt);

if(FAILED(hr))
{
SAFE_RELEASE(pAMStreamConfig);
FreeMediaType(*pmt);
//ERR_DEBUG("初始化设置视频格式失败");
return hr;
}
}
SAFE_RELEASE(pAMStreamConfig);
FreeMediaType(*pmt);//
return hr;
}

4、建立视频渲染器 创建视频渲染器(呈现器),对捕获视频进行显示,代码如下:

HRESULT CCaptureVideo::CreateVideoRender(int nType)
{
HRESULT hr = NOERROR;
if(m_pWindowRender)
{
SAFE_RELEASE(m_pWindowRender);
}
//创建解码器filter,CLSID_VideoRendererDefault,//
if(nType==0)
{
SAFE_RELEASE(m_pWindowRender);
return NOERROR;
}
else if(nType==1)
{

hr = CoCreateInstance(CLSID_VideoRendererDefault,0,CLSCTX_INPROC_SERVER,
IID_IBaseFilter,(void **)&m_pWindowRender);

}
else if(nType==2)
{
hr = CoCreateInstance(CLSID_VideoRenderer,0,CLSCTX_INPROC_SERVER,
IID_IBaseFilter,(void **)&m_pWindowRender);

}
else if(nType==3)//不显示
{
hr = CoCreateInstance(CLSID_NullRenderer,0,CLSCTX_INPROC_SERVER,
IID_IBaseFilter,(void **)&m_pWindowRender);
}
else
{
SAFE_RELEASE(m_pWindowRender);
return NOERROR;
}

if(FAILED(hr))
{
SAFE_RELEASE(m_pWindowRender);
//ERR_DEBUG("接收创建呈现器失败");
return hr;
}
if(m_pGraphBuilder)
{
hr = m_pGraphBuilder->AddFilter(m_pWindowRender,L"recv render");
if(FAILED(hr))
{
SAFE_RELEASE(m_pWindowRender);
//ERR_DEBUG("加入接收创建呈现器失败");
return hr;
}
}
return hr;
}

5、预览采集到的视频数据

首先,初始化过滤器链路管理器,把指定采集设备的过滤器添加到链路中,然后渲染RenderStream方法把所有的过滤器链接起来,最后根据设定的显示窗口预览采集到的视频数据,具体实现过程如下:

hr = CreateCaptureSampleGrabber(m_strRGBByte);
if(FAILED(hr))
{
SAFE_RELEASE(m_pSampleGrabberFilter);
SAFE_RELEASE(m_pSampleGrabber);
//ERR_DEBUG("CreateCaptureSampleGrabber Failed");
return -1;
}
if(m_nDeinterlace==1)//m_iHeight/m_iWidth!=(1.5/4))
{
CreateDeinterlaceFilter();
}
if(m_pVideoDeinterlaceFilter)
{
hr = m_pCaptureGraphBulid->RenderStream(&pCategorySuc,&MEDIATYPE_Video,m_pBaseFilter,m_pVideoDeinterlaceFilter,m_pSampleGrabberFilter);
hr = m_pCaptureGraphBulid->RenderStream(NULL,NULL,m_pSampleGrabberFilter,NULL,m_pWindowRender);
if(FAILED(hr))
{
hr = m_pCaptureGraphBulid->RenderStream(&PIN_CATEGORY_PREVIEW,&MEDIATYPE_Video,m_pBaseFilter,m_pSampleGrabberFilter,NULL);
if(FAILED(hr))
{
//ERR_DEBUG("PrivewVideoDev RenderStream Failed ");
return -1;
}
}
}
else
{
hr = m_pCaptureGraphBulid->RenderStream(&pCategorySuc,&MEDIATYPE_Video,m_pBaseFilter,m_pSampleGrabberFilter,m_pWindowRender);
if(FAILED(hr))
{
hr = m_pCaptureGraphBulid->RenderStream(&pCategoryFail,&MEDIATYPE_Video,m_pBaseFilter,m_pSampleGrabberFilter,m_pWindowRender);
if(FAILED(hr))
{
//ERR_DEBUG("PrivewVideoDev RenderStream Failed ");
return -1;
}
}
}

if(m_bThread)
{
m_pSampleGrabber->SetOneShot(FALSE);
m_pSampleGrabber->SetBufferSamples(TRUE);
// m_pSampleGrabber->SetOneShot(TRUE);

}
else
{
m_pSampleGrabber->SetBufferSamples(TRUE);
m_pSampleGrabber->SetOneShot(FALSE);
//m_nDataType:数据类型1--音频,2--视频数据
//nIndex:设备编号:音频-1,视频0---N
m_cSampleGrabberCB.SetDataInfo(m_nIndex, m_nDataType);
int nMode=1;//0--SampleCB,1--BufferCB
m_pSampleGrabber->SetCallback(&m_cSampleGrabberCB, nMode);
}

if(m_pVideoWin==NULL&&m_nRenderType!=2)
{
hr = m_pCaptureGraphBulid->FindInterface(&pCategorySuc,&MEDIATYPE_Video,//CAPTURE
m_pBaseFilter,IID_IVideoWindow,(void **)&m_pVideoWin);
if (FAILED(hr))//建立失败则查找CAPTURE口
{
hr = m_pCaptureGraphBulid->FindInterface(&pCategoryFail,&MEDIATYPE_Video,//CAPTURE
m_pBaseFilter,IID_IVideoWindow,(void **)&m_pVideoWin);
if (FAILED(hr))
{
//ERR_DEBUG("CreateCaptureGraphBuilder QueryInterface m_pVideoWin Failed");
return -1;
}
}
}

//设置视频显示窗口
SetupVideoWindow();
hr = StartPreview();
if(FAILED(hr))
{
return -1;
}

然后,设置视频显示窗口:

//设置窗口句柄,并自动更新显示大小
HRESULT CCaptureVideo::SetupVideoWindow(HWND hWnd)
{
HRESULT hr=NOERROR;

if(m_pVideoWin==NULL)
{
return hr;
}

if(hWnd!=NULL)
m_hWnd = hWnd;

hr = m_pVideoWin->put_Owner((OAHWND)m_hWnd);
// hr = m_pVideoWin->put_MessageDrain((OAHWND)m_hWnd);
if (FAILED(hr))
{
//ERR_DEBUG("SetupVideoWindow put_Owner Error");
return hr;
}
hr = m_pVideoWin->put_WindowStyle(WS_CHILD |SS_NOTIFY| WS_CLIPCHILDREN);
if (FAILED(hr))
{
//ERR_DEBUG("SetupVideoWindow put_WindowStyle");
return hr;
}

ResizeVideoWindow();
hr = m_pVideoWin->put_Visible(OATRUE);

return hr;
}

最后,开始预览:

HRESULT CCaptureVideo::StartPreview()
{
HRESULT hr=NOERROR;
if(m_pMediaCon==NULL)
{
hr = m_pGraphBuilder->QueryInterface(IID_IMediaControl, (void **)&m_pMediaCon);
if(SUCCEEDED(hr))
{
hr = m_pMediaCon->Run();
if(FAILED(hr))
{
m_pMediaCon->Stop();
}
}
}
else
{
hr = m_pMediaCon->Run();
if(FAILED(hr))
{
m_pMediaCon->Stop();
}
}
return hr;
}

至此,DShow对视频的采集整个完整的流程就完成了。

三、DShow采集的两种模式 1、线程模式(拉模式) 线程模式采用多线程的方式,在线程回调中调用GetCurrentBuffer函数获取采集缓存中的一帧数据,这里获取的数据是之前设置的色彩格式的数据(如果设置成功的话,否则是默认格式);如果要得到指定的格式需要进行色彩格式转换。 线程执行函数如下:

void CCaptureVideo::OnThreadDeal()
{
BYTE *pData=NULL;
long lDatasize=0;
char strMediaType[24]=_T("YUY2");
if(!m_strRGBByte.IsEmpty())
{
strcpy_s(strMediaType, 24, m_strRGBByte);
}
//读取缓冲区数据
pData=GetCurrentBuffer(lDatasize,strMediaType);
//TRACE("OnThreadDeal:%d\r\n",lDatasize);
if (m_sThreadCalbackInfo.realDataCalFunc&&m_sThreadCalbackInfo.pMaster&&pData&&lDatasize>0)
{
//执行数据回调
m_sThreadCalbackInfo.realDataCalFunc(m_nIndex, pData, lDatasize,
(RealDataStreamType)m_nDataType,NULL, m_sThreadCalbackInfo.pMaster);
}
}

2、回调模式(推模式) 回调模式是利用ISampleGrabber提供的回调函数接口进行设置,该设置回调函数原型如下:

virtual HRESULT STDMETHODCALLTYPE ISampleGrabber::SetCallback( 
ISampleGrabberCB *pCallback,
long WhichMethodToCallback) = 0;

从函数接口我们可以看出,第一个参数需要一个ISampleGrabberCB类型的参数,我们自定义一个回调接口类:class CSampleGrabberCB:public ISampleGrabberCB,用以处理回调数据的接收及外调; 设置接口:

/*
nDataType:数据类型1--音频,2--视频数据
nIndex:设备编号:音频-1,视频0---N
*/
void CSampleGrabberCB::SetDataInfo(int nIndex,int nDataType)
{
m_nIndex=nIndex;
m_nDataType= nDataType;
}

回调函数处理:

//统一的回调函数
STDMETHODIMP CSampleGrabberCB::BufferCB( double dblSampleTime, BYTE *pBuffer, long lBufferSize )
{

if (!pBuffer)
return E_POINTER;

now_tick=::GetTickCount();

first_tick = now_tick;
if (m_nDataType == 1)//视频
{
// TRACE("BufferCB:%d-%d-%d-%d\r\n", m_nIndex, m_nDataType, now_tick - first_tick, lBufferSize);
}
else if (m_nDataType == 2)//音频
{
// TRACE("BufferCB:%d-%d-%d-%d\r\n", m_nIndex, m_nDataType, now_tick - first_tick, lBufferSize);
}

if(pBuffer==NULL||lBufferSize<=0)
return 0;

if (m_realDataCallback && m_pMaster)
{
m_realDataCallback(m_nIndex, pBuffer, lBufferSize, (RealDataStreamType)m_nDataType,NULL, m_pMaster);
return 0;
}
if( g_cbInfo.lBufferSize < lBufferSize )
{
delete [] g_cbInfo.pBuffer;
g_cbInfo.pBuffer = NULL;
g_cbInfo.lBufferSize = 0;
g_cbInfo.bHaveData=FALSE;
}

// Since we can 't access Windows API functions in this callback, just
// copy the bitmap data to a global structure for later reference.
g_cbInfo.dblSampleTime = dblSampleTime;

// If we haven 't yet allocated the data buffer, do it now.
// Just allocate what we need to store the new bitmap.
if (!g_cbInfo.pBuffer)
{
g_cbInfo.pBuffer = new BYTE[lBufferSize];
g_cbInfo.lBufferSize = lBufferSize;
}

if( !g_cbInfo.pBuffer )
{
g_cbInfo.lBufferSize = 0;
g_cbInfo.bHaveData=FALSE;
return E_OUTOFMEMORY;
}

// Copy the bitmap data into our global buffer
memcpy(g_cbInfo.pBuffer, pBuffer, lBufferSize);
g_cbInfo.bHaveData=TRUE;

return 0;
}

两种方式相比较,个人观点:各有优劣;在SkeyeLive中我们采用的是回调方式,当时引进这个库就是为了能在采集端保证音视频从源头是同步的,当然,其实线程模式也是能实现同步的;线程模式的优点是:采集即时性高,即需即取,几乎不会有延时,缺点就是:如果出现取数据端不及时时,如果不考虑缓存的情况下可能就会出现丢帧。而回调模式就正好相反,其优点是:稳定性高,随时都能保证取的帧是连续的,即使不做缓存也不会出现取出来的数据出现丢帧的情况,当然在取数据时比如编码慢(或者回调中做其他延时处理),就会出现预览和回调同步延时的情况,回调缓存的数据量会越来越大,延时也将增大;当然,如果在多路同时采集时,甚至多路同时进行数据处理时,采用回调模式会更显优势!

标签:视频,DirectShow,return,FAILED,hr,音视频,采集,NULL
From: https://blog.51cto.com/openskeye/6083970

相关文章

  • SkeyeLive中DShow本地采集视频参数设置及可能出现的错误提示详解
    在近期发布的SkeyeLive多窗口版本中,由于界面的局限性,选择性的将本地采集的音视频参数设置在界面上剔除掉了(暂时还没想好放在哪里,后续版本会在界面调整后添加),大家可以查看Ske......
  • lazada商品详情数据接口采集代码展示
    业务背景 在很多行业,比如商品采集、刊登、直播、数据分析、竞价等行业都需要用到相关的销量接口,但是官方一般又没有开放这些接口,怎么办?解决方案(点击获取key和secret)目前......
  • 16行代码采集原神官网角色全图&全语音
    嗨害大家好鸭!我是小熊猫~本来是不玩原神的,但是实在是经不住诱惑鸭~毕竟谁能拒绝可以爬树、炸鱼、壶里造房子、抓小动物、躲猫猫的对战游戏捏~准备工具源码资料电子书:​​点......
  • 1688商品列表数据接口采集代码展示
    1688商品列表接口背景在很多行业,比如商品采集、刊登、直播、数据分析等行业都需要用到相关的商品详情接口,但是官方一般又没有开放这些接口,怎么办?解决方案(​​点击获取key......
  • Qt音视频开发16-通用悬浮按钮工具栏的设计
    一、前言通用悬浮按钮工具栏这个功能经过了好几个版本的迭代,一开始设计的时候是写在视频控件widget窗体中,当时功能简单就放一排按钮在顶部悬浮widget中就好,随着用户需求的......
  • 小红书数据关键词采集
    1功能描述这是一个基于小红书微信小程序,实现小红书关键词搜索记录的爬虫。2效果演示记录image.png这是用python做的爬虫程序。运行代码,输入想要搜索的关键词。可以......
  • FANUC机床数据采集程序预估计划
    写一下准备怎么写这程序,要完成,或注意些什么,先胡乱写个大概方向(记得一个名言,已经在我身上多次验证:当你遇到困难,你把它写出来,这个困难就已经解决80%了)大概是这个意思,真的有......
  • 采集FANUC数据的准备工作
     进行工厂设备数据采集,C#编程语言肯定必须得会的。工业设备的相关类库都是C,C++编写的,JAVA的JNA调用C库还不会用,用C#编程调用C库应该方便点。然而,现实是调用一点......
  • Qt音视频开发15-动态切换解码内核的设计
    一、前言动态切换解码内核这个需求也是源自客户的真实需求,既然是动态切换,那肯定是运行期间切换,而不是通过改变标志位重新编译程序来切换,最开始做的就是这种方式,这样就是实......
  • 抖音批量采集
    前言步骤运行程序进行选项设置新增采集开始新增开始采集采集结果详细分类采集视频......