10#include <ascii-chat/video/webcam/webcam.h>
11#include <ascii-chat/common.h>
12#include <ascii-chat/util/time.h>
13#include <ascii-chat/util/overflow.h>
14#include <ascii-chat/platform/system.h>
17#include <mfreadwrite.h>
24struct webcam_context_t {
25 IMFMediaSource *device;
26 IMFSourceReader *reader;
31 image_t *cached_frame;
34static HRESULT enumerate_devices_and_print(
void) {
35 IMFAttributes *attr = NULL;
36 IMFActivate **devices = NULL;
41 hr = MFCreateAttributes(&attr, 1);
43 return SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to create MF attributes: 0x%08x", hr);
48 IMFAttributes_SetGUID(attr, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
50 SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to set MF device type: 0x%08x", hr);
55 hr = MFEnumDeviceSources(attr, &devices, &count);
57 SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to enumerate MF devices: 0x%08x", hr);
61 log_info(
"Found %d video capture device(s):", count);
62 for (UINT32 i = 0; i < count; i++) {
63 LPWSTR friendlyName = NULL;
64 UINT32 nameLength = 0;
66 hr = IMFActivate_GetAllocatedString(devices[i], &MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &friendlyName, &nameLength);
67 if (SUCCEEDED(hr) && friendlyName) {
69 int len = WideCharToMultiByte(CP_UTF8, 0, friendlyName, -1, NULL, 0, NULL, NULL);
71 char *mbName = SAFE_MALLOC((
size_t)len,
void *);
72 if (mbName && WideCharToMultiByte(CP_UTF8, 0, friendlyName, -1, mbName, len, NULL, NULL)) {
73 log_info(
" Device %d: %s", i, mbName);
77 CoTaskMemFree(friendlyName);
79 log_info(
" Device %d: <Unknown Name>", i);
85 for (UINT32 i = 0; i < count; i++) {
87 IMFActivate_Release(devices[i]);
90 CoTaskMemFree((
void *)devices);
93 IMFAttributes_Release(attr);
99asciichat_error_t
webcam_init_context(webcam_context_t **ctx,
unsigned short int device_index) {
100 log_info(
"Opening Windows webcam with Media Foundation, device index %d", device_index);
102 webcam_context_t *cam;
103 cam = SAFE_MALLOC(
sizeof(webcam_context_t), webcam_context_t *);
110 cam->mf_initialized = FALSE;
111 cam->com_initialized = FALSE;
112 cam->cached_frame = NULL;
115 IMFAttributes *attr = NULL;
116 IMFActivate **devices = NULL;
121 platform_stderr_redirect_handle_t stderr_handle = platform_stderr_redirect_to_null();
124 hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
126 cam->com_initialized = TRUE;
127 }
else if (hr != RPC_E_CHANGED_MODE) {
128 platform_stderr_restore(stderr_handle);
129 SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to initialize COM: 0x%08x", hr);
134 hr = MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET);
136 platform_stderr_restore(stderr_handle);
137 SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to startup Media Foundation: 0x%08x", hr);
140 cam->mf_initialized = TRUE;
142 platform_stderr_restore(stderr_handle);
145 enumerate_devices_and_print();
148 hr = MFCreateAttributes(&attr, 1);
150 SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to create MF attributes: 0x%08x", hr);
155 IMFAttributes_SetGUID(attr, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
157 SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to set MF device type: 0x%08x", hr);
162 hr = MFEnumDeviceSources(attr, &devices, &count);
164 SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to enumerate MF devices: 0x%08x", hr);
169 SET_ERRNO(ERROR_WEBCAM,
"No video capture devices found");
174 if (device_index >= count) {
175 SET_ERRNO(ERROR_WEBCAM,
"Device index %d out of range (0-%d)", device_index, count - 1);
181 hr = IMFActivate_ActivateObject(devices[device_index], &IID_IMFMediaSource, (
void **)&cam->device);
182 log_info(
"IMFActivate_ActivateObject returned: 0x%08x", hr);
185 log_error(
"CRITICAL: Failed to activate MF device: 0x%08x", hr);
186 log_error(
" 0x80070005 = E_ACCESSDENIED (device in use)");
187 log_error(
" 0xc00d3704 = Device already in use");
188 log_error(
" 0xc00d3e85 = MF_E_VIDEO_RECORDING_DEVICE_INVALIDATED");
189 result = SET_ERRNO(ERROR_WEBCAM_IN_USE,
"Media Foundation device %d is in use (HRESULT: 0x%08x)", device_index, hr);
195 IMFAttributes *readerAttrs = NULL;
196 hr = MFCreateAttributes(&readerAttrs, 1);
198 log_error(
"Failed to create reader attributes: 0x%08x", hr);
200 SET_ERRNO(ERROR_WEBCAM,
"Failed to create reader attributes for device %d (HRESULT: 0x%08x)", device_index, hr);
206 hr = IMFAttributes_SetUINT32(readerAttrs, &MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, TRUE);
208 log_warn(
"Failed to set advanced video processing attribute: 0x%08x", hr);
211 hr = MFCreateSourceReaderFromMediaSource(cam->device, readerAttrs, &cam->reader);
212 log_info(
"MFCreateSourceReaderFromMediaSource returned: 0x%08x, readerAttrs=%p", hr, (
void *)readerAttrs);
215 IMFAttributes_Release(readerAttrs);
219 log_error(
"CRITICAL: Failed to create MF source reader: 0x%08x", hr);
221 SET_ERRNO(ERROR_WEBCAM_IN_USE,
222 "Failed to create Media Foundation source reader for device %d (HRESULT: 0x%08x)", device_index, hr);
227 hr = IMFSourceReader_SetStreamSelection(cam->reader, (DWORD)MF_SOURCE_READER_ALL_STREAMS, FALSE);
228 log_info(
"SetStreamSelection (deselect all) returned: 0x%08x", hr);
230 log_warn(
"Failed to deselect all streams: 0x%08x", hr);
233 hr = IMFSourceReader_SetStreamSelection(cam->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE);
234 log_info(
"SetStreamSelection (select video) returned: 0x%08x", hr);
236 log_error(
"Failed to select video stream: 0x%08x", hr);
237 result = SET_ERRNO(ERROR_WEBCAM,
"Failed to select video stream for device %d (HRESULT: 0x%08x)", device_index, hr);
244 IMFMediaType *rgbType = NULL;
245 hr = MFCreateMediaType(&rgbType);
247 IMFMediaType_SetGUID(rgbType, &MF_MT_MAJOR_TYPE, &MFMediaType_Video);
248 IMFMediaType_SetGUID(rgbType, &MF_MT_SUBTYPE, &MFVideoFormat_RGB32);
252 UINT64 frameSize = ((UINT64)640 << 32) | (UINT64)480;
253 hr = IMFMediaType_SetUINT64(rgbType, &MF_MT_FRAME_SIZE, frameSize);
255 log_warn(
"Could not set frame size to 640x480: 0x%08x", hr);
259 hr = IMFSourceReader_SetCurrentMediaType(cam->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, NULL, rgbType);
260 IMFMediaType_Release(rgbType);
263 log_info(
"Successfully requested RGB32 output format at 640x480");
265 log_warn(
"Could not set RGB32 format: 0x%08x, will use native format", hr);
271 IMFMediaType *currentType = NULL;
272 hr = IMFSourceReader_GetCurrentMediaType(cam->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, ¤tType);
274 UINT64 frameSize = 0;
275 hr = IMFMediaType_GetUINT64(currentType, &MF_MT_FRAME_SIZE, &frameSize);
277 cam->width = (int)(frameSize >> 32);
278 cam->height = (int)(frameSize & 0xFFFFFFFF);
279 log_info(
"Media Foundation webcam opened: %dx%d", cam->width, cam->height);
284 log_warn(
"Could not get frame size, using default 640x480");
286 IMFMediaType_Release(currentType);
290 log_warn(
"Could not get media type, using default 640x480");
293 DWORD streamIndex, flags;
295 IMFSample *sample = NULL;
297 hr = IMFSourceReader_ReadSample(cam->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM,
299 &streamIndex, &flags, ×tamp, &sample);
302 log_error(
"CRITICAL: Failed to read test frame during initialization: 0x%08x", hr);
304 SET_ERRNO(ERROR_WEBCAM_IN_USE,
"Failed to read test frame from device %d (HRESULT: 0x%08x)", device_index, hr);
310 IMFSample_Release(sample);
315 for (UINT32 i = 0; i < count; i++) {
317 IMFActivate_Release(devices[i]);
320 CoTaskMemFree((
void *)devices);
324 IMFAttributes_Release(attr);
334 for (UINT32 i = 0; i < count; i++) {
336 IMFActivate_Release(devices[i]);
339 CoTaskMemFree((
void *)devices);
342 IMFAttributes_Release(attr);
345 IMFSourceReader_Release(cam->reader);
348 IMFMediaSource_Release(cam->device);
350 if (cam->mf_initialized) {
353 if (cam->com_initialized) {
363 if (ctx && ctx->reader) {
366 HRESULT hr = IMFSourceReader_Flush(ctx->reader, (DWORD)MF_SOURCE_READER_ALL_STREAMS);
368 log_warn(
"IMFSourceReader_Flush failed: 0x%08lx", hr);
370 log_debug(
"Flushed webcam source reader");
380 IMFSourceReader_Release(ctx->reader);
384 IMFMediaSource_Release(ctx->device);
387 if (ctx->mf_initialized) {
389 ctx->mf_initialized = FALSE;
391 if (ctx->com_initialized) {
393 ctx->com_initialized = FALSE;
396 if (ctx->cached_frame) {
398 ctx->cached_frame = NULL;
401 log_debug(
"Windows Media Foundation webcam closed");
406 if (!ctx || !ctx->reader) {
411 IMFSample *sample = NULL;
412 DWORD streamIndex, flags;
416 LARGE_INTEGER freq, start, end;
417 QueryPerformanceFrequency(&freq);
418 QueryPerformanceCounter(&start);
422 hr = IMFSourceReader_ReadSample(ctx->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM,
424 &streamIndex, &flags, ×tamp, &sample);
426 QueryPerformanceCounter(&end);
427 double elapsed_ms = ((double)(end.QuadPart - start.QuadPart)) * 1000.0 / (
double)freq.QuadPart;
429 char duration_str[32];
431 log_info(
"ReadSample took %s (hr=0x%08x, flags=0x%08x, sample=%p)", duration_str, hr, flags, sample);
434 if (SUCCEEDED(hr) && (flags & MF_SOURCE_READERF_STREAMTICK)) {
435 log_info(
"Received stream tick, no sample yet");
439 if (SUCCEEDED(hr) && (flags & MF_SOURCE_READERF_ENDOFSTREAM)) {
440 log_warn(
"End of stream reached");
445 log_error(
"CRITICAL: Failed to read MF sample on FIRST attempt: 0x%08x", hr);
446 log_error(
" 0x80070005 = E_ACCESSDENIED (device in use)");
447 log_error(
" 0xc00d3704 = Device already in use");
448 log_error(
" 0xc00d3e85 = MF_E_VIDEO_RECORDING_DEVICE_INVALIDATED");
449 log_error(
" 0x80004005 = E_FAIL (generic failure)");
450 log_error(
" 0xc00d36b2 = MF_E_INVALIDREQUEST");
451 log_error(
" 0xc00d36c4 = MF_E_HW_MFT_FAILED_START_STREAMING");
458 static int null_count = 0;
460 if (null_count <= 10) {
462 if (null_count == 1) {
463 log_info(
"No sample available yet (this is normal during startup)");
465 }
else if (null_count > 50) {
467 log_error(
"Too many consecutive NULL samples (%d) - device likely in use", null_count);
473 IMFMediaBuffer *buffer = NULL;
474 hr = IMFSample_ConvertToContiguousBuffer(sample, &buffer);
476 static int buffer_fail_count = 0;
478 log_debug(
"Failed to get contiguous buffer: 0x%08x (failure #%d)", hr, buffer_fail_count);
480 if (buffer_fail_count > 20) {
481 log_error(
"CRITICAL: Failed to get media buffer %d times - webcam likely in use", buffer_fail_count);
484 IMFSample_Release(sample);
489 BYTE *bufferData = NULL;
490 DWORD bufferLength = 0;
491 hr = IMFMediaBuffer_Lock(buffer, &bufferData, NULL, &bufferLength);
493 static int lock_fail_count = 0;
495 log_debug(
"Failed to lock MF buffer: 0x%08x (failure #%d)", hr, lock_fail_count);
497 if (lock_fail_count > 20) {
498 log_error(
"CRITICAL: Failed to lock media buffer %d times - webcam likely in use", lock_fail_count);
501 IMFMediaBuffer_Release(buffer);
502 IMFSample_Release(sample);
507 UINT32 width = (UINT32)ctx->width;
508 UINT32 height = (UINT32)ctx->height;
512 size_t pixel_count_check = 0;
513 if (checked_size_mul((
size_t)width, (
size_t)height, &pixel_count_check) != ASCIICHAT_OK) {
514 log_error(
"webcam_read_context: dimensions overflow: %u x %u", width, height);
515 IMFMediaBuffer_Unlock(buffer);
516 IMFMediaBuffer_Release(buffer);
517 IMFSample_Release(sample);
522 size_t pixel_buffer_size = 0;
523 if (checked_size_mul(pixel_count_check,
sizeof(rgb_pixel_t), &pixel_buffer_size) != ASCIICHAT_OK) {
524 log_error(
"webcam_read_context: pixel buffer overflow: %zu pixels", pixel_count_check);
525 IMFMediaBuffer_Unlock(buffer);
526 IMFMediaBuffer_Release(buffer);
527 IMFSample_Release(sample);
533 if (!ctx->cached_frame || ctx->cached_frame->w != (
int)width || ctx->cached_frame->h != (
int)height) {
535 if (ctx->cached_frame) {
540 image_t *img = SAFE_MALLOC(
sizeof(image_t), image_t *);
541 img->w = (int)(
unsigned int)width;
542 img->h = (int)(
unsigned int)height;
544 img->pixels = SAFE_MALLOC_SIMD(pixel_buffer_size, rgb_pixel_t *);
545 ctx->cached_frame = img;
548 image_t *img = ctx->cached_frame;
554 LARGE_INTEGER copy_start, copy_end;
555 QueryPerformanceCounter(©_start);
557 UINT32 pixel_count = width * height;
558 BYTE *src = bufferData;
559 rgb_pixel_t *dst = img->pixels;
563 for (i = 0; i + 4 <= pixel_count; i += 4) {
586 for (; i < pixel_count; i++) {
594 QueryPerformanceCounter(©_end);
595 double copy_ms = ((double)(copy_end.QuadPart - copy_start.QuadPart)) * 1000.0 / (
double)freq.QuadPart;
597 char copy_duration_str[32];
599 log_info(
"Pixel copy took %s (%u pixels)", copy_duration_str, pixel_count);
602 IMFMediaBuffer_Unlock(buffer);
603 IMFMediaBuffer_Release(buffer);
604 IMFSample_Release(sample);
610 if (!ctx || !width || !height) {
611 return SET_ERRNO(ERROR_INVALID_PARAM,
"webcam_get_dimensions: invalid parameters");
615 *height = ctx->height;
619asciichat_error_t
webcam_list_devices(webcam_device_info_t **out_devices,
unsigned int *out_count) {
620 if (!out_devices || !out_count) {
621 return SET_ERRNO(ERROR_INVALID_PARAM,
"webcam_list_devices: invalid parameters");
627 IMFAttributes *attr = NULL;
628 IMFActivate **mf_devices = NULL;
631 asciichat_error_t result = ASCIICHAT_OK;
632 BOOL com_initialized = FALSE;
633 BOOL mf_initialized = FALSE;
636 platform_stderr_redirect_handle_t stderr_handle = platform_stderr_redirect_to_null();
639 hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
641 com_initialized = TRUE;
642 }
else if (hr != RPC_E_CHANGED_MODE) {
643 platform_stderr_restore(stderr_handle);
644 return SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to initialize COM: 0x%08x", hr);
648 hr = MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET);
650 platform_stderr_restore(stderr_handle);
651 result = SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to startup Media Foundation: 0x%08x", hr);
654 mf_initialized = TRUE;
656 platform_stderr_restore(stderr_handle);
659 hr = MFCreateAttributes(&attr, 1);
661 result = SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to create MF attributes: 0x%08x", hr);
667 IMFAttributes_SetGUID(attr, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
669 result = SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to set MF device type: 0x%08x", hr);
674 hr = MFEnumDeviceSources(attr, &mf_devices, &mf_count);
676 result = SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to enumerate MF devices: 0x%08x", hr);
686 webcam_device_info_t *devices = SAFE_CALLOC(mf_count,
sizeof(webcam_device_info_t), webcam_device_info_t *);
688 result = SET_ERRNO(ERROR_MEMORY,
"Failed to allocate device info array");
693 for (UINT32 i = 0; i < mf_count; i++) {
694 devices[i].index = i;
696 LPWSTR friendlyName = NULL;
697 UINT32 nameLength = 0;
699 hr = IMFActivate_GetAllocatedString(mf_devices[i], &MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &friendlyName,
701 if (SUCCEEDED(hr) && friendlyName) {
703 int len = WideCharToMultiByte(CP_UTF8, 0, friendlyName, -1, NULL, 0, NULL, NULL);
704 if (len > 0 && len < WEBCAM_DEVICE_NAME_MAX) {
705 WideCharToMultiByte(CP_UTF8, 0, friendlyName, -1, devices[i].name, WEBCAM_DEVICE_NAME_MAX, NULL, NULL);
707 SAFE_STRNCPY(devices[i].name,
"<Unknown>", WEBCAM_DEVICE_NAME_MAX);
709 CoTaskMemFree(friendlyName);
711 SAFE_STRNCPY(devices[i].name,
"<Unknown>", WEBCAM_DEVICE_NAME_MAX);
715 *out_devices = devices;
716 *out_count = mf_count;
720 for (UINT32 i = 0; i < mf_count; i++) {
722 IMFActivate_Release(mf_devices[i]);
725 CoTaskMemFree((
void *)mf_devices);
728 IMFAttributes_Release(attr);
730 if (mf_initialized) {
733 if (com_initialized) {
int format_duration_ms(double milliseconds, char *buffer, size_t buffer_size)
void image_destroy(image_t *p)
asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index)
void webcam_free_device_list(webcam_device_info_t *devices)
void webcam_flush_context(webcam_context_t *ctx)
asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count)
asciichat_error_t webcam_get_dimensions(webcam_context_t *ctx, int *width, int *height)
image_t * webcam_read_context(webcam_context_t *ctx)
void webcam_cleanup_context(webcam_context_t *ctx)