#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { struct Handle { HANDLE value = INVALID_HANDLE_VALUE; Handle() = default; explicit Handle(HANDLE handle) : value(handle) {} Handle(const Handle&) = delete; Handle& operator=(const Handle&) = delete; Handle(Handle&& other) noexcept : value(other.value) { other.value = INVALID_HANDLE_VALUE; } Handle& operator=(Handle&& other) noexcept { if (this != &other) { close(); value = other.value; other.value = INVALID_HANDLE_VALUE; } return *this; } ~Handle() { close(); } void close() { if (value != INVALID_HANDLE_VALUE && value != nullptr) { CloseHandle(value); value = INVALID_HANDLE_VALUE; } } explicit operator bool() const { return value != INVALID_HANDLE_VALUE && value != nullptr; } }; struct DeviceInfoSet { HDEVINFO value = INVALID_HANDLE_VALUE; explicit DeviceInfoSet(HDEVINFO info) : value(info) {} DeviceInfoSet(const DeviceInfoSet&) = delete; DeviceInfoSet& operator=(const DeviceInfoSet&) = delete; ~DeviceInfoSet() { if (value != INVALID_HANDLE_VALUE) { SetupDiDestroyDeviceInfoList(value); } } }; using NtDeviceIoControlFileFn = NTSTATUS(NTAPI*)( HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, ULONG IoControlCode, PVOID InputBuffer, ULONG InputBufferLength, PVOID OutputBuffer, ULONG OutputBufferLength); constexpr NTSTATUS StatusPending = static_cast(0x00000103L); struct PinCandidate { ULONG pinId = 0; std::vector format; LONG width = 0; LONG height = 0; LONG imageBytes = 0; LONGLONG interval100ns = 0; GUID subtype{}; std::string fourcc; }; struct MimirKsCameraFrame { std::vector data; std::int64_t timestampNs = 0; std::uint64_t sequence = 0; int byteLength = 0; }; struct MimirKsCameraCapture { Handle filter; Handle pin; PinCandidate candidate; std::thread worker; std::mutex mutex; std::condition_variable condition; std::deque queue; std::atomic running = false; int queueDepth = 8; std::uint64_t sequence = 0; }; long long nowNs() { return std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count(); } std::string lowercase(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return value; } std::string wideToUtf8(const wchar_t* value) { if (!value) { return {}; } const int bytes = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr); if (bytes <= 1) { return {}; } std::string text(static_cast(bytes - 1), '\0'); WideCharToMultiByte(CP_UTF8, 0, value, -1, text.data(), bytes, nullptr, nullptr); return text; } std::string fourccOrGuid(const GUID& guid) { if (guid.Data2 == 0x0000 && guid.Data3 == 0x0010 && guid.Data4[0] == 0x80 && guid.Data4[1] == 0x00 && guid.Data4[2] == 0x00 && guid.Data4[3] == 0xaa && guid.Data4[4] == 0x00 && guid.Data4[5] == 0x38 && guid.Data4[6] == 0x9b && guid.Data4[7] == 0x71) { char text[5]{}; text[0] = static_cast(guid.Data1 & 0xff); text[1] = static_cast((guid.Data1 >> 8) & 0xff); text[2] = static_cast((guid.Data1 >> 16) & 0xff); text[3] = static_cast((guid.Data1 >> 24) & 0xff); return text; } return {}; } bool ksProperty(HANDLE handle, void* property, DWORD propertyBytes, void* output, DWORD outputBytes, DWORD* returned = nullptr) { DWORD bytes = 0; const BOOL ok = DeviceIoControl( handle, IOCTL_KS_PROPERTY, property, propertyBytes, output, outputBytes, &bytes, nullptr); if (returned) { *returned = bytes; } return ok != FALSE; } template bool getPinProperty(HANDLE filter, ULONG pinId, ULONG propertyId, T& value) { KSP_PIN property{}; property.Property.Set = KSPROPSETID_Pin; property.Property.Id = propertyId; property.Property.Flags = KSPROPERTY_TYPE_GET; property.PinId = pinId; DWORD returned = 0; return ksProperty(filter, &property, sizeof(property), &value, sizeof(value), &returned); } std::vector enumerateCapturePaths() { std::vector paths; DeviceInfoSet info(SetupDiGetClassDevsA( &KSCATEGORY_CAPTURE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE)); if (info.value == INVALID_HANDLE_VALUE) { return paths; } for (DWORD index = 0;; ++index) { SP_DEVICE_INTERFACE_DATA interfaceData{}; interfaceData.cbSize = sizeof(interfaceData); if (!SetupDiEnumDeviceInterfaces(info.value, nullptr, &KSCATEGORY_CAPTURE, index, &interfaceData)) { break; } DWORD required = 0; SetupDiGetDeviceInterfaceDetailW(info.value, &interfaceData, nullptr, 0, &required, nullptr); if (required == 0) { continue; } std::vector storage(required); auto* detail = reinterpret_cast(storage.data()); detail->cbSize = sizeof(*detail); if (SetupDiGetDeviceInterfaceDetailW(info.value, &interfaceData, detail, required, nullptr, nullptr)) { paths.push_back(wideToUtf8(detail->DevicePath)); } } return paths; } std::vector inspectPins(HANDLE filter) { ULONG pinCount = 0; KSPROPERTY countProperty{}; countProperty.Set = KSPROPSETID_Pin; countProperty.Id = KSPROPERTY_PIN_CTYPES; countProperty.Flags = KSPROPERTY_TYPE_GET; if (!ksProperty(filter, &countProperty, sizeof(countProperty), &pinCount, sizeof(pinCount))) { return {}; } std::vector candidates; for (ULONG pinId = 0; pinId < pinCount; ++pinId) { KSPIN_DATAFLOW dataflow{}; if (!getPinProperty(filter, pinId, KSPROPERTY_PIN_DATAFLOW, dataflow) || dataflow != KSPIN_DATAFLOW_OUT) { continue; } KSP_PIN rangeProperty{}; rangeProperty.Property.Set = KSPROPSETID_Pin; rangeProperty.Property.Id = KSPROPERTY_PIN_DATARANGES; rangeProperty.Property.Flags = KSPROPERTY_TYPE_GET; rangeProperty.PinId = pinId; DWORD rangeBytes = 0; ksProperty(filter, &rangeProperty, sizeof(rangeProperty), nullptr, 0, &rangeBytes); if (rangeBytes == 0) { continue; } std::vector ranges(rangeBytes); DWORD returned = 0; if (!ksProperty(filter, &rangeProperty, sizeof(rangeProperty), ranges.data(), static_cast(ranges.size()), &returned)) { continue; } auto* multiple = reinterpret_cast(ranges.data()); auto* cursor = ranges.data() + sizeof(KSMULTIPLE_ITEM); for (ULONG i = 0; i < multiple->Count; ++i) { auto* dataRange = reinterpret_cast(cursor); const bool videoInfo = IsEqualGUID(dataRange->MajorFormat, KSDATAFORMAT_TYPE_VIDEO) && IsEqualGUID(dataRange->Specifier, KSDATAFORMAT_SPECIFIER_VIDEOINFO) && dataRange->FormatSize >= sizeof(KS_DATARANGE_VIDEO); if (videoInfo) { auto* video = reinterpret_cast(cursor); const auto& bmi = video->VideoInfoHeader.bmiHeader; PinCandidate candidate{}; candidate.pinId = pinId; candidate.width = bmi.biWidth; candidate.height = bmi.biHeight; candidate.imageBytes = bmi.biSizeImage > 0 ? bmi.biSizeImage : static_cast(dataRange->SampleSize); candidate.interval100ns = video->VideoInfoHeader.AvgTimePerFrame; candidate.subtype = dataRange->SubFormat; candidate.fourcc = fourccOrGuid(candidate.subtype); candidate.format.resize(sizeof(KS_DATAFORMAT_VIDEOINFOHEADER)); auto* format = reinterpret_cast(candidate.format.data()); format->DataFormat.FormatSize = sizeof(KS_DATAFORMAT_VIDEOINFOHEADER); format->DataFormat.SampleSize = candidate.imageBytes; format->DataFormat.MajorFormat = KSDATAFORMAT_TYPE_VIDEO; format->DataFormat.SubFormat = dataRange->SubFormat; format->DataFormat.Specifier = KSDATAFORMAT_SPECIFIER_VIDEOINFO; format->VideoInfoHeader = video->VideoInfoHeader; format->VideoInfoHeader.bmiHeader.biSizeImage = candidate.imageBytes; candidates.push_back(std::move(candidate)); } cursor += dataRange->FormatSize; } } return candidates; } const PinCandidate* findCandidate( const std::vector& candidates, int width, int height, const char* subtype, double minFps) { const std::string target = subtype ? subtype : ""; for (const auto& candidate : candidates) { const double fps = candidate.interval100ns > 0 ? 10000000.0 / static_cast(candidate.interval100ns) : 0.0; if (candidate.width == width && candidate.height == height && fps >= minFps && candidate.fourcc == target) { return &candidate; } } return nullptr; } bool setState(HANDLE pin, KSSTATE state) { KSPROPERTY property{}; property.Set = KSPROPSETID_Connection; property.Id = KSPROPERTY_CONNECTION_STATE; property.Flags = KSPROPERTY_TYPE_SET; DWORD returned = 0; return DeviceIoControl( pin, IOCTL_KS_PROPERTY, &property, sizeof(property), &state, sizeof(state), &returned, nullptr) != FALSE; } int strideBytes(const PinCandidate& candidate) { return candidate.height > 0 && candidate.imageBytes > 0 ? candidate.imageBytes / candidate.height : 0; } void pushFrame(MimirKsCameraCapture& capture, const std::uint8_t* data, ULONG byteLength) { MimirKsCameraFrame frame; frame.byteLength = static_cast(byteLength); frame.timestampNs = nowNs(); frame.sequence = capture.sequence++; frame.data.resize(byteLength); std::memcpy(frame.data.data(), data, byteLength); { std::lock_guard lock(capture.mutex); capture.queue.push_back(std::move(frame)); while (capture.queue.size() > 32) { capture.queue.pop_front(); } } capture.condition.notify_one(); } void captureThread(MimirKsCameraCapture* capture) { auto* ntDeviceIoControlFile = reinterpret_cast( GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtDeviceIoControlFile")); if (!ntDeviceIoControlFile) { capture->running.store(false); return; } const ULONG connectBytes = sizeof(KSPIN_CONNECT) + static_cast(capture->candidate.format.size()); std::vector connectStorage(connectBytes); auto* connect = reinterpret_cast(connectStorage.data()); connect->Interface.Set = KSINTERFACESETID_Standard; connect->Interface.Id = KSINTERFACE_STANDARD_STREAMING; connect->Medium.Set = KSMEDIUMSETID_Standard; connect->Medium.Id = KSMEDIUM_TYPE_ANYINSTANCE; connect->PinId = capture->candidate.pinId; connect->Priority.PriorityClass = KSPRIORITY_NORMAL; connect->Priority.PrioritySubClass = 1; std::memcpy(connectStorage.data() + sizeof(KSPIN_CONNECT), capture->candidate.format.data(), capture->candidate.format.size()); HANDLE rawPin = INVALID_HANDLE_VALUE; const HRESULT hr = KsCreatePin(capture->filter.value, connect, GENERIC_READ, &rawPin); if (FAILED(hr)) { capture->running.store(false); return; } capture->pin = Handle(rawPin); if (!setState(capture->pin.value, KSSTATE_ACQUIRE) || !setState(capture->pin.value, KSSTATE_PAUSE) || !setState(capture->pin.value, KSSTATE_RUN)) { capture->running.store(false); return; } const DWORD frameBytes = static_cast(capture->candidate.imageBytes); struct ReadSlot { Handle event; IO_STATUS_BLOCK status{}; KSSTREAM_HEADER header{}; std::vector frame; bool pending = false; }; std::vector slots(static_cast(std::max(1, capture->queueDepth))); std::vector waitEvents(slots.size()); for (auto& slot : slots) { slot.event = Handle(CreateEventW(nullptr, TRUE, FALSE, nullptr)); if (!slot.event) { capture->running.store(false); return; } slot.frame.resize(frameBytes); slot.header.Size = sizeof(KSSTREAM_HEADER); slot.header.FrameExtent = frameBytes; slot.header.Data = slot.frame.data(); slot.header.PresentationTime.Numerator = 1; slot.header.PresentationTime.Denominator = 1; waitEvents[&slot - slots.data()] = slot.event.value; } auto submit = [&](ReadSlot& slot) -> bool { ResetEvent(slot.event.value); slot.status = {}; slot.header.DataUsed = 0; slot.header.OptionsFlags = 0; const NTSTATUS status = ntDeviceIoControlFile( capture->pin.value, slot.event.value, nullptr, nullptr, &slot.status, IOCTL_KS_READ_STREAM, &slot.header, sizeof(slot.header), &slot.header, sizeof(slot.header)); if (status == StatusPending) { slot.pending = true; return true; } slot.pending = false; if (status >= 0 && slot.header.DataUsed > 0) { pushFrame(*capture, slot.frame.data(), slot.header.DataUsed); return true; } return false; }; for (auto& slot : slots) { if (!submit(slot)) { capture->running.store(false); return; } } while (capture->running.load()) { const DWORD waitResult = WaitForMultipleObjects( static_cast(waitEvents.size()), waitEvents.data(), FALSE, 100); if (waitResult == WAIT_TIMEOUT) { continue; } if (waitResult < WAIT_OBJECT_0 || waitResult >= WAIT_OBJECT_0 + waitEvents.size()) { break; } auto& slot = slots[static_cast(waitResult - WAIT_OBJECT_0)]; slot.pending = false; if (slot.status.Status < 0 || slot.header.DataUsed == 0) { break; } pushFrame(*capture, slot.frame.data(), slot.header.DataUsed); if (!submit(slot)) { break; } } setState(capture->pin.value, KSSTATE_STOP); CancelIoEx(capture->pin.value, nullptr); for (auto& slot : slots) { if (slot.pending) { WaitForSingleObject(slot.event.value, 100); } } capture->running.store(false); } } // namespace extern "C" { __declspec(dllexport) MimirKsCameraCapture* mimir_ks_create( const char* pathNeedle, int width, int height, const char* subtype, double minFps, int queueDepth) { const std::string needle = lowercase(pathNeedle ? pathNeedle : ""); for (const auto& path : enumerateCapturePaths()) { if (!needle.empty() && lowercase(path).find(needle) == std::string::npos) { continue; } Handle filter(CreateFileA( path.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); if (!filter) { continue; } auto candidates = inspectPins(filter.value); const auto* candidate = findCandidate(candidates, width, height, subtype, minFps); if (!candidate) { continue; } auto* capture = new MimirKsCameraCapture(); capture->filter = std::move(filter); capture->candidate = *candidate; capture->queueDepth = std::max(1, std::min(queueDepth, 32)); return capture; } return nullptr; } __declspec(dllexport) int mimir_ks_start(MimirKsCameraCapture* capture) { if (!capture || capture->running.exchange(true)) { return 0; } capture->worker = std::thread(captureThread, capture); return 1; } __declspec(dllexport) int mimir_ks_read( MimirKsCameraCapture* capture, int* width, int* height, int* stride, char* fourcc, int fourccCapacity, std::int64_t* timestampNs, std::uint64_t* sequence, std::uint8_t* destination, int destinationBytes) { if (!capture || !destination || destinationBytes <= 0) { return 0; } MimirKsCameraFrame frame; { std::lock_guard lock(capture->mutex); if (capture->queue.empty()) { return 0; } frame = std::move(capture->queue.front()); capture->queue.pop_front(); } if (frame.byteLength > destinationBytes) { return -frame.byteLength; } std::memcpy(destination, frame.data.data(), static_cast(frame.byteLength)); if (width) { *width = static_cast(capture->candidate.width); } if (height) { *height = static_cast(capture->candidate.height); } if (stride) { *stride = strideBytes(capture->candidate); } if (fourcc && fourccCapacity > 0) { std::snprintf(fourcc, static_cast(fourccCapacity), "%s", capture->candidate.fourcc.c_str()); } if (timestampNs) { *timestampNs = frame.timestampNs; } if (sequence) { *sequence = frame.sequence; } return frame.byteLength; } __declspec(dllexport) void mimir_ks_destroy(MimirKsCameraCapture* capture) { if (!capture) { return; } capture->running.store(false); if (capture->pin) { CancelIoEx(capture->pin.value, nullptr); } if (capture->worker.joinable()) { capture->worker.join(); } delete capture; } }