ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
webcam_avfoundation.m
Go to the documentation of this file.
1
7#ifdef __APPLE__
8
9#import <AVFoundation/AVFoundation.h>
10#import <CoreMedia/CoreMedia.h>
11#import <CoreVideo/CoreVideo.h>
12#include <dispatch/dispatch.h>
13
15#include <ascii-chat/common.h>
18
19// AVFoundation timeout configuration
20#define AVFOUNDATION_FRAME_TIMEOUT_NS 500000000 // 500ms timeout (adjustable for slow cameras)
21#define AVFOUNDATION_INIT_TIMEOUT_NS (3 * NSEC_PER_SEC) // 3 second timeout for initialization
22
23@interface WebcamCaptureDelegate : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
24@property(nonatomic, strong) dispatch_semaphore_t frameSemaphore;
25@property(nonatomic) CVPixelBufferRef currentFrame;
26@property(nonatomic) int frameWidth;
27@property(nonatomic) int frameHeight;
28@property(nonatomic) BOOL isActive;
29@property(nonatomic) BOOL hasNewFrame;
30@property(nonatomic, strong) NSLock *frameLock;
31@end
32
33@implementation WebcamCaptureDelegate
34
35- (instancetype)init {
36 self = [super init];
37 if (self) {
38 _frameSemaphore = dispatch_semaphore_create(0);
39 _currentFrame = NULL;
40 _frameWidth = 0;
41 _frameHeight = 0;
42 _isActive = YES;
43 _hasNewFrame = NO;
44 _frameLock = [[NSLock alloc] init];
45 }
46 return self;
47}
48
49- (void)dealloc {
50 [_frameLock lock];
51 if (_currentFrame) {
52 CVPixelBufferRelease(_currentFrame);
53 _currentFrame = NULL;
54 }
55 [_frameLock unlock];
56 [_frameLock release];
57 // Release the dispatch semaphore (required under MRC for dispatch objects)
58 if (_frameSemaphore) {
59 dispatch_release(_frameSemaphore);
60 _frameSemaphore = nil;
61 }
62 [super dealloc];
63}
64
65- (void)captureOutput:(AVCaptureOutput *)output
66 didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
67 fromConnection:(AVCaptureConnection *)connection {
68 // Check if delegate is still active to prevent callbacks during cleanup
69 if (!self.isActive) {
70 return;
71 }
72
73 CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
74 if (!pixelBuffer)
75 return;
76
77 [self.frameLock lock];
78
79 // Replace old frame with new one
80 CVPixelBufferRef oldFrame = self.currentFrame;
81 self.currentFrame = CVPixelBufferRetain(pixelBuffer);
82
83 if (oldFrame) {
84 CVPixelBufferRelease(oldFrame);
85 }
86
87 // Update dimensions
88 self.frameWidth = (int)CVPixelBufferGetWidth(pixelBuffer);
89 self.frameHeight = (int)CVPixelBufferGetHeight(pixelBuffer);
90
91 // Always signal semaphore when we get a new frame
92 self.hasNewFrame = YES;
93
94 [self.frameLock unlock];
95
96 dispatch_semaphore_signal(self.frameSemaphore);
97}
98
99- (void)deactivate {
100 self.isActive = NO;
101}
102
103@end
104
105struct webcam_context_t {
106 AVCaptureSession *session;
107 AVCaptureDeviceInput *input;
108 AVCaptureVideoDataOutput *output;
109 WebcamCaptureDelegate *delegate;
110 dispatch_queue_t queue;
111 int width;
112 int height;
113 image_t *cached_frame; // Reusable frame buffer (allocated once, reused for each read)
114 OSType actual_pixel_format; // Track actual format from camera
115};
116
117// Helper function to get supported device types without deprecated ones
118static NSArray *getSupportedDeviceTypes(void) {
119 NSMutableArray *deviceTypes = [NSMutableArray array];
120
121 // Always add built-in camera
122 [deviceTypes addObject:AVCaptureDeviceTypeBuiltInWideAngleCamera];
123
124 // Add continuity camera support for macOS 13.0+
125 if (@available(macOS 13.0, *)) {
126 [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
127 }
128
129 // Add external camera support for macOS 14.0+
130 if (@available(macOS 14.0, *)) {
131 [deviceTypes addObject:AVCaptureDeviceTypeExternal];
132 }
133
134 return [[deviceTypes copy] autorelease];
135}
136
137asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index) {
138 webcam_context_t *context;
139 context = SAFE_MALLOC(sizeof(webcam_context_t), webcam_context_t *);
140 if (!context) {
141 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate webcam context");
142 }
143
144 memset(context, 0, sizeof(webcam_context_t));
145
146 @autoreleasepool {
147 // Create capture session
148 context->session = [[AVCaptureSession alloc] init];
149 if (!context->session) {
150 SAFE_FREE(context);
151 return SET_ERRNO(ERROR_WEBCAM, "Failed to create AVCaptureSession");
152 }
153
154 // Set session preset for quality
155 [context->session setSessionPreset:AVCaptureSessionPreset640x480];
156
157 // Find camera device using newer discovery session API with proper device types
158 AVCaptureDevice *device = nil;
159
160 NSArray *deviceTypes = getSupportedDeviceTypes();
161 AVCaptureDeviceDiscoverySession *discoverySession =
162 [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
163 mediaType:AVMediaTypeVideo
164 position:AVCaptureDevicePositionUnspecified];
165
166 NSArray *devices = discoverySession.devices;
167
168 for (AVCaptureDevice *device in devices) {
169 NSUInteger idx = [devices indexOfObject:device];
170 log_debug("Found device %lu: %s (type: %s)", (unsigned long)idx, [device.localizedName UTF8String],
171 [device.deviceType UTF8String]);
172 }
173
174 if (device_index < [devices count]) {
175 device = [devices objectAtIndex:device_index];
176 } else if ([devices count] > 0) {
177 device = [devices objectAtIndex:0]; // Use first available camera
178 }
179
180 // Fallback to default device if discovery session fails
181 if (!device) {
182 device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
183 }
184
185 if (!device) {
186 log_error("No camera device found at index %d", device_index);
187 [context->session release];
188 SAFE_FREE(context);
189 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
190 }
191
192 // Create device input
193 NSError *error = nil;
194 context->input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
195 if (!context->input || error) {
196 log_error("Failed to create device input: %s", [[error localizedDescription] UTF8String]);
197 [context->session release];
198 SAFE_FREE(context);
199 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
200 }
201
202 // Add input to session
203 if ([context->session canAddInput:context->input]) {
204 [context->session addInput:context->input];
205 } else {
206 log_error("Cannot add input to capture session");
207 [context->input release];
208 [context->session release];
209 SAFE_FREE(context);
210 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
211 }
212
213 // Create video output
214 context->output = [[AVCaptureVideoDataOutput alloc] init];
215 if (!context->output) {
216 log_error("Failed to create video output");
217 [context->input release];
218 [context->session release];
219 SAFE_FREE(context);
220 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
221 }
222
223 // Configure video output for RGB format with fallback support
224 // Try RGB24 first, then fallback to other common formats (UYVY, YUV420, etc)
225 NSArray *preferredFormats = @[
226 @(kCVPixelFormatType_24RGB), // Preferred: 24-bit RGB
227 @(kCVPixelFormatType_422YpCbCr8), // UYVY - very common
228 @(kCVPixelFormatType_420YpCbCr8Planar), // I420/YUV420 (3-plane)
229 @(kCVPixelFormatType_32BGRA), // 32-bit BGRA fallback
230 ];
231
232 NSDictionary *videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey : preferredFormats};
233 [context->output setVideoSettings:videoSettings];
234 log_debug("Configured AVFoundation video output with format priorities: RGB24, UYVY, I420, BGRA");
235
236 // Create delegate and queue
237 context->delegate = [[WebcamCaptureDelegate alloc] init];
238 context->queue = dispatch_queue_create("webcam_capture_queue", DISPATCH_QUEUE_SERIAL);
239
240 [context->output setSampleBufferDelegate:context->delegate queue:context->queue];
241
242 // Add output to session
243 if ([context->session canAddOutput:context->output]) {
244 [context->session addOutput:context->output];
245 } else {
246 log_error("Cannot add output to capture session");
247 [context->delegate release];
248 [context->output release];
249 [context->input release];
250 [context->session release];
251 dispatch_release(context->queue);
252 SAFE_FREE(context);
253 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
254 }
255
256 // Configure frame rate before starting session
257 log_debug("AVFoundation: Attempting to configure frame rate...");
258 AVCaptureDeviceInput *deviceInput = (AVCaptureDeviceInput *)context->input;
259 AVCaptureDevice *captureDevice = [deviceInput device];
260
261 if (captureDevice) {
262 log_debug("AVFoundation: Found device for FPS configuration");
263 NSError *lockError = nil;
264 uint32_t target_fps = (uint32_t)GET_OPTION(fps);
265 if (target_fps == 0)
266 target_fps = 60;
267
268 if ([captureDevice lockForConfiguration:&lockError]) {
269 // Find the best format that supports the target FPS
270 AVCaptureDeviceFormat *bestFormat = nil;
271 AVFrameRateRange *bestRange = nil;
272
273 for (AVCaptureDeviceFormat *format in captureDevice.formats) {
274 for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges) {
275 if (range.minFrameRate <= target_fps && range.maxFrameRate >= target_fps &&
276 (!bestFormat || range.maxFrameRate < bestRange.maxFrameRate)) {
277 bestFormat = format;
278 bestRange = range;
279 }
280 }
281 }
282
283 if (bestFormat && bestRange) {
284 @try {
285 captureDevice.activeFormat = bestFormat;
286 captureDevice.activeVideoMinFrameDuration = CMTimeMake(1, (int32_t)target_fps);
287 captureDevice.activeVideoMaxFrameDuration = CMTimeMake(1, (int32_t)target_fps);
288 log_info("AVFoundation: Configured for %u FPS (device supports %.0f - %.0f FPS)", target_fps,
289 bestRange.minFrameRate, bestRange.maxFrameRate);
290 } @catch (NSException *exception) {
291 log_warn("AVFoundation: Device rejected %u FPS configuration: %s; using its default frame rate", target_fps,
292 [[exception reason] UTF8String]);
293 }
294 } else {
295 // Device doesn't support requested FPS - find the maximum FPS available
296 AVFrameRateRange *maxRange = nil;
297 AVCaptureDeviceFormat *maxFormat = nil;
298 for (AVCaptureDeviceFormat *format in captureDevice.formats) {
299 for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges) {
300 if (!maxRange || range.maxFrameRate > maxRange.maxFrameRate) {
301 maxRange = range;
302 maxFormat = format;
303 }
304 }
305 }
306
307 if (maxFormat && maxRange) {
308 int actual_fps = (int)maxRange.maxFrameRate;
309 @try {
310 captureDevice.activeFormat = maxFormat;
311 captureDevice.activeVideoMinFrameDuration = CMTimeMake(1, actual_fps);
312 captureDevice.activeVideoMaxFrameDuration = CMTimeMake(1, actual_fps);
313 log_warn("Device does not support %u FPS - using maximum available %.0f FPS", target_fps,
314 maxRange.maxFrameRate);
315 } @catch (NSException *exception) {
316 log_warn("AVFoundation: Device rejected maximum frame rate configuration: %s; using its default frame rate",
317 [[exception reason] UTF8String]);
318 }
319 } else {
320 log_warn("Could not determine device FPS capabilities, using device default");
321 }
322 }
323 [captureDevice unlockForConfiguration];
324 } else {
325 log_warn("Could not lock device for FPS configuration: %s", [[lockError localizedDescription] UTF8String]);
326 }
327 }
328
329 // Start the session
330 log_debug("Starting AVFoundation capture session...");
331 [context->session startRunning];
332
333 // Check if session is actually running
334 if (![context->session isRunning]) {
335 log_error("Failed to start capture session - session not running");
336 [context->delegate release];
337 [context->output release];
338 [context->input release];
339 [context->session release];
340 dispatch_release(context->queue);
341 SAFE_FREE(context);
342 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
343 }
344 log_debug("Capture session started successfully");
345
346 // Wait for first frame to get dimensions
347 dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, AVFOUNDATION_INIT_TIMEOUT_NS);
348 if (dispatch_semaphore_wait(context->delegate.frameSemaphore, timeout) == 0) {
349 context->width = context->delegate.frameWidth;
350 context->height = context->delegate.frameHeight;
351 log_debug("AVFoundation webcam initialized: %dx%d, delegate isActive=%d", context->width, context->height,
352 context->delegate.isActive);
353 } else {
354 log_error("Timeout waiting for first frame");
355 [context->session stopRunning];
356 [context->delegate release];
357 [context->output release];
358 [context->input release];
359 [context->session release];
360 dispatch_release(context->queue);
361 SAFE_FREE(context);
362 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
363 }
364 }
365
366 *ctx = context;
367 return ASCIICHAT_OK;
368}
369
378static BOOL convert_pixel_buffer_to_rgb24(CVPixelBufferRef pixelBuffer, rgb_pixel_t *dest_pixels, int width,
379 int height) {
380 OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
381
382 if (CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess) {
383 log_error("Failed to lock pixel buffer for format 0x%x", pixelFormat);
384 return FALSE;
385 }
386
387 uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer);
388 size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
389
390 BOOL success = FALSE;
391
392 switch (pixelFormat) {
393 case kCVPixelFormatType_24RGB:
394 // Direct copy - already RGB24
395 for (int y = 0; y < height; y++) {
396 memcpy(&dest_pixels[y * width], &baseAddress[y * bytesPerRow], width * 3);
397 }
398 success = TRUE;
399 break;
400
401 case kCVPixelFormatType_32BGRA: {
402 // Convert BGRA to RGB (skip alpha channel)
403 for (int y = 0; y < height; y++) {
404 uint8_t *src = &baseAddress[y * bytesPerRow];
405 rgb_pixel_t *dst = &dest_pixels[y * width];
406 for (int x = 0; x < width; x++) {
407 dst[x].b = src[x * 4 + 0]; // B
408 dst[x].g = src[x * 4 + 1]; // G
409 dst[x].r = src[x * 4 + 2]; // R
410 // Skip alpha at src[x*4 + 3]
411 }
412 }
413 success = TRUE;
414 break;
415 }
416
417 case kCVPixelFormatType_422YpCbCr8: {
418 // UYVY format: U Y V Y (packed)
419 for (int y = 0; y < height; y++) {
420 uint8_t *src = &baseAddress[y * bytesPerRow];
421 rgb_pixel_t *dst = &dest_pixels[y * width];
422
423 for (int x = 0; x < width; x += 2) {
424 int u = src[x * 2 + 0] - 128;
425 int y0 = src[x * 2 + 1] - 16;
426 int v = src[x * 2 + 2] - 128;
427 int y1 = src[x * 2 + 3] - 16;
428
429 // Convert YUV to RGB
430 int r0 = (298 * y0 + 409 * v + 128) >> 8;
431 int g0 = (298 * y0 - 100 * u - 208 * v + 128) >> 8;
432 int b0 = (298 * y0 + 516 * u + 128) >> 8;
433
434 dst[x].r = (uint8_t)(r0 < 0 ? 0 : (r0 > 255 ? 255 : r0));
435 dst[x].g = (uint8_t)(g0 < 0 ? 0 : (g0 > 255 ? 255 : g0));
436 dst[x].b = (uint8_t)(b0 < 0 ? 0 : (b0 > 255 ? 255 : b0));
437
438 int r1 = (298 * y1 + 409 * v + 128) >> 8;
439 int g1 = (298 * y1 - 100 * u - 208 * v + 128) >> 8;
440 int b1 = (298 * y1 + 516 * u + 128) >> 8;
441
442 dst[x + 1].r = (uint8_t)(r1 < 0 ? 0 : (r1 > 255 ? 255 : r1));
443 dst[x + 1].g = (uint8_t)(g1 < 0 ? 0 : (g1 > 255 ? 255 : g1));
444 dst[x + 1].b = (uint8_t)(b1 < 0 ? 0 : (b1 > 255 ? 255 : b1));
445 }
446 }
447 success = TRUE;
448 break;
449 }
450
451 case kCVPixelFormatType_420YpCbCr8Planar: {
452 // I420 planar format
453 size_t plane_width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
454 size_t plane_height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
455
456 uint8_t *y_data = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
457 uint8_t *u_data = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
458 uint8_t *v_data = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2);
459
460 size_t y_stride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
461 size_t u_stride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
462 size_t v_stride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2);
463
464 for (int y = 0; y < (int)plane_height; y++) {
465 rgb_pixel_t *dst = &dest_pixels[y * width];
466
467 for (int x = 0; x < (int)plane_width; x++) {
468 int y_val = y_data[y * y_stride + x] - 16;
469 int u_val = u_data[(y / 2) * u_stride + (x / 2)] - 128;
470 int v_val = v_data[(y / 2) * v_stride + (x / 2)] - 128;
471
472 int r = (298 * y_val + 409 * v_val + 128) >> 8;
473 int g = (298 * y_val - 100 * u_val - 208 * v_val + 128) >> 8;
474 int b = (298 * y_val + 516 * u_val + 128) >> 8;
475
476 dst[x].r = (uint8_t)(r < 0 ? 0 : (r > 255 ? 255 : r));
477 dst[x].g = (uint8_t)(g < 0 ? 0 : (g > 255 ? 255 : g));
478 dst[x].b = (uint8_t)(b < 0 ? 0 : (b > 255 ? 255 : b));
479 }
480 }
481 success = TRUE;
482 break;
483 }
484
485 default:
486 log_error("Unsupported pixel format from camera: 0x%x", pixelFormat);
487 break;
488 }
489
490 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
491
492 return success;
493}
494
496 (void)ctx;
497 // No-op on macOS - AVFoundation handles frame buffering internally
498}
499
501 if (!ctx)
502 return;
503
504 @autoreleasepool {
505 // First, deactivate delegate to reject new callbacks
506 if (ctx->delegate) {
507 [ctx->delegate deactivate];
508 }
509
510 // Remove delegate to prevent any further callbacks
511 if (ctx->output) {
512 [ctx->output setSampleBufferDelegate:nil queue:nil];
513 }
514
515 // Now stop the session
516 if (ctx->session && [ctx->session isRunning]) {
517 [ctx->session stopRunning];
518
519 // Wait longer for session to fully stop
520 usleep(200000); // 200ms - give more time for cleanup
521 }
522
523 // Clean up session and associated objects
524 if (ctx->session) {
525 if (ctx->input && [ctx->session.inputs containsObject:ctx->input]) {
526 [ctx->session removeInput:ctx->input];
527 }
528 if (ctx->output && [ctx->session.outputs containsObject:ctx->output]) {
529 [ctx->session removeOutput:ctx->output];
530 }
531 [ctx->session release];
532 ctx->session = nil;
533 }
534
535 if (ctx->input) {
536 [ctx->input release];
537 ctx->input = nil;
538 }
539
540 if (ctx->output) {
541 [ctx->output release];
542 ctx->output = nil;
543 }
544
545 if (ctx->delegate) {
546 [ctx->delegate release];
547 ctx->delegate = nil;
548 }
549
550 if (ctx->queue) {
551 dispatch_release(ctx->queue);
552 ctx->queue = nil;
553 }
554 }
555
556 // Free cached frame if it was allocated
557 if (ctx->cached_frame) {
558 image_destroy(ctx->cached_frame);
559 ctx->cached_frame = NULL;
560 }
561
562 SAFE_FREE(ctx);
563 // log_info("AVFoundation webcam cleaned up");
564}
565
567 if (!ctx || !ctx->delegate || !ctx->delegate.isActive)
568 return NULL;
569
570 @autoreleasepool {
571 // Wait for a frame (configurable timeout for different camera speeds)
572 dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, AVFOUNDATION_FRAME_TIMEOUT_NS);
573 if (dispatch_semaphore_wait(ctx->delegate.frameSemaphore, timeout) != 0) {
574 log_error("No webcam frame available within timeout: %0.2f seconds",
575 AVFOUNDATION_FRAME_TIMEOUT_NS / NSEC_PER_SEC);
576 return NULL; // No frame available within timeout
577 }
578
579 // Reset the new frame flag so we can get the next semaphore signal
580 ctx->delegate.hasNewFrame = NO;
581
582 // Lock to safely access the current frame
583 [ctx->delegate.frameLock lock];
584
585 CVPixelBufferRef pixelBuffer = ctx->delegate.currentFrame;
586 if (!pixelBuffer) {
587 [ctx->delegate.frameLock unlock];
588 return NULL;
589 }
590
591 // Retain the buffer while we're using it
592 CVPixelBufferRetain(pixelBuffer);
593
594 [ctx->delegate.frameLock unlock];
595
596 // Lock the pixel buffer
597 if (CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess) {
598 log_error("Failed to lock pixel buffer");
599 CVPixelBufferRelease(pixelBuffer);
600 return NULL;
601 }
602
603 // Get buffer info
604 size_t width = CVPixelBufferGetWidth(pixelBuffer);
605 size_t height = CVPixelBufferGetHeight(pixelBuffer);
606 uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer);
607
608 if (!baseAddress) {
609 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
610 CVPixelBufferRelease(pixelBuffer); // Release our retained reference
611 log_error("Failed to get pixel buffer base address");
612 return NULL;
613 }
614
615 // Allocate or reuse cached frame (allocated once, reused for each call)
616 // This matches the documented API contract: "Subsequent calls reuse the same buffer"
617 if (!ctx->cached_frame) {
618 ctx->cached_frame = image_new((int)width, (int)height);
619 if (!ctx->cached_frame) {
620 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
621 CVPixelBufferRelease(pixelBuffer); // Release our retained reference
622 log_error("Failed to allocate image buffer");
623 return NULL;
624 }
625 }
626
627 image_t *img = ctx->cached_frame;
628
629 // Get actual pixel format from camera
630 OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
631 if (ctx->actual_pixel_format != pixelFormat) {
632 ctx->actual_pixel_format = pixelFormat;
633 const char *format_name = "Unknown";
634 switch (pixelFormat) {
635 case kCVPixelFormatType_24RGB:
636 format_name = "RGB24";
637 break;
638 case kCVPixelFormatType_32BGRA:
639 format_name = "BGRA32";
640 break;
641 case kCVPixelFormatType_422YpCbCr8:
642 format_name = "UYVY";
643 break;
644 case kCVPixelFormatType_420YpCbCr8Planar:
645 format_name = "I420";
646 break;
647 }
648 log_debug("Camera delivering pixels in %s format (0x%x)", format_name, pixelFormat);
649 }
650
651 // Convert pixel buffer to RGB24 (handles format conversion if needed)
652 if (!convert_pixel_buffer_to_rgb24(pixelBuffer, img->pixels, (int)width, (int)height)) {
653 CVPixelBufferRelease(pixelBuffer);
654 log_error("Failed to convert pixel buffer to RGB24");
655 return NULL;
656 }
657
658 // Release our retained reference
659 CVPixelBufferRelease(pixelBuffer);
660
661 return img;
662 }
663}
664
665asciichat_error_t webcam_get_dimensions(webcam_context_t *ctx, int *width, int *height) {
666 if (!ctx || !width || !height)
667 return ERROR_INVALID_PARAM;
668
669 *width = ctx->width;
670 *height = ctx->height;
671 return ASCIICHAT_OK;
672}
673
674asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count) {
675 if (!out_devices || !out_count) {
676 return SET_ERRNO(ERROR_INVALID_PARAM, "webcam_list_devices: invalid parameters");
677 }
678
679 *out_devices = NULL;
680 *out_count = 0;
681
682 @autoreleasepool {
683 // Get all video capture devices using the helper that handles deprecated types
684 NSArray *deviceTypes = getSupportedDeviceTypes();
685 AVCaptureDeviceDiscoverySession *discoverySession =
686 [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
687 mediaType:AVMediaTypeVideo
688 position:AVCaptureDevicePositionUnspecified];
689
690 NSArray<AVCaptureDevice *> *av_devices = discoverySession.devices;
691 NSUInteger device_count = [av_devices count];
692
693 if (device_count == 0) {
694 // No devices found - not an error
695 return ASCIICHAT_OK;
696 }
697
698 // Allocate device array
699 webcam_device_info_t *devices =
700 SAFE_CALLOC((size_t)device_count, sizeof(webcam_device_info_t), webcam_device_info_t *);
701 if (!devices) {
702 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate device info array");
703 }
704
705 // Populate device info
706 for (NSUInteger i = 0; i < device_count; i++) {
707 AVCaptureDevice *device = av_devices[i];
708 devices[i].index = (unsigned int)i;
709
710 const char *name = [[device localizedName] UTF8String];
711 if (name) {
712 SAFE_STRNCPY(devices[i].name, name, WEBCAM_DEVICE_NAME_MAX);
713 } else {
714 SAFE_STRNCPY(devices[i].name, "<Unknown>", WEBCAM_DEVICE_NAME_MAX);
715 }
716 }
717
718 *out_devices = devices;
719 *out_count = (unsigned int)device_count;
720 }
721
722 return ASCIICHAT_OK;
723}
724
726 SAFE_FREE(devices);
727}
728
729#endif // __APPLE__
⚙️ Common definitions, error codes, macros, and types shared throughout the application
unsigned int uint32_t
Definition common.h:58
#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 char uint8_t
Definition common.h:56
#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_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 GET_OPTION(field)
Safely get a specific option field (lock-free read)
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
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.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Image structure.
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
🖼️ Safe overflow-checked buffer size calculations for images and video frames