Mimir/native/camera_capture/mimir_ks_camera_capture.cpp
2026-05-27 14:26:11 +01:00

702 lines
19 KiB
C++

#include <windows.h>
#include <winioctl.h>
#include <setupapi.h>
#include <ks.h>
#include <ksmedia.h>
#include <winternl.h>
#include <algorithm>
#include <atomic>
#include <cctype>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <deque>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
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<NTSTATUS>(0x00000103L);
struct PinCandidate
{
ULONG pinId = 0;
std::vector<std::uint8_t> format;
LONG width = 0;
LONG height = 0;
LONG imageBytes = 0;
LONGLONG interval100ns = 0;
GUID subtype{};
std::string fourcc;
};
struct MimirKsCameraFrame
{
std::vector<std::uint8_t> 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<MimirKsCameraFrame> queue;
std::atomic<bool> running = false;
int queueDepth = 8;
std::uint64_t sequence = 0;
};
long long nowNs()
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(
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<char>(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<std::size_t>(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<char>(guid.Data1 & 0xff);
text[1] = static_cast<char>((guid.Data1 >> 8) & 0xff);
text[2] = static_cast<char>((guid.Data1 >> 16) & 0xff);
text[3] = static_cast<char>((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 <typename T>
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<std::string> enumerateCapturePaths()
{
std::vector<std::string> 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<std::uint8_t> storage(required);
auto* detail = reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA_W*>(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<PinCandidate> 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<PinCandidate> 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<std::uint8_t> ranges(rangeBytes);
DWORD returned = 0;
if (!ksProperty(filter, &rangeProperty, sizeof(rangeProperty), ranges.data(), static_cast<DWORD>(ranges.size()), &returned))
{
continue;
}
auto* multiple = reinterpret_cast<KSMULTIPLE_ITEM*>(ranges.data());
auto* cursor = ranges.data() + sizeof(KSMULTIPLE_ITEM);
for (ULONG i = 0; i < multiple->Count; ++i)
{
auto* dataRange = reinterpret_cast<KSDATARANGE*>(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<KS_DATARANGE_VIDEO*>(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<LONG>(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<KS_DATAFORMAT_VIDEOINFOHEADER*>(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<PinCandidate>& 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<double>(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<int>(byteLength);
frame.timestampNs = nowNs();
frame.sequence = capture.sequence++;
frame.data.resize(byteLength);
std::memcpy(frame.data.data(), data, byteLength);
{
std::lock_guard<std::mutex> 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<NtDeviceIoControlFileFn>(
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtDeviceIoControlFile"));
if (!ntDeviceIoControlFile)
{
capture->running.store(false);
return;
}
const ULONG connectBytes = sizeof(KSPIN_CONNECT) + static_cast<ULONG>(capture->candidate.format.size());
std::vector<std::uint8_t> connectStorage(connectBytes);
auto* connect = reinterpret_cast<KSPIN_CONNECT*>(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<DWORD>(capture->candidate.imageBytes);
struct ReadSlot
{
Handle event;
IO_STATUS_BLOCK status{};
KSSTREAM_HEADER header{};
std::vector<std::uint8_t> frame;
bool pending = false;
};
std::vector<ReadSlot> slots(static_cast<std::size_t>(std::max(1, capture->queueDepth)));
std::vector<HANDLE> 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<DWORD>(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<std::size_t>(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<std::mutex> 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<std::size_t>(frame.byteLength));
if (width)
{
*width = static_cast<int>(capture->candidate.width);
}
if (height)
{
*height = static_cast<int>(capture->candidate.height);
}
if (stride)
{
*stride = strideBytes(capture->candidate);
}
if (fourcc && fourccCapacity > 0)
{
std::snprintf(fourcc, static_cast<std::size_t>(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;
}
}