ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
webcam_mediafoundation.c
Go to the documentation of this file.
1
7#ifdef _WIN32
8
9#define COBJMACROS
11#include <ascii-chat/common.h>
14#include <ascii-chat/log/io.h>
17#include <mfapi.h>
18#include <mfidl.h>
19#include <mfreadwrite.h>
20#include <mferror.h>
21
22// Windows Media Foundation webcam implementation
23// Note: MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING and
24// MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING are defined in mfreadwrite.h
25
26struct webcam_context_t {
27 IMFMediaSource *device;
28 IMFSourceReader *reader;
29 int width;
30 int height;
31 BOOL mf_initialized;
32 BOOL com_initialized;
33 image_t *cached_frame; // Reusable frame buffer (allocated once, reused for each read)
34};
35
36static HRESULT enumerate_devices_and_print(void) {
37 IMFAttributes *attr = NULL;
38 IMFActivate **devices = NULL;
39 UINT32 count = 0;
40 HRESULT hr;
41
42 // Create attribute store for device enumeration
43 hr = MFCreateAttributes(&attr, 1);
44 if (FAILED(hr)) {
45 return SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to create MF attributes: 0x%08x", hr);
46 }
47
48 // Set the device type to video capture
49 hr =
50 IMFAttributes_SetGUID(attr, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
51 if (FAILED(hr)) {
52 SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to set MF device type: 0x%08x", hr);
53 goto cleanup;
54 }
55
56 // Enumerate video capture devices
57 hr = MFEnumDeviceSources(attr, &devices, &count);
58 if (FAILED(hr)) {
59 SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to enumerate MF devices: 0x%08x", hr);
60 goto cleanup;
61 }
62
63 log_info("Found %d video capture device(s):", count);
64 for (UINT32 i = 0; i < count; i++) {
65 LPWSTR friendlyName = NULL;
66 UINT32 nameLength = 0;
67
68 hr = IMFActivate_GetAllocatedString(devices[i], &MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &friendlyName, &nameLength);
69 if (SUCCEEDED(hr) && friendlyName) {
70 // Convert wide string to multibyte for logging
71 int len = WideCharToMultiByte(CP_UTF8, 0, friendlyName, -1, NULL, 0, NULL, NULL);
72 if (len > 0) {
73 char *mbName = SAFE_MALLOC((size_t)len, void *);
74 if (mbName && WideCharToMultiByte(CP_UTF8, 0, friendlyName, -1, mbName, len, NULL, NULL) > 0) {
75 log_info(" Device %d: %s", i, mbName);
76 }
77 SAFE_FREE(mbName);
78 }
79 CoTaskMemFree(friendlyName);
80 } else {
81 log_info(" Device %d: <Unknown Name>", i);
82 }
83 }
84
85cleanup:
86 if (devices) {
87 for (UINT32 i = 0; i < count; i++) {
88 if (devices[i]) {
89 IMFActivate_Release(devices[i]);
90 }
91 }
92 CoTaskMemFree((void *)devices);
93 }
94 if (attr) {
95 IMFAttributes_Release(attr);
96 }
97
98 return hr;
99}
100
101asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index) {
102 log_info("Opening Windows webcam with Media Foundation, device index %d", device_index);
103
104 webcam_context_t *cam;
106
107 // Initialize all fields
108 cam->device = NULL;
109 cam->reader = NULL;
110 cam->width = 0;
111 cam->height = 0;
112 cam->mf_initialized = FALSE;
113 cam->com_initialized = FALSE;
114 cam->cached_frame = NULL;
115
116 HRESULT hr;
117 IMFAttributes *attr = NULL;
118 IMFActivate **devices = NULL;
119 UINT32 count = 0;
120 int result = -1;
121
122 // Capture Windows Media Foundation's initialization output
123 LOG_IO("mf", {
124 // Initialize COM
125 hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
126 if (SUCCEEDED(hr)) {
127 cam->com_initialized = TRUE;
128 } else if (hr != RPC_E_CHANGED_MODE) {
129 SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to initialize COM: 0x%08x", hr);
130 // Note: We can't use goto from within LOG_IO block, so we'll set hr to FAILED
131 hr = E_FAIL;
132 }
133
134 // Initialize Media Foundation
135 if (SUCCEEDED(hr)) {
136 hr = MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET);
137 if (FAILED(hr)) {
138 SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to startup Media Foundation: 0x%08x", hr);
139 } else {
140 cam->mf_initialized = TRUE;
141 }
142 }
143 });
144
145 if (FAILED(hr)) {
146 goto error;
147 }
148
149 // Enumerate and print all devices
150 enumerate_devices_and_print();
151
152 // Create attribute store for device enumeration
153 hr = MFCreateAttributes(&attr, 1);
154 if (FAILED(hr)) {
155 SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to create MF attributes: 0x%08x", hr);
156 goto error;
157 }
158
159 hr =
160 IMFAttributes_SetGUID(attr, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
161 if (FAILED(hr)) {
162 SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to set MF device type: 0x%08x", hr);
163 goto error;
164 }
165
166 // Enumerate devices
167 hr = MFEnumDeviceSources(attr, &devices, &count);
168 if (FAILED(hr)) {
169 SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to enumerate MF devices: 0x%08x", hr);
170 goto error;
171 }
172
173 if (count == 0) {
174 SET_ERRNO(ERROR_WEBCAM, "No video capture devices found");
175 hr = E_FAIL;
176 goto error;
177 }
178
179 if (device_index >= count) {
180 SET_ERRNO(ERROR_WEBCAM, "Device index %d out of range (0-%d)", device_index, count - 1);
181 hr = E_FAIL;
182 goto error;
183 }
184
185 // Create media source for the specified device
186 hr = IMFActivate_ActivateObject(devices[device_index], &IID_IMFMediaSource, (void **)&cam->device);
187 log_info("IMFActivate_ActivateObject returned: 0x%08x", hr);
188
189 if (FAILED(hr)) {
190 log_error("CRITICAL: Failed to activate MF device: 0x%08x", hr);
191 log_error(" 0x80070005 = E_ACCESSDENIED (device in use)");
192 log_error(" 0xc00d3704 = Device already in use");
193 log_error(" 0xc00d3e85 = MF_E_VIDEO_RECORDING_DEVICE_INVALIDATED");
194 result = SET_ERRNO(ERROR_WEBCAM_IN_USE, "Media Foundation device %d is in use (HRESULT: 0x%08x)", device_index, hr);
195 goto error;
196 }
197
198 // Create source reader with GPU-accelerated video processing (Windows 8+)
199 // This enables automatic YUV->RGB conversion with hardware acceleration
200 IMFAttributes *readerAttrs = NULL;
201 hr = MFCreateAttributes(&readerAttrs, 1);
202 if (FAILED(hr)) {
203 log_error("Failed to create reader attributes: 0x%08x", hr);
204 result =
205 SET_ERRNO(ERROR_WEBCAM, "Failed to create reader attributes for device %d (HRESULT: 0x%08x)", device_index, hr);
206 goto error;
207 }
208
209 // Enable advanced video processing for GPU-accelerated YUV->RGB conversion (Windows 8+)
210 // This is the recommended attribute for webcam capture with format conversion
211 hr = IMFAttributes_SetUINT32(readerAttrs, &MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, TRUE);
212 if (FAILED(hr)) {
213 log_warn("Failed to set advanced video processing attribute: 0x%08x", hr);
214 }
215
216 hr = MFCreateSourceReaderFromMediaSource(cam->device, readerAttrs, &cam->reader);
217 log_info("MFCreateSourceReaderFromMediaSource returned: 0x%08x, readerAttrs=%p", hr, (void *)readerAttrs);
218
219 if (readerAttrs) {
220 IMFAttributes_Release(readerAttrs);
221 }
222
223 if (FAILED(hr)) {
224 log_error("CRITICAL: Failed to create MF source reader: 0x%08x", hr);
225 result =
227 "Failed to create Media Foundation source reader for device %d (HRESULT: 0x%08x)", device_index, hr);
228 goto error;
229 }
230
231 // IMPORTANT: Select the video stream first before configuring
232 hr = IMFSourceReader_SetStreamSelection(cam->reader, (DWORD)MF_SOURCE_READER_ALL_STREAMS, FALSE);
233 log_info("SetStreamSelection (deselect all) returned: 0x%08x", hr);
234 if (FAILED(hr)) {
235 log_warn("Failed to deselect all streams: 0x%08x", hr);
236 }
237
238 hr = IMFSourceReader_SetStreamSelection(cam->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE);
239 log_info("SetStreamSelection (select video) returned: 0x%08x", hr);
240 if (FAILED(hr)) {
241 log_error("Failed to select video stream: 0x%08x", hr);
242 result = SET_ERRNO(ERROR_WEBCAM, "Failed to select video stream for device %d (HRESULT: 0x%08x)", device_index, hr);
243 goto error;
244 }
245
246 // Prefer the highest advertised capture rate, then the mode closest to 640x480.
247 IMFMediaType *bestType = NULL;
248 UINT32 bestNumerator = 0, bestDenominator = 1;
249 UINT64 bestDistance = UINT64_MAX;
250 for (DWORD index = 0;; index++) {
251 IMFMediaType *nativeType = NULL;
252 hr =
253 IMFSourceReader_GetNativeMediaType(cam->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, index, &nativeType);
254 if (hr == MF_E_NO_MORE_TYPES)
255 break;
256 if (FAILED(hr)) {
257 log_warn("Could not enumerate webcam format %lu: 0x%08x", (unsigned long)index, hr);
258 break;
259 }
260 UINT64 nativeSize = 0, nativeRate = 0;
261 if (SUCCEEDED(IMFMediaType_GetUINT64(nativeType, &MF_MT_FRAME_SIZE, &nativeSize)) &&
262 SUCCEEDED(IMFMediaType_GetUINT64(nativeType, &MF_MT_FRAME_RATE, &nativeRate))) {
263 UINT32 numerator = (UINT32)(nativeRate >> 32), denominator = (UINT32)nativeRate;
264 UINT64 width = nativeSize >> 32, height = (UINT32)nativeSize;
265 UINT64 pixels = width * height;
266 UINT64 distance = pixels > 640 * 480 ? pixels - 640 * 480 : 640 * 480 - pixels;
267 if (numerator && denominator) {
268 log_info("Webcam supported mode: %llux%llu at %.3f FPS", (unsigned long long)width, (unsigned long long)height,
269 (double)numerator / denominator);
270 UINT64 candidate = (UINT64)numerator * bestDenominator;
271 UINT64 best = (UINT64)bestNumerator * denominator;
272 if (candidate > best || (candidate == best && distance < bestDistance)) {
273 if (bestType)
274 IMFMediaType_Release(bestType);
275 bestType = nativeType;
276 nativeType = NULL;
277 bestNumerator = numerator;
278 bestDenominator = denominator;
279 bestDistance = distance;
280 }
281 }
282 }
283 if (nativeType)
284 IMFMediaType_Release(nativeType);
285 }
286 if (bestType) {
287 hr = IMFSourceReader_SetCurrentMediaType(cam->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, NULL, bestType);
288 IMFMediaType_Release(bestType);
289 if (FAILED(hr)) {
290 log_warn("Could not select fastest webcam mode: 0x%08x", hr);
291 bestNumerator = 0;
292 } else {
293 log_info("Selected webcam maximum capture rate: %.3f FPS", (double)bestNumerator / bestDenominator);
294 }
295 }
296
297 // Request RGB32 output format (BGRA) at 640x480 resolution
298 // Combined with MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, this enables
299 // GPU-accelerated YUV->RGB conversion
300 IMFMediaType *rgbType = NULL;
301 hr = MFCreateMediaType(&rgbType);
302 if (SUCCEEDED(hr)) {
303 IMFMediaType_SetGUID(rgbType, &MF_MT_MAJOR_TYPE, &MFMediaType_Video);
304 IMFMediaType_SetGUID(rgbType, &MF_MT_SUBTYPE, &MFVideoFormat_RGB32);
305 if (bestNumerator) {
306 IMFMediaType_SetUINT64(rgbType, &MF_MT_FRAME_RATE, ((UINT64)bestNumerator << 32) | bestDenominator);
307 }
308
309 // Request 640x480 resolution (we only need 480x270 max for ascii-chat)
310 // This dramatically reduces pixel copy overhead (307,200 vs 8,294,400 pixels)
311 UINT64 frameSize = ((UINT64)640 << 32) | (UINT64)480;
312 hr = IMFMediaType_SetUINT64(rgbType, &MF_MT_FRAME_SIZE, frameSize);
313 if (FAILED(hr)) {
314 log_warn("Could not set frame size to 640x480: 0x%08x", hr);
315 }
316
317 // Use partial type - let MF fill in other details
318 hr = IMFSourceReader_SetCurrentMediaType(cam->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, NULL, rgbType);
319 IMFMediaType_Release(rgbType);
320
321 if (SUCCEEDED(hr)) {
322 log_info("Successfully requested RGB32 output format at 640x480");
323 } else {
324 log_warn("Could not configure RGB32 webcam output: 0x%08x", hr);
325 result = SET_ERRNO(ERROR_WEBCAM, "Could not configure RGB32 webcam output: 0x%08x", hr);
326 goto error;
327 }
328 }
329
330 // Get actual media type and dimensions
331 IMFMediaType *currentType = NULL;
332 hr = IMFSourceReader_GetCurrentMediaType(cam->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, &currentType);
333 if (SUCCEEDED(hr)) {
334 UINT64 frameSize = 0;
335 hr = IMFMediaType_GetUINT64(currentType, &MF_MT_FRAME_SIZE, &frameSize);
336 if (SUCCEEDED(hr)) {
337 cam->width = (int)(frameSize >> 32);
338 cam->height = (int)(frameSize & 0xFFFFFFFF);
339 log_info("Media Foundation webcam opened: %dx%d", cam->width, cam->height);
340 UINT64 actualRate = 0;
341 if (SUCCEEDED(IMFMediaType_GetUINT64(currentType, &MF_MT_FRAME_RATE, &actualRate)) && (UINT32)actualRate) {
342 log_info("Negotiated webcam capture rate: %.3f FPS", (double)(actualRate >> 32) / (UINT32)actualRate);
343 }
344 } else {
345 // Default resolution if we can't get the actual size
346 cam->width = 640;
347 cam->height = 480;
348 log_warn("Could not get frame size, using default 640x480");
349 }
350 IMFMediaType_Release(currentType);
351 } else {
352 cam->width = 640;
353 cam->height = 480;
354 log_warn("Could not get media type, using default 640x480");
355 }
356
357 DWORD streamIndex, flags;
358 LONGLONG timestamp;
359 IMFSample *sample = NULL;
360
361 hr = IMFSourceReader_ReadSample(cam->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM,
362 0, // Regular synchronous read
363 &streamIndex, &flags, &timestamp, &sample);
364
365 if (FAILED(hr)) {
366 log_error("CRITICAL: Failed to read test frame during initialization: 0x%08x", hr);
367 result =
368 SET_ERRNO(ERROR_WEBCAM_IN_USE, "Failed to read test frame from device %d (HRESULT: 0x%08x)", device_index, hr);
369 goto error;
370 }
371
372 // Clean up the test sample if we got one
373 if (sample) {
374 IMFSample_Release(sample);
375 }
376
377 // Cleanup enumeration resources on success
378 if (devices) {
379 for (UINT32 i = 0; i < count; i++) {
380 if (devices[i]) {
381 IMFActivate_Release(devices[i]);
382 }
383 }
384 CoTaskMemFree((void *)devices);
385 devices = NULL;
386 }
387 if (attr) {
388 IMFAttributes_Release(attr);
389 attr = NULL;
390 }
391
392 *ctx = cam;
393
394 /* Register webcam context with named registry */
395 char device_name[32];
396 snprintf(device_name, sizeof(device_name), "device:%u", device_index);
397 NAMED_REGISTER(cam, device_name, "webcam", "0x%tx", NULL);
398
399 return ASCIICHAT_OK;
400
401error:
402 // Only cleanup devices and attr if they haven't been cleaned up yet
403 if (devices) {
404 for (UINT32 i = 0; i < count; i++) {
405 if (devices[i]) {
406 IMFActivate_Release(devices[i]);
407 }
408 }
409 CoTaskMemFree((void *)devices);
410 }
411 if (attr) {
412 IMFAttributes_Release(attr);
413 }
414 if (cam->reader) {
415 IMFSourceReader_Release(cam->reader);
416 }
417 if (cam->device) {
418 IMFMediaSource_Release(cam->device);
419 }
420 if (cam->mf_initialized) {
421 MFShutdown();
422 }
423 if (cam->com_initialized) {
424 CoUninitialize();
425 }
426 if (cam) {
427 SAFE_FREE(cam);
428 }
429 return result;
430}
431
433 if (ctx && ctx->reader) {
434 // Flush to cancel any pending ReadSample operations
435 // This interrupts blocking synchronous reads in other threads
436 HRESULT hr = IMFSourceReader_Flush(ctx->reader, (DWORD)MF_SOURCE_READER_ALL_STREAMS);
437 if (FAILED(hr)) {
438 log_warn("IMFSourceReader_Flush failed: 0x%08lx", hr);
439 } else {
440 log_debug("Flushed webcam source reader");
441 }
442 }
443}
444
446 if (ctx) {
447 NAMED_UNREGISTER(ctx);
448
449 if (ctx->reader) {
450 // Flush before release to ensure no pending operations
452 IMFSourceReader_Release(ctx->reader);
453 ctx->reader = NULL;
454 }
455 if (ctx->device) {
456 IMFMediaSource_Release(ctx->device);
457 ctx->device = NULL;
458 }
459 if (ctx->mf_initialized) {
460 MFShutdown();
461 ctx->mf_initialized = FALSE;
462 }
463 if (ctx->com_initialized) {
464 CoUninitialize();
465 ctx->com_initialized = FALSE;
466 }
467 // Free cached frame if it was allocated
468 if (ctx->cached_frame) {
469 image_destroy(ctx->cached_frame);
470 ctx->cached_frame = NULL;
471 }
472 SAFE_FREE(ctx);
473 log_debug("Windows Media Foundation webcam closed");
474 }
475}
476
478 if (!ctx || !ctx->reader) {
479 return NULL;
480 }
481
482 HRESULT hr;
483 IMFSample *sample = NULL;
484 DWORD streamIndex, flags;
485 LONGLONG timestamp;
486
487 // Timing diagnostic: measure ReadSample duration
488 LARGE_INTEGER freq, start, end;
489 QueryPerformanceFrequency(&freq);
490 QueryPerformanceCounter(&start);
491
492 // Read a sample from the source reader
493 // Use 0 for synchronous blocking read (DRAIN flag was wrong - that's for EOF)
494 hr = IMFSourceReader_ReadSample(ctx->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM,
495 0, // Regular synchronous read
496 &streamIndex, &flags, &timestamp, &sample);
497
498 QueryPerformanceCounter(&end);
499 double elapsed_ms = ((double)(end.QuadPart - start.QuadPart)) * 1000.0 / (double)freq.QuadPart;
500
501 char duration_str[32];
502 time_pretty((uint64_t)(elapsed_ms * 1e6), -1, duration_str, sizeof(duration_str));
503 log_debug_every(5 * NS_PER_SEC_INT, "ReadSample took %s (hr=0x%08x, flags=0x%08x, sample=%p)", duration_str, hr,
504 flags, sample);
505
506 // Check for stream tick or other non-data flags
507 if (SUCCEEDED(hr) && (flags & MF_SOURCE_READERF_STREAMTICK)) {
508 log_info("Received stream tick, no sample yet");
509 return NULL;
510 }
511
512 if (SUCCEEDED(hr) && (flags & MF_SOURCE_READERF_ENDOFSTREAM)) {
513 log_warn("End of stream reached");
514 return NULL;
515 }
516
517 if (FAILED(hr)) {
518 log_error("CRITICAL: Failed to read MF sample on FIRST attempt: 0x%08x", hr);
519 log_error(" 0x80070005 = E_ACCESSDENIED (device in use)");
520 log_error(" 0xc00d3704 = Device already in use");
521 log_error(" 0xc00d3e85 = MF_E_VIDEO_RECORDING_DEVICE_INVALIDATED");
522 log_error(" 0x80004005 = E_FAIL (generic failure)");
523 log_error(" 0xc00d36b2 = MF_E_INVALIDREQUEST");
524 log_error(" 0xc00d36c4 = MF_E_HW_MFT_FAILED_START_STREAMING");
525
526 // Return NULL - device is likely in use
527 return NULL;
528 }
529
530 if (!sample) {
531 static int null_count = 0;
532 null_count++;
533 if (null_count <= 10) {
534 // This is normal when using DRAIN flag - it means no sample ready yet
535 if (null_count == 1) {
536 log_info("No sample available yet (this is normal during startup)");
537 }
538 } else if (null_count > 50) {
539 // Too many consecutive NULL samples - likely exclusive access issue
540 log_error("Too many consecutive NULL samples (%d) - device likely in use", null_count);
541 }
542 return NULL;
543 }
544
545 // Get the media buffer from the sample
546 IMFMediaBuffer *buffer = NULL;
547 hr = IMFSample_ConvertToContiguousBuffer(sample, &buffer);
548 if (FAILED(hr)) {
549 static int buffer_fail_count = 0;
550 buffer_fail_count++;
551 log_debug("Failed to get contiguous buffer: 0x%08x (failure #%d)", hr, buffer_fail_count);
552
553 if (buffer_fail_count > 20) {
554 log_error("CRITICAL: Failed to get media buffer %d times - webcam likely in use", buffer_fail_count);
555 }
556
557 IMFSample_Release(sample);
558 return NULL;
559 }
560
561 // Lock the buffer and get the data
562 BYTE *bufferData = NULL;
563 DWORD bufferLength = 0;
564 hr = IMFMediaBuffer_Lock(buffer, &bufferData, NULL, &bufferLength);
565 if (FAILED(hr)) {
566 static int lock_fail_count = 0;
567 lock_fail_count++;
568 log_debug("Failed to lock MF buffer: 0x%08x (failure #%d)", hr, lock_fail_count);
569
570 if (lock_fail_count > 20) {
571 log_error("CRITICAL: Failed to lock media buffer %d times - webcam likely in use", lock_fail_count);
572 }
573
574 IMFMediaBuffer_Release(buffer);
575 IMFSample_Release(sample);
576 return NULL;
577 }
578
579 // Use cached frame dimensions from context (no need to query media type every frame)
580 UINT32 width = (UINT32)ctx->width;
581 UINT32 height = (UINT32)ctx->height;
582
583 // Check for integer overflow before multiplication (prevents heap corruption).
584 // Check pixel count overflow
585 size_t pixel_count_check = 0;
586 if (checked_size_mul((size_t)width, (size_t)height, &pixel_count_check) != ASCIICHAT_OK) {
587 log_error("webcam_read_context: dimensions overflow: %u x %u", width, height);
588 IMFMediaBuffer_Unlock(buffer);
589 IMFMediaBuffer_Release(buffer);
590 IMFSample_Release(sample);
591 return NULL;
592 }
593
594 // Check pixel buffer size overflow
595 size_t pixel_buffer_size = 0;
596 if (checked_size_mul(pixel_count_check, sizeof(rgb_pixel_t), &pixel_buffer_size) != ASCIICHAT_OK) {
597 log_error("webcam_read_context: pixel buffer overflow: %zu pixels", pixel_count_check);
598 IMFMediaBuffer_Unlock(buffer);
599 IMFMediaBuffer_Release(buffer);
600 IMFSample_Release(sample);
601 return NULL;
602 }
603
604 // Allocate or reuse cached frame (allocated once, reused for each call)
605 // This matches the documented API contract: "Subsequent calls reuse the same buffer"
606 if (!ctx->cached_frame || ctx->cached_frame->w != (int)width || ctx->cached_frame->h != (int)height) {
607 // Free old cached frame if it exists with different dimensions
608 if (ctx->cached_frame) {
609 image_destroy(ctx->cached_frame);
610 }
611
612 // Create new image_t structure
613 image_t *img = SAFE_MALLOC(sizeof(image_t), image_t *);
614 img->w = (int)(unsigned int)width;
615 img->h = (int)(unsigned int)height;
616 // Use SIMD-aligned allocation for optimal NEON/AVX performance with vld3q_u8
617 img->pixels = SAFE_MALLOC_SIMD(pixel_buffer_size, rgb_pixel_t *);
618 ctx->cached_frame = img;
619 }
620
621 image_t *img = ctx->cached_frame;
622
623 // Copy RGB32 data (BGRA order in Media Foundation)
624 // Media Foundation converts YUV->RGB32 via GPU-accelerated pipeline
625 // RGB32 format is 4 bytes per pixel: B, G, R, A (we ignore alpha)
626
627 LARGE_INTEGER copy_start, copy_end;
628 QueryPerformanceCounter(&copy_start);
629
630 UINT32 pixel_count = width * height;
631 BYTE *src = bufferData;
632 rgb_pixel_t *dst = img->pixels;
633
634 // Optimized pixel copy - process 4 pixels at a time
635 UINT32 i;
636 for (i = 0; i + 4 <= pixel_count; i += 4) {
637 // Pixel 0
638 dst[0].b = src[0];
639 dst[0].g = src[1];
640 dst[0].r = src[2];
641 // Pixel 1
642 dst[1].b = src[4];
643 dst[1].g = src[5];
644 dst[1].r = src[6];
645 // Pixel 2
646 dst[2].b = src[8];
647 dst[2].g = src[9];
648 dst[2].r = src[10];
649 // Pixel 3
650 dst[3].b = src[12];
651 dst[3].g = src[13];
652 dst[3].r = src[14];
653
654 src += 16; // 4 pixels * 4 bytes
655 dst += 4;
656 }
657
658 // Handle remaining pixels
659 for (; i < pixel_count; i++) {
660 dst->b = src[0];
661 dst->g = src[1];
662 dst->r = src[2];
663 src += 4;
664 dst++;
665 }
666
667 QueryPerformanceCounter(&copy_end);
668 double copy_ms = ((double)(copy_end.QuadPart - copy_start.QuadPart)) * 1000.0 / (double)freq.QuadPart;
669
670 char copy_duration_str[32];
671 time_pretty((uint64_t)(copy_ms * 1e6), -1, copy_duration_str, sizeof(copy_duration_str));
672 log_debug_every(5 * NS_PER_SEC_INT, "Pixel copy took %s (%u pixels)", copy_duration_str, pixel_count);
673
674 // Unlock and cleanup
675 IMFMediaBuffer_Unlock(buffer);
676 IMFMediaBuffer_Release(buffer);
677 IMFSample_Release(sample);
678
679 return img;
680}
681
682asciichat_error_t webcam_get_dimensions(webcam_context_t *ctx, int *width, int *height) {
683 if (!ctx || !width || !height) {
684 return SET_ERRNO(ERROR_INVALID_PARAM, "webcam_get_dimensions: invalid parameters");
685 }
686
687 *width = ctx->width;
688 *height = ctx->height;
689 return ASCIICHAT_OK;
690}
691
692asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count) {
693 if (!out_devices || !out_count) {
694 return SET_ERRNO(ERROR_INVALID_PARAM, "webcam_list_devices: invalid parameters");
695 }
696
697 *out_devices = NULL;
698 *out_count = 0;
699
700 IMFAttributes *attr = NULL;
701 IMFActivate **mf_devices = NULL;
702 UINT32 mf_count = 0;
703 HRESULT hr;
705 BOOL com_initialized = FALSE;
706 BOOL mf_initialized = FALSE;
707
708 // Capture Windows Media Foundation's initialization output
709 LOG_IO("mf", {
710 // Initialize COM
711 hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
712 if (SUCCEEDED(hr)) {
713 com_initialized = TRUE;
714 } else if (hr != RPC_E_CHANGED_MODE) {
715 result = SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to initialize COM: 0x%08x", hr);
716 hr = E_FAIL;
717 }
718
719 // Initialize Media Foundation
720 if (SUCCEEDED(hr)) {
721 hr = MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET);
722 if (FAILED(hr)) {
723 result = SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to startup Media Foundation: 0x%08x", hr);
724 } else {
725 mf_initialized = TRUE;
726 }
727 }
728 });
729
730 if (FAILED(hr)) {
731 goto cleanup;
732 }
733
734 // Create attribute store for device enumeration
735 hr = MFCreateAttributes(&attr, 1);
736 if (FAILED(hr)) {
737 result = SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to create MF attributes: 0x%08x", hr);
738 goto cleanup;
739 }
740
741 // Set the device type to video capture
742 hr =
743 IMFAttributes_SetGUID(attr, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
744 if (FAILED(hr)) {
745 result = SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to set MF device type: 0x%08x", hr);
746 goto cleanup;
747 }
748
749 // Enumerate video capture devices
750 hr = MFEnumDeviceSources(attr, &mf_devices, &mf_count);
751 if (FAILED(hr)) {
752 result = SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to enumerate MF devices: 0x%08x", hr);
753 goto cleanup;
754 }
755
756 if (mf_count == 0) {
757 // No devices found - not an error, just return empty list
758 goto cleanup;
759 }
760
761 // Allocate output array
763 if (!devices) {
764 result = SET_ERRNO(ERROR_MEMORY, "Failed to allocate device info array");
765 goto cleanup;
766 }
767
768 // Populate device info
769 for (UINT32 i = 0; i < mf_count; i++) {
770 devices[i].index = i;
771
772 LPWSTR friendlyName = NULL;
773 UINT32 nameLength = 0;
774
775 hr = IMFActivate_GetAllocatedString(mf_devices[i], &MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &friendlyName,
776 &nameLength);
777 if (SUCCEEDED(hr) && friendlyName) {
778 // Convert wide string to UTF-8
779 int len = WideCharToMultiByte(CP_UTF8, 0, friendlyName, -1, NULL, 0, NULL, NULL);
780 if (len > 0 && len < WEBCAM_DEVICE_NAME_MAX) {
781 WideCharToMultiByte(CP_UTF8, 0, friendlyName, -1, devices[i].name, WEBCAM_DEVICE_NAME_MAX, NULL, NULL);
782 } else {
783 SAFE_STRNCPY(devices[i].name, "<Unknown>", WEBCAM_DEVICE_NAME_MAX);
784 }
785 CoTaskMemFree(friendlyName);
786 } else {
787 SAFE_STRNCPY(devices[i].name, "<Unknown>", WEBCAM_DEVICE_NAME_MAX);
788 }
789 }
790
791 *out_devices = devices;
792 *out_count = mf_count;
793
794cleanup:
795 if (mf_devices) {
796 for (UINT32 i = 0; i < mf_count; i++) {
797 if (mf_devices[i]) {
798 IMFActivate_Release(mf_devices[i]);
799 }
800 }
801 CoTaskMemFree((void *)mf_devices);
802 }
803 if (attr) {
804 IMFAttributes_Release(attr);
805 }
806 if (mf_initialized) {
807 MFShutdown();
808 }
809 if (com_initialized) {
810 CoUninitialize();
811 }
812
813 return result;
814}
815
817 SAFE_FREE(devices);
818}
819
820#endif
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
#define SAFE_MALLOC_SIMD(size, cast)
Definition common.h:364
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
#define NAMED_REGISTER(ptr, name, type, fmt, parent_ptr)
Register any pointer with base name, type, format spec, and location (auto-suffix)
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
#define SET_ERRNO_SYS(code, context_msg,...)
Set error code with custom message and system error context, returning the error code.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_WEBCAM_IN_USE
Definition error_codes.h:65
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ ERROR_WEBCAM
Definition error_codes.h:64
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define NS_PER_SEC_INT
Definition time.h:157
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index)
Initialize webcam context for advanced operations.
asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count)
Enumerate available webcam devices.
void webcam_free_device_list(webcam_device_info_t *devices)
Free device list returned by webcam_list_devices()
void webcam_flush_context(webcam_context_t *ctx)
Flush/interrupt pending read operations on webcam context.
struct webcam_context_t webcam_context_t
Opaque webcam context structure.
Definition webcam.h:150
asciichat_error_t webcam_get_dimensions(webcam_context_t *ctx, int *width, int *height)
Get webcam frame dimensions.
#define WEBCAM_DEVICE_NAME_MAX
Maximum length of webcam device name.
Definition webcam.h:74
image_t * webcam_read_context(webcam_context_t *ctx)
Capture a frame from webcam context.
void webcam_cleanup_context(webcam_context_t *ctx)
Clean up webcam context and release resources.
Capture stdout/stderr and redirect to logging system.
#define LOG_IO(prefix, block)
Capture output from a code block and log it.
Definition io.h:81
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
✅ Safe Integer Arithmetic and Overflow Detection
Image structure.
int w
Image width in pixels (must be > 0)
int h
Image height in pixels (must be > 0)
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order)
RGB pixel structure.
uint8_t b
Blue color component (0-255)
uint8_t g
Green color component (0-255)
uint8_t r
Red color component (0-255)
Webcam device information structure.
Definition webcam.h:86
unsigned int index
Device index (use with webcam_init_context)
Definition webcam.h:87
Cross-platform system functions interface for ascii-chat.
⏱️ High-precision timing utilities using sokol_time.h and uthash