一、问题描述:
使用QT开发视频会议时需要实现实时检测USB摄像头/麦克风拔插的功能,这里主要涉及到对一些Windows API的了解以及windows系统的设备管理识别不同种设备时的原理,在实现过程中主要参考了以下两篇文章以及微软开发手册。
监测硬件的插入或者拔除https://www.codeproject.com/Articles/14500/Detecting-Hardware-Insertion-and-or-Removal Windows下检测usb设备拔插的demohttps://blog.csdn.net/explorer114/article/details/50563051
本文将着重讲解windows识别设备所需的基础概念和原理以及在实现过程中对于上述参考文章需要修改的地方。
二、基本概念
1.windows下常见的设备类型有哪些?
从图2-1 设备管理器的截图中,我们可以看到常见的设备类型有监视器,键盘,鼠标,音频输入和输出,照相机等等,然后我们点击其中一个设备类型就可以看见下面具体连接的设备。
图2-1 设备管理器
2.windows是如何归类一个设备和标识一个设备类型的呢?
微软提出一个概念叫做设备安装类(Setup Class),为了简化设备的安装过程,windows会将以相同方式设置和配置的设备分组为一个设备安装类,比如,windows将所有的网卡(PCI网卡/无线网卡/外接USB网卡)都归到网络适配器一类。为了区分不同的设备安装类,每一个设备安装类都关联了一个GUID,系统定义的安装类Guid是在devguid.h中定义的,并且通常具有GUID_DEVCLASS_Xxx的格式的符号名称。具体可以参考下面微软官方的解释。
Overview of Device Setup Classes - Windows drivers | Microsoft DocsOverview of Device Setup Classeshttps://docs.microsoft.com/en-us/windows-hardware/drivers/install/overview-of-device-setup-classes从图2-2 devguid.h中我们可以看到所有设备安装类的guid,比如电池设备,蓝牙设备的guid等等,在注册表路径HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class 下可以查看到设备安装类GUID
图 2-2 devguid.h
还有一个与之相对应的概念叫做设备接口类(Device Interface Class),顾名思义,设备对外的接口,用于设备的管理和访问操作,系统对设备的状态进行跟踪。
驱动程序可以注册一个设备接口类,然后向每个可能向其发送用户模式I/O请求的设备对象启用该类的实例,当驱动程序注册设备接口类的实例时,I/O管理器将设备和设备接口类GUID与符号链接名称相关。符号链接名称存储在注册表中,使用该接口的应用程序可以查询该接口的实例并接收表示支持该接口的设备的符号链接名称,然后应用程序就可以使用符号链接名称作为I/O请求的目标。
每个设备接口类都与一个GUID相关联。系统在设备特定的头文件中为通用设备接口类定义GUID,供应商可以创建额外的设备接口类。例如,三种不同类型的鼠标设备可能是同一设备接口类的成员,即使一种通过 USB 端口连接,另一种通过串行端口连接,第三种通过红外端口连接。每个驱动程序将其设备注册为接口类 GUID_DEVINTERFACE_MOUSE 的成员。此 GUID 在头文件Ntddmou.h中定义。
在注册表路径 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceClasses可以查看到设备接口类GUID,如图2-3 注册表中的guid
图2-3 注册表中的设备类guid
详细内容请参考微软官方文档:
Overview of Device Interface Classes - Windows drivers | Microsoft DocsOverview of Device Interface Classeshttps://docs.microsoft.com/en-us/windows-hardware/drivers/install/overview-of-device-interface-classes
在下面的链接中我们可以查到一些我们需要用到的设备接口类GUIDGUID_DEVICE_MEMORY - Windows drivers | Microsoft DocsGUID_DEVICE_MEMORYhttps://docs.microsoft.com/en-us/windows-hardware/drivers/install/guid-device-memory
三、检测USB摄像头拔插的思路
检测设备的拔插主要分为三个步骤:
1.使用RegisterDeviceNotification()注册需要监控的设备的事件提醒,这里需要把对应的设备接口类GUID传进入,比如要监控USB设备就传入GUID_DEVINTERFACE_USB_DEVICE,这里需要注意的是在上述参考教程中用到的这些GUID注册进去之后,消息提示只能判断出是插入/拔出了一个USB设备,并不能判断是不是USB摄像头或者麦克风,所以这里要添加以下两个GUID:
// KSCATEGORY_VIDEO_CAMERA
{ 0xE5323777, 0xF976, 0x4f5b, { 0x9B, 0x55, 0xB9, 0x46, 0x99, 0xC4, 0x6E, 0x44}},
// KSCATEGORY_AUDIO
{ 0x6994AD04, 0x93EF, 0x11D0, { 0xA3, 0xCC, 0x00, 0xA0, 0xC9, 0x22, 0x31, 0x96}},
图3-1 示例中的设备GUID列表
2.在事件处理函数中处理回调的设备拔插消息,如果是QT 则在nativeEvent()中处理,如果是基于 windows 原生API开发的,就在WindowProc()回调函数中处理WM_DEVICECHANGE事件,如果msg->wParm == DBT_DEVICEARRIVAL 则是设备插入,如果是msg->wParam == DBT_DEVICEREMOVECOMPLETE 则是设备移除。
bool kducserver::nativeEvent(const QByteArray &eventType, void *message, long *result)
{
//Just handle windows message for device in and out.
#ifdef UC_OS_WIN
MSG *msg = static_cast<MSG*>(message);
switch (msg->message) {
//You can refer to: https://docs.microsoft.com/en-us/windows/win32/devio/wm-devicechange
case WM_DEVICECHANGE:
if (msg->wParam == DBT_DEVICEARRIVAL || msg->wParam == DBT_DEVICEREMOVECOMPLETE) {
judgeDevice(msg->wParam, msg->lParam);
}
LOGI(em_log_mod_ptt, "Received WM_DEVICECHANGE message, wparam:%x lparam:%x", msg->wParam, msg->lParam);
break;
default:
break;
}
#endif
return false;
}
3.接下来就是根据不同的设备类型获取dbcc_name,这里只处理了DBT_DEVTYP_DEVICEINTERFACE这种设备类型。这里有一个地方需要注意的是上述参考文章是将pHdr转换成PDEV_BROADCAST_DEVICEINTERFACE,我们可以看一下这个结构体的定义,如果定义了UNICODE宏,则使用的是宽字符类型的结构体,dbcc_name的类型是wchar_t,如果没有定义,则dbcc_name是char,但是在实际使用过程中,可以发现我在visual studio中并没有定义UNICODE宏,但是回调的却是wchar_t类型的dbcc_name,导致我在解析的过程中获取失败,所以我在转化pHdr时直接使用的是宽字符类型的结构体
图3-2 Dbt.h中该结构体的定义
图3-3 DEV_BROADCAST_DEVICEINTERFACE_W文章来源:https://www.toymoban.com/news/detail-472194.html
int kducserver::judgeDevice(WPARAM wParam, LPARAM lParam)
{
if (!(DBT_DEVICEARRIVAL == wParam || DBT_DEVICEREMOVECOMPLETE == wParam))
return S_FALSE;
PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lParam;
PDEV_BROADCAST_DEVICEINTERFACE_W pDevInf;
PDEV_BROADCAST_HANDLE pDevHnd;
PDEV_BROADCAST_OEM pDevOem;
PDEV_BROADCAST_PORT pDevPort;
PDEV_BROADCAST_VOLUME pDevVolume;
switch (pHdr->dbch_devicetype)
{
case DBT_DEVTYP_DEVICEINTERFACE:
pDevInf = (PDEV_BROADCAST_DEVICEINTERFACE_W)pHdr;
updateDevice(pDevInf, wParam);
break;
case DBT_DEVTYP_HANDLE:
pDevHnd = (PDEV_BROADCAST_HANDLE)pHdr;
break;
case DBT_DEVTYP_OEM:
pDevOem = (PDEV_BROADCAST_OEM)pHdr;
break;
case DBT_DEVTYP_PORT:
pDevPort = (PDEV_BROADCAST_PORT)pHdr;
break;
case DBT_DEVTYP_VOLUME:
pDevVolume = (PDEV_BROADCAST_VOLUME)pHdr;
break;
}
return S_OK;
}
接下来从dbcc_name中解析出类,然后就可以调用SetupDiGetDeviceRegistryProperty()函数获得设备的友好名称,类名等等,这里我是通过类名来判断的,如果类名==“Camera”,则判断拔入/拔出设备是摄像头。如果有其他需求可以灵活处理,下面附上SetupDiGetXXX接口的链接SetupDiGetClassDevsW function (setupapi.h) - Win32 apps | Microsoft Docshttps://docs.microsoft.com/zh-cn/windows/win32/api/setupapi/nf-setupapi-setupdigetclassdevsw文章来源地址https://www.toymoban.com/news/detail-472194.html
void kducserver::updateDevice(PDEV_BROADCAST_DEVICEINTERFACE_W pDevInf, WPARAM wParam)
{
QString deviceName = QString::fromWCharArray(pDevInf->dbcc_name);
if (!deviceName.size() > 4) {
LOGI(em_log_mod_base, "pDevInf->dacc_name length error!");
return;
}
std::string szDevIdOld = deviceName.right(deviceName.size()-4).toStdString(); /*jump "\\?\" */
LOGD(em_log_mod_base, "dbcc_name:%s", szDevIdOld.c_str());
std::string old_value("#");
std::string new_value("\\");
std::string szDevId = replace_all_distinct(szDevIdOld, old_value, new_value);
transform(szDevId.begin(), szDevId.end(), szDevId.begin(), toupper);
int index = szDevId.find("\\");
if (-1 == index)
{
LOGI(em_log_mod_base, "szDevId.find(\"\\\") error!");
return;
}
std::string szClass = szDevId.substr(0, 3);
const char* pchClass = szClass.c_str();
DWORD dwFlag = DBT_DEVICEARRIVAL != wParam ? DIGCF_ALLCLASSES : (DIGCF_ALLCLASSES
| DIGCF_PRESENT);
HDEVINFO hDevInfo = SetupDiGetClassDevs(NULL,pchClass, NULL, dwFlag);
if (INVALID_HANDLE_VALUE == hDevInfo)
{
LOGI(em_log_mod_base, "SetupDiGetClassDevs(): return ERROR!");
return;
}
SP_DEVINFO_DATA spDevInfoData;
TCHAR buf[MAX_PATH];
TCHAR className[MAX_PATH];
if (findDevice(hDevInfo, szDevId, spDevInfoData))
{
DWORD DataT;
DWORD nSize = 0;
//get device setup class name
//you can refer to https://docs.microsoft.com/en-us/windows/win32/api/setupapi/nf-setupapi-setupdigetdeviceregistrypropertya
if (SetupDiGetDeviceRegistryProperty(hDevInfo, &spDevInfoData,
SPDRP_CLASS, &DataT, (PBYTE)className, sizeof(className), &nSize))
{
LOGD(em_log_mod_base, "Get device class name:%s\n",className);
}
/*get Friendly Name or Device Description*/
if (SetupDiGetDeviceRegistryProperty(hDevInfo, &spDevInfoData,
SPDRP_FRIENDLYNAME, &DataT, (PBYTE)buf, sizeof(buf), &nSize))
{
;
}
else if (SetupDiGetDeviceRegistryProperty(hDevInfo, &spDevInfoData,
SPDRP_DEVICEDESC, &DataT, (PBYTE)buf, sizeof(buf), &nSize))
{
;
}
else
{
lstrcpy(buf, _T("Unknown"));
}
}
std::string Arrival = " has arrived";
std::string Removel = " has removed";
std::string stDeviceDescription(buf);
std::string stDeviceName = getClassDesc(&(spDevInfoData.ClassGuid));
std::string stShowArrival = stDeviceName + Arrival +"[Device describe: " + stDeviceDescription + "]";
std::string stShowRemove = stDeviceName + Removel +"[Device describe: " + stDeviceDescription + "]";
const char* chShowArrival = stShowArrival.c_str();
const char* chShowRemove = stShowRemove.c_str();
std::string name(className);
if (DBT_DEVICEARRIVAL == wParam)
{
if (name == USB_VIDEO_CLASS) {
//这里可以替换成自己前端需要处理的逻辑
notifyFrontDeviceRemoved(false, false);
}
else if(name==USB_VOICE_CLASS){
notifyFrontDeviceRemoved(true, false);
}
LOGD(em_log_mod_base, "%s\n", chShowArrival);
}
else
{
if (name == USB_VIDEO_CLASS) {
notifyFrontDeviceRemoved(false,true);
}
else if (name == USB_VOICE_CLASS) {
notifyFrontDeviceRemoved(true, true);
}
LOGD(em_log_mod_base, "%s\n", chShowRemove);
}
SetupDiDestroyDeviceInfoList(hDevInfo);
}
bool kducserver::findDevice(HDEVINFO& hDevInfo, std::string& szDevId, SP_DEVINFO_DATA& spDevInfoData)
{
int nPos = szDevId.find("\\{");
std::string stDevIdShort(szDevId, 0, nPos);
spDevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
for (int i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, &spDevInfoData); i++)
{
DWORD nSize = 0;
TCHAR buf[MAX_PATH];
if (!SetupDiGetDeviceInstanceId(hDevInfo, &spDevInfoData, buf, sizeof(buf), &nSize))
{
LOGI(em_log_mod_base, _T("SetupDiGetDeviceInstanceId(): error"));
return FALSE;
}
if (stDevIdShort == buf)
{
return TRUE;
}
}
return FALSE;
}
std::string kducserver::getClassDesc(const GUID* guid)
{
TCHAR buf[MAX_PATH];
DWORD size;
if (SetupDiGetClassDescription(guid, buf, sizeof(buf), &size))
{
return std::string(buf);
}
else
{
return _T("");
}
}
std::string& kducserver::replace_all_distinct(std::string& str, const std::string& old_value, const std::string& new_value)
{
for (std::string::size_type pos(0); pos != std::string::npos; pos += new_value.length())
{
if ((pos = str.find(old_value, pos)) != std::string::npos)
{
str.replace(pos, old_value.length(), new_value);
}
else
{
break;
}
}
return str;
}
到了这里,关于QT在Windows下检测USB设备热拔插的思路的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!