#define LOG_MODULE "cam-hook" // clang-format off // Don't format because the order is important here #include #include // clang-format on #include #include #include #include #include #include #include "hook/com-proxy.h" #include "hook/table.h" #include "camhook/cam.h" #include "util/defs.h" #include "util/log.h" #include "util/str.h" #include "util/time.h" #define CAMERA_DATA_STRING_SIZE 0x100 EXTERN_GUID( MY_MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, 0xc60ac5fe, 0x252a, 0x478f, 0xa0, 0xef, 0xbc, 0x8f, 0xa5, 0xf7, 0xca, 0xd3); EXTERN_GUID( MY_MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID, 0x8ac3587a, 0x4ae7, 0x42d8, 0x99, 0xe0, 0x0a, 0x60, 0x13, 0xee, 0xf9, 0x0f); EXTERN_GUID( MY_MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, 0x58f0aad8, 0x22bf, 0x4f8a, 0xbb, 0x3d, 0xd2, 0xc4, 0x97, 0x8c, 0x6e, 0x2f); // define ourselves cause mingw has these wrong struct CameraData { bool setup; char name[CAMERA_DATA_STRING_SIZE]; char deviceInstancePath[CAMERA_DATA_STRING_SIZE]; wchar_t deviceSymbolicLink[CAMERA_DATA_STRING_SIZE]; char extra_upper[CAMERA_DATA_STRING_SIZE]; int address; char parent_name[CAMERA_DATA_STRING_SIZE]; char parent_deviceInstancePath[CAMERA_DATA_STRING_SIZE]; int parent_address; bool fake_addressed; int fake_address; bool fake_located; size_t fake_located_node; }; static struct CameraData camData[CAMHOOK_CONFIG_CAM_MAX]; int camAddresses[CAMHOOK_CONFIG_CAM_MAX] = { 1, 7, }; static size_t num_addressed_cams = 0; static size_t num_located_cams = 0; static CONFIGRET my_CM_Locate_DevNodeA( PDEVINST pdnDevInst, DEVINSTID_A pDeviceID, ULONG ulFlags); static CONFIGRET my_CM_Get_Parent(PDEVINST pdnDevInst, DEVINST dnDevInst, ULONG ulFlags); static CONFIGRET my_CM_Get_Device_IDA( DEVINST dnDevInst, PSTR Buffer, ULONG BufferLen, ULONG ulFlags); static CONFIGRET (*real_CM_Locate_DevNodeA)( PDEVINST pdnDevInst, DEVINSTID_A pDeviceID, ULONG ulFlags); static CONFIGRET (*real_CM_Get_Parent)( PDEVINST pdnDevInst, DEVINST dnDevInst, ULONG ulFlags); static CONFIGRET (*real_CM_Get_Device_IDA)( DEVINST dnDevInst, PSTR Buffer, ULONG BufferLen, ULONG ulFlags); static HRESULT my_MFEnumDeviceSources( IMFAttributes *pAttributes, IMFActivate ***pppSourceActivate, UINT32 *pcSourceActivate); static HRESULT (*real_MFEnumDeviceSources)( IMFAttributes *pAttributes, IMFActivate ***pppSourceActivate, UINT32 *pcSourceActivate); static BOOL my_SetupDiDestroyDeviceInfoList(HDEVINFO DeviceInfoSet); static BOOL (*real_SetupDiDestroyDeviceInfoList)(HDEVINFO DeviceInfoSet); static BOOL my_SetupDiEnumDeviceInfo( HDEVINFO DeviceInfoSet, DWORD MemberIndex, PSP_DEVINFO_DATA DeviceInfoData); static BOOL (*real_SetupDiEnumDeviceInfo)( HDEVINFO DeviceInfoSet, DWORD MemberIndex, PSP_DEVINFO_DATA DeviceInfoData); static BOOL my_SetupDiGetDeviceRegistryPropertyA( HDEVINFO DeviceInfoSet, PSP_DEVINFO_DATA DeviceInfoData, DWORD Property, PDWORD PropertyRegDataType, PBYTE PropertyBuffer, DWORD PropertyBufferSize, PDWORD RequiredSize); static BOOL (*real_SetupDiGetDeviceRegistryPropertyA)( HDEVINFO DeviceInfoSet, PSP_DEVINFO_DATA DeviceInfoData, DWORD Property, PDWORD PropertyRegDataType, PBYTE PropertyBuffer, DWORD PropertyBufferSize, PDWORD RequiredSize); static HDEVINFO my_SetupDiGetClassDevsA( CONST GUID *ClassGuid, PCSTR Enumerator, HWND hwndParent, DWORD Flags); static HDEVINFO (*real_SetupDiGetClassDevsA)( CONST GUID *ClassGuid, PCSTR Enumerator, HWND hwndParent, DWORD Flags); static const struct hook_symbol camhook_cfgmgr32_syms[] = { {.name = "CM_Locate_DevNodeA", .patch = my_CM_Locate_DevNodeA, .link = (void **) &real_CM_Locate_DevNodeA}, {.name = "CM_Get_Parent", .patch = my_CM_Get_Parent, .link = (void **) &real_CM_Get_Parent}, {.name = "CM_Get_Device_IDA", .patch = my_CM_Get_Device_IDA, .link = (void **) &real_CM_Get_Device_IDA}, {.name = "SetupDiDestroyDeviceInfoList", .patch = my_SetupDiDestroyDeviceInfoList, .link = (void **) &real_SetupDiDestroyDeviceInfoList}, {.name = "SetupDiEnumDeviceInfo", .patch = my_SetupDiEnumDeviceInfo, .link = (void **) &real_SetupDiEnumDeviceInfo}, {.name = "SetupDiGetDeviceRegistryPropertyA", .patch = my_SetupDiGetDeviceRegistryPropertyA, .link = (void **) &real_SetupDiGetDeviceRegistryPropertyA}, {.name = "SetupDiGetClassDevsA", .patch = my_SetupDiGetClassDevsA, .link = (void **) &real_SetupDiGetClassDevsA}, }; static const struct hook_symbol camhook_mf_syms[] = { {.name = "MFEnumDeviceSources", .patch = my_MFEnumDeviceSources, .link = (void **) &real_MFEnumDeviceSources}, }; DEVINST camhook_custom_nodes[CAMHOOK_CONFIG_CAM_MAX] = { 0x04040004, 0x04040008, }; DEVINST camhook_custom_parent_nodes[CAMHOOK_CONFIG_CAM_MAX] = { 0x04040014, 0x04040018, }; const char *camhook_custom_parent_device_id[CAMHOOK_CONFIG_CAM_MAX] = { "USB\\VEN_1022&DEV_7908", "USB\\VEN_1022&DEV_7914", }; static CONFIGRET my_CM_Locate_DevNodeA(PDEVINST pdnDevInst, DEVINSTID_A pDeviceID, ULONG ulFlags) { log_info("Inside: %s", __FUNCTION__); char builtString[CAMERA_DATA_STRING_SIZE] = {0}; if (pdnDevInst) { log_info("seeking: %s", pDeviceID); for (size_t i = 0; i < CAMHOOK_CONFIG_CAM_MAX; ++i) { if (camData[i].setup) { snprintf( builtString, CAMERA_DATA_STRING_SIZE, "USB\\VID_288C&PID_0002&MI_00\\%s", camData[i].extra_upper); log_info("built: %s", builtString); if (strcmp(pDeviceID, builtString) == 0) { if (!camData[i].fake_located) { camData[i].fake_located_node = num_located_cams; camData[i].fake_located = true; ++num_located_cams; } log_info( "Injecting custom device %d to node %x", (int) i, (int) camData[i].fake_located_node); *pdnDevInst = camhook_custom_nodes[camData[i].fake_located_node]; return CR_SUCCESS; } } } } return real_CM_Locate_DevNodeA(pdnDevInst, pDeviceID, ulFlags); } static CONFIGRET my_CM_Get_Parent(PDEVINST pdnDevInst, DEVINST dnDevInst, ULONG ulFlags) { log_info("Inside: %s", __FUNCTION__); if (pdnDevInst) { for (size_t i = 0; i < CAMHOOK_CONFIG_CAM_MAX; ++i) { if (dnDevInst == camhook_custom_nodes[i]) { log_info("Injecting custom parent %d", (int) i); *pdnDevInst = camhook_custom_parent_nodes[i]; return CR_SUCCESS; } } } return real_CM_Get_Parent(pdnDevInst, dnDevInst, ulFlags); } static CONFIGRET my_CM_Get_Device_IDA( DEVINST dnDevInst, PSTR Buffer, ULONG BufferLen, ULONG ulFlags) { log_info("Inside: %s", __FUNCTION__); if (Buffer) { for (size_t i = 0; i < CAMHOOK_CONFIG_CAM_MAX; ++i) { if (dnDevInst == camhook_custom_parent_nodes[i]) { log_info("Injecting custom parent %d ID", (int) i); strncpy(Buffer, camhook_custom_parent_device_id[i], BufferLen); Buffer[BufferLen - 1] = '\0'; log_info("%s", Buffer); return CR_SUCCESS; } } } return real_CM_Get_Device_IDA(dnDevInst, Buffer, BufferLen, ulFlags); } static HRESULT(STDCALL *real_GetAllocatedString)( IMFActivate *self, REFGUID guidKey, LPWSTR *ppwszValue, UINT32 *pcchLength); HRESULT my_GetAllocatedString( IMFActivate *self, REFGUID guidKey, LPWSTR *ppwszValue, UINT32 *pcchLength) { HRESULT ret; log_info("Inside: %s", __FUNCTION__); // should probably check GUID, oh well ret = real_GetAllocatedString(self, guidKey, ppwszValue, pcchLength); char *pMBBuffer = (char *) malloc(0x100); wcstombs(pMBBuffer, *ppwszValue, 0x100); log_info("Obtained: %s", pMBBuffer); wchar_t *pwc = NULL; // look for a matching deviceSymbolicLink for (size_t i = 0; i < CAMHOOK_CONFIG_CAM_MAX; ++i) { if (!pwc) { if (camData[i].setup) { pwc = wcsstr(*ppwszValue, camData[i].deviceSymbolicLink); } } } // if matches, replace with target device ID if (pwc) { // \\?\usb#vid_288c&pid_0002&mi_00 pwc[12] = L'2'; pwc[13] = L'8'; pwc[14] = L'8'; pwc[15] = L'c'; pwc[21] = L'0'; pwc[22] = L'0'; pwc[23] = L'0'; pwc[24] = L'2'; pwc[29] = L'0'; pwc[30] = L'0'; wcstombs(pMBBuffer, *ppwszValue, 0x100); log_info("Replaced: %s", pMBBuffer); } free(pMBBuffer); return ret; } static HRESULT my_MFEnumDeviceSources( IMFAttributes *pAttributes, IMFActivate ***pppSourceActivate, UINT32 *pcSourceActivate) { IMFActivate *api; IMFActivateVtbl *api_vtbl; struct com_proxy *api_proxy; UINT32 nsrcs; HRESULT ret; log_info("Inside: %s", __FUNCTION__); ret = real_MFEnumDeviceSources( pAttributes, pppSourceActivate, pcSourceActivate); nsrcs = *pcSourceActivate; for (UINT32 i = 0; i < nsrcs; ++i) { api = (*pppSourceActivate)[i]; ret = com_proxy_wrap(&api_proxy, api, sizeof(*api->lpVtbl)); if (ret != S_OK) { log_warning("Wrapping com proxy failed: %08lx", ret); return ret; } api_vtbl = api_proxy->vptr; real_GetAllocatedString = api_vtbl->GetAllocatedString; api_vtbl->GetAllocatedString = my_GetAllocatedString; (*pppSourceActivate)[i] = (IMFActivate *) api_proxy; } return ret; } #define CUSTOM_DEVICE_HANDLE (void *) 0x04041110 static HDEVINFO RealReplacedHandle; static BOOL my_SetupDiDestroyDeviceInfoList(HDEVINFO DeviceInfoSet) { if (DeviceInfoSet == CUSTOM_DEVICE_HANDLE) { log_info("Inside: %s", __FUNCTION__); DeviceInfoSet = RealReplacedHandle; RealReplacedHandle = NULL; return real_SetupDiDestroyDeviceInfoList(DeviceInfoSet); } return real_SetupDiDestroyDeviceInfoList(DeviceInfoSet); } static BOOL my_SetupDiEnumDeviceInfo( HDEVINFO DeviceInfoSet, DWORD MemberIndex, PSP_DEVINFO_DATA DeviceInfoData) { if (DeviceInfoSet == CUSTOM_DEVICE_HANDLE) { // log_info("Inside: %s", __FUNCTION__); return real_SetupDiEnumDeviceInfo( RealReplacedHandle, MemberIndex, DeviceInfoData); } return real_SetupDiEnumDeviceInfo( DeviceInfoSet, MemberIndex, DeviceInfoData); } static BOOL my_SetupDiGetDeviceRegistryPropertyA( HDEVINFO DeviceInfoSet, PSP_DEVINFO_DATA DeviceInfoData, DWORD Property, PDWORD PropertyRegDataType, PBYTE PropertyBuffer, DWORD PropertyBufferSize, PDWORD RequiredSize) { if (DeviceInfoSet == CUSTOM_DEVICE_HANDLE) { BOOL ret = real_SetupDiGetDeviceRegistryPropertyA( RealReplacedHandle, DeviceInfoData, Property, PropertyRegDataType, PropertyBuffer, PropertyBufferSize, RequiredSize); if (Property == SPDRP_DEVICEDESC) { if (PropertyBuffer) { char *PropertyBufferChar = (char *) PropertyBuffer; for (size_t i = 0; i < CAMHOOK_CONFIG_CAM_MAX; ++i) { if (camData[i].setup) { if (strcmp( PropertyBufferChar, camData[i].parent_name) == 0) { log_info( "%s: replacing %s", __FUNCTION__, camData[i].parent_name); strncpy( PropertyBufferChar, "USB Composite Device", PropertyBufferSize); } } } } return ret; } else if (Property == SPDRP_ADDRESS) { if (PropertyBuffer) { int addr = *(int *) PropertyBuffer; for (size_t i = 0; i < CAMHOOK_CONFIG_CAM_MAX; ++i) { if (camData[i].setup) { if (addr == camData[i].parent_address) { if (!camData[i].fake_addressed) { camData[i].fake_address = camAddresses[num_addressed_cams]; camData[i].fake_addressed = true; ++num_addressed_cams; } log_info( "%s: assigning cam %d to addr %d", __FUNCTION__, (int) i, camData[i].fake_address); *(int *) PropertyBuffer = camData[i].fake_address; } } } } return ret; } } return real_SetupDiGetDeviceRegistryPropertyA( DeviceInfoSet, DeviceInfoData, Property, PropertyRegDataType, PropertyBuffer, PropertyBufferSize, RequiredSize); } static HDEVINFO my_SetupDiGetClassDevsA( CONST GUID *ClassGuid, PCSTR Enumerator, HWND hwndParent, DWORD Flags) { if (ClassGuid == NULL && Enumerator != NULL && hwndParent == NULL && Flags == (DIGCF_PRESENT | DIGCF_ALLCLASSES)) { if (RealReplacedHandle) { log_info("Replacement handle is already set?"); } if (strcmp(Enumerator, "USB") == 0) { log_info("Inside: %s", __FUNCTION__); RealReplacedHandle = real_SetupDiGetClassDevsA( ClassGuid, Enumerator, hwndParent, Flags); if (RealReplacedHandle == INVALID_HANDLE_VALUE) { return INVALID_HANDLE_VALUE; } return CUSTOM_DEVICE_HANDLE; } } return real_SetupDiGetClassDevsA(ClassGuid, Enumerator, hwndParent, Flags); } bool check_four(const char inA[4], const char inB[4]) { return (*(uint32_t *) inA == *(uint32_t *) inB); } char *grab_next_camera_id(char *buffer, size_t bsz) { static size_t gotten = 0; IMFAttributes *pAttributes = NULL; IMFActivate **ppDevices = NULL; buffer[0] = '\0'; HRESULT hr = MFCreateAttributes(&pAttributes, 1); if (FAILED(hr)) { log_info("MFCreateAttributes failed: %ld", hr); goto done; } hr = pAttributes->lpVtbl->SetGUID( pAttributes, &MY_MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, &MY_MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID); if (FAILED(hr)) { log_info("SetGUID failed: %ld", hr); goto done; } UINT32 count; hr = MFEnumDeviceSources(pAttributes, &ppDevices, &count); if (FAILED(hr)) { log_info("MFEnumDeviceSources failed: %ld", hr); goto done; } if (count <= gotten) { log_info("gotten failed: %d < %d", count, (int) gotten); // not enough remaining goto done; } wchar_t wSymLink[CAMERA_DATA_STRING_SIZE]; UINT32 sz; hr = ppDevices[gotten]->lpVtbl->GetString( ppDevices[gotten], &MY_MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, wSymLink, CAMERA_DATA_STRING_SIZE, &sz); if (FAILED(hr)) { log_info("GetString failed: %ld", hr); goto done; } wcstombs(buffer, wSymLink, bsz); log_info("Detected webcam: %s\n", buffer); ++gotten; done: if (pAttributes) { pAttributes->lpVtbl->Release(pAttributes); } for (DWORD i = 0; i < count; i++) { if (ppDevices != NULL && ppDevices[i]) { ppDevices[i]->lpVtbl->Release(ppDevices[i]); } } CoTaskMemFree(ppDevices); return buffer; } bool convert_sym_to_path(const char *sym, char *path) { HDEVINFO DeviceInfoSet = SetupDiCreateDeviceInfoList(NULL, NULL); if (DeviceInfoSet == INVALID_HANDLE_VALUE) { log_info("Could not open SetupDiCreateDeviceInfoList\n"); return 0; } SP_DEVICE_INTERFACE_DATA DeviceInterfaceData = {0}; DeviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); if (!SetupDiOpenDeviceInterfaceA( DeviceInfoSet, sym, 0, &DeviceInterfaceData)) { log_info("Could not SetupDiOpenDeviceInterfaceA\n"); return 0; } SP_DEVINFO_DATA DeviceInfoData = {0}; DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA); if (!SetupDiGetDeviceInterfaceDetailA( DeviceInfoSet, &DeviceInterfaceData, NULL, 0, NULL, &DeviceInfoData)) { if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { log_info("Could not SetupDiGetDeviceInterfaceDetailA\n"); return 0; } } DWORD sz; if (!SetupDiGetDeviceInstanceIdA( DeviceInfoSet, &DeviceInfoData, path, CAMERA_DATA_STRING_SIZE, &sz)) { log_info("Could not SetupDiGetDeviceInstanceIdA\n"); return 0; } if (DeviceInfoSet != INVALID_HANDLE_VALUE) { if (!SetupDiDeleteDeviceInterfaceData( DeviceInfoSet, &DeviceInterfaceData)) { log_info("Could not SetupDiDeleteDeviceInterfaceData\n"); return 0; } SetupDiDestroyDeviceInfoList(DeviceInfoSet); } return 1; } void strtolower(char *str) { for (size_t i = 0; str[i]; i++) { str[i] = tolower(str[i]); } } bool convert_path_to_fakesym(const char *path, wchar_t *sym, char *extra_o) { char root[16] = {0}; char vidstr[16] = {0}; char pidstr[16] = {0}; char mistr[16] = {0}; char extra[64] = {0}; sscanf( path, "%[^\\]\\%[^&]&%[^&]&%[^\\]\\%s", root, vidstr, pidstr, mistr, extra); strcpy(extra_o, extra); strtolower(root); strtolower(vidstr); strtolower(pidstr); strtolower(mistr); strtolower(extra); char buffer[CAMERA_DATA_STRING_SIZE]; snprintf( buffer, CAMERA_DATA_STRING_SIZE, "\\\\?\\%s#%s&%s&%s#%s", root, vidstr, pidstr, mistr, extra); mbstowcs(sym, buffer, CAMERA_DATA_STRING_SIZE); return true; } void fill_cam_struct(struct CameraData *data, const char *devid) { char buffer[CAMERA_DATA_STRING_SIZE]; data->setup = false; if (!devid || strlen(devid) == 0) { devid = grab_next_camera_id(buffer, CAMERA_DATA_STRING_SIZE); } if (!devid || strlen(devid) == 0) { // no more cameras remain? return; } if (strlen(devid) >= CAMERA_DATA_STRING_SIZE) { // error probably log something? return; } // detect input type if (check_four(devid, "\\\\?\\")) { // SYMBOLIC_LINK if (!convert_sym_to_path(devid, data->deviceInstancePath)) { log_info("Could not convert %s to path", devid); return; } } else if (check_four(devid, "USB\\")) { // Device instance path strcpy(data->deviceInstancePath, devid); // continue } else { // UNKNOWN ENTRY log_info("UNK: %s", devid); log_info("Please enter the device instance path"); return; } if (!convert_path_to_fakesym( data->deviceInstancePath, data->deviceSymbolicLink, data->extra_upper)) { log_info("Could not convert %s to sym", data->deviceInstancePath); return; } log_info("dev path: %s", data->deviceInstancePath); // locate device nodes DEVINST dnDevInst; DEVINST parentDev; CONFIGRET cmret; cmret = CM_Locate_DevNodeA( &dnDevInst, data->deviceInstancePath, CM_LOCATE_DEVNODE_NORMAL); if (cmret != CR_SUCCESS) { log_info("CM_Locate_DevNodeA fail: %s", data->deviceInstancePath); return; } cmret = CM_Get_Parent(&parentDev, dnDevInst, 0); if (cmret != CR_SUCCESS) { log_info("CM_Get_Parent fail: %s", data->deviceInstancePath); return; } cmret = CM_Get_Device_IDA( parentDev, data->parent_deviceInstancePath, CAMERA_DATA_STRING_SIZE, 0); if (cmret != CR_SUCCESS) { log_info("CM_Get_Device_IDA parent fail: %s", data->deviceInstancePath); return; } ULONG szAddr; ULONG szDesc; szAddr = 4; szDesc = CAMERA_DATA_STRING_SIZE; cmret = CM_Get_DevNode_Registry_PropertyA( dnDevInst, CM_DRP_ADDRESS, NULL, &data->address, &szAddr, 0); if (cmret != CR_SUCCESS) { log_info( "CM_Get_DevNode_Registry_PropertyA fail: %s", data->deviceInstancePath); return; } cmret = CM_Get_DevNode_Registry_PropertyA( dnDevInst, CM_DRP_DEVICEDESC, NULL, &data->name, &szDesc, 0); if (cmret != CR_SUCCESS) { log_info( "CM_Get_DevNode_Registry_PropertyA fail: %s", data->deviceInstancePath); return; } szAddr = 4; szDesc = CAMERA_DATA_STRING_SIZE; cmret = CM_Get_DevNode_Registry_PropertyA( parentDev, CM_DRP_ADDRESS, NULL, &data->parent_address, &szAddr, 0); if (cmret != CR_SUCCESS) { log_info( "CM_Get_DevNode_Registry_PropertyA parent fail: %s", data->deviceInstancePath); return; } cmret = CM_Get_DevNode_Registry_PropertyA( parentDev, CM_DRP_DEVICEDESC, NULL, &data->parent_name, &szDesc, 0); if (cmret != CR_SUCCESS) { log_info( "CM_Get_DevNode_Registry_PropertyA parent fail: %s", data->deviceInstancePath); return; } log_info("Found %s @ %d", data->name, data->address); log_info("Parent %s @ %d", data->parent_name, data->parent_address); data->setup = true; } void camhook_init(struct camhook_config_cam *config_cam) { MFStartup(0x20070u, 0); // fill before applying hooks for (size_t i = 0; i < config_cam->num_devices; ++i) { // Check if this camera is disabled first if (config_cam->disable_camera[i]) { // If so, pretend this camera is already assigned and move on num_addressed_cams++; num_located_cams++; continue; } // If not, try to hook the camera fill_cam_struct(&camData[i], config_cam->device_id[i]); } size_t num_setup = 0; for (size_t i = 0; i < config_cam->num_devices; ++i) { if (camData[i].setup) { num_setup++; } } if (num_setup > 0) { hook_table_apply( NULL, "setupapi.dll", camhook_cfgmgr32_syms, lengthof(camhook_cfgmgr32_syms)); hook_table_apply( NULL, "Mf.dll", camhook_mf_syms, lengthof(camhook_mf_syms)); log_info("Inserted cam hooks for %d cams", (int) num_setup); // If the user has manually disabled all cams, don't print this in the // log } else if (num_addressed_cams != config_cam->num_devices) { log_info("No cams detected, not hooking"); } } void camhook_fini(void) { MFShutdown(); }