ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
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
14#include "video/webcam/webcam.h"
15#include "common.h"
16#include "util/image.h"
17
18// AVFoundation timeout configuration
19#define AVFOUNDATION_FRAME_TIMEOUT_NS 500000000 // 500ms timeout (adjustable for slow cameras)
20#define AVFOUNDATION_INIT_TIMEOUT_NS (3 * NSEC_PER_SEC) // 3 second timeout for initialization
21
22@interface WebcamCaptureDelegate : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
23@property(nonatomic, strong) dispatch_semaphore_t frameSemaphore;
24@property(nonatomic) CVPixelBufferRef currentFrame;
25@property(nonatomic) int frameWidth;
26@property(nonatomic) int frameHeight;
27@property(nonatomic) BOOL isActive;
28@property(nonatomic) BOOL hasNewFrame;
29@property(nonatomic, strong) NSLock *frameLock;
30@end
31
32@implementation WebcamCaptureDelegate
33
34- (instancetype)init {
35 self = [super init];
36 if (self) {
37 _frameSemaphore = dispatch_semaphore_create(0);
38 _currentFrame = NULL;
39 _frameWidth = 0;
40 _frameHeight = 0;
41 _isActive = YES;
42 _hasNewFrame = NO;
43 _frameLock = [[NSLock alloc] init];
44 }
45 return self;
46}
47
48- (void)dealloc {
49 [_frameLock lock];
50 if (_currentFrame) {
51 CVPixelBufferRelease(_currentFrame);
52 _currentFrame = NULL;
53 }
54 [_frameLock unlock];
55 [_frameLock release];
56 // Release the dispatch semaphore (required under MRC for dispatch objects)
57 if (_frameSemaphore) {
58 dispatch_release(_frameSemaphore);
59 _frameSemaphore = nil;
60 }
61 [super dealloc];
62}
63
64- (void)captureOutput:(AVCaptureOutput *)output
65 didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
66 fromConnection:(AVCaptureConnection *)connection {
67 // Check if delegate is still active to prevent callbacks during cleanup
68 if (!self.isActive) {
69 return;
70 }
71
72 CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
73 if (!pixelBuffer)
74 return;
75
76 [self.frameLock lock];
77
78 // Replace old frame with new one
79 CVPixelBufferRef oldFrame = self.currentFrame;
80 self.currentFrame = CVPixelBufferRetain(pixelBuffer);
81
82 if (oldFrame) {
83 CVPixelBufferRelease(oldFrame);
84 }
85
86 // Update dimensions
87 self.frameWidth = (int)CVPixelBufferGetWidth(pixelBuffer);
88 self.frameHeight = (int)CVPixelBufferGetHeight(pixelBuffer);
89
90 // Always signal semaphore when we get a new frame
91 self.hasNewFrame = YES;
92
93 [self.frameLock unlock];
94
95 dispatch_semaphore_signal(self.frameSemaphore);
96}
97
98- (void)deactivate {
99 self.isActive = NO;
100}
101
102@end
103
104struct webcam_context_t {
105 AVCaptureSession *session;
106 AVCaptureDeviceInput *input;
107 AVCaptureVideoDataOutput *output;
108 WebcamCaptureDelegate *delegate;
109 dispatch_queue_t queue;
110 int width;
111 int height;
112};
113
114// Helper function to get supported device types without deprecated ones
115static NSArray *getSupportedDeviceTypes(void) {
116 NSMutableArray *deviceTypes = [NSMutableArray array];
117
118 // Always add built-in camera
119 [deviceTypes addObject:AVCaptureDeviceTypeBuiltInWideAngleCamera];
120
121 // Add continuity camera support for macOS 13.0+
122 if (@available(macOS 13.0, *)) {
123 [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
124 }
125
126 // Add external camera support for macOS 14.0+
127 if (@available(macOS 14.0, *)) {
128 [deviceTypes addObject:AVCaptureDeviceTypeExternal];
129 }
130
131 return [[deviceTypes copy] autorelease];
132}
133
134asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index) {
135 webcam_context_t *context;
136 context = SAFE_MALLOC(sizeof(webcam_context_t), webcam_context_t *);
137 if (!context) {
138 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate webcam context");
139 }
140
141 memset(context, 0, sizeof(webcam_context_t));
142
143 @autoreleasepool {
144 // Create capture session
145 context->session = [[AVCaptureSession alloc] init];
146 if (!context->session) {
147 SAFE_FREE(context);
148 return SET_ERRNO(ERROR_WEBCAM, "Failed to create AVCaptureSession");
149 }
150
151 // Set session preset for quality
152 [context->session setSessionPreset:AVCaptureSessionPreset640x480];
153
154 // Find camera device using newer discovery session API with proper device types
155 AVCaptureDevice *device = nil;
156
157 NSArray *deviceTypes = getSupportedDeviceTypes();
158 AVCaptureDeviceDiscoverySession *discoverySession =
159 [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
160 mediaType:AVMediaTypeVideo
161 position:AVCaptureDevicePositionUnspecified];
162
163 NSArray *devices = discoverySession.devices;
164
165 for (AVCaptureDevice *device in devices) {
166 NSUInteger idx = [devices indexOfObject:device];
167 log_info("Found device %lu: %s (type: %s)", (unsigned long)idx, [device.localizedName UTF8String],
168 [device.deviceType UTF8String]);
169 }
170
171 if (device_index < [devices count]) {
172 device = [devices objectAtIndex:device_index];
173 } else if ([devices count] > 0) {
174 device = [devices objectAtIndex:0]; // Use first available camera
175 }
176
177 // Fallback to default device if discovery session fails
178 if (!device) {
179 device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
180 }
181
182 if (!device) {
183 log_error("No camera device found at index %d", device_index);
184 [context->session release];
185 SAFE_FREE(context);
186 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
187 }
188
189 // Create device input
190 NSError *error = nil;
191 context->input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
192 if (!context->input || error) {
193 log_error("Failed to create device input: %s", [[error localizedDescription] UTF8String]);
194 [context->session release];
195 SAFE_FREE(context);
196 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
197 }
198
199 // Add input to session
200 if ([context->session canAddInput:context->input]) {
201 [context->session addInput:context->input];
202 } else {
203 log_error("Cannot add input to capture session");
204 [context->input release];
205 [context->session release];
206 SAFE_FREE(context);
207 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
208 }
209
210 // Create video output
211 context->output = [[AVCaptureVideoDataOutput alloc] init];
212 if (!context->output) {
213 log_error("Failed to create video output");
214 [context->input release];
215 [context->session release];
216 SAFE_FREE(context);
217 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
218 }
219
220 // Configure video output for RGB format
221 NSDictionary *videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_24RGB)};
222 [context->output setVideoSettings:videoSettings];
223
224 // Create delegate and queue
225 context->delegate = [[WebcamCaptureDelegate alloc] init];
226 context->queue = dispatch_queue_create("webcam_capture_queue", DISPATCH_QUEUE_SERIAL);
227
228 [context->output setSampleBufferDelegate:context->delegate queue:context->queue];
229
230 // Add output to session
231 if ([context->session canAddOutput:context->output]) {
232 [context->session addOutput:context->output];
233 } else {
234 log_error("Cannot add output to capture session");
235 [context->delegate release];
236 [context->output release];
237 [context->input release];
238 [context->session release];
239 dispatch_release(context->queue);
240 SAFE_FREE(context);
241 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
242 }
243
244 // Start the session
245 log_info("Starting AVFoundation capture session...");
246 [context->session startRunning];
247
248 // Check if session is actually running
249 if (![context->session isRunning]) {
250 log_error("Failed to start capture session - session not running");
251 [context->delegate release];
252 [context->output release];
253 [context->input release];
254 [context->session release];
255 dispatch_release(context->queue);
256 SAFE_FREE(context);
257 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
258 }
259 log_info("Capture session started successfully");
260
261 // Wait for first frame to get dimensions
262 dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, AVFOUNDATION_INIT_TIMEOUT_NS);
263 if (dispatch_semaphore_wait(context->delegate.frameSemaphore, timeout) == 0) {
264 context->width = context->delegate.frameWidth;
265 context->height = context->delegate.frameHeight;
266 log_info("AVFoundation webcam initialized: %dx%d, delegate isActive=%d", context->width, context->height,
267 context->delegate.isActive);
268 } else {
269 log_error("Timeout waiting for first frame");
270 [context->session stopRunning];
271 [context->delegate release];
272 [context->output release];
273 [context->input release];
274 [context->session release];
275 dispatch_release(context->queue);
276 SAFE_FREE(context);
277 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize webcam");
278 }
279 }
280
281 *ctx = context;
282 return ASCIICHAT_OK;
283}
284
286 (void)ctx;
287 // No-op on macOS - AVFoundation handles frame buffering internally
288}
289
291 if (!ctx)
292 return;
293
294 @autoreleasepool {
295 // First, deactivate delegate to reject new callbacks
296 if (ctx->delegate) {
297 [ctx->delegate deactivate];
298 }
299
300 // Remove delegate to prevent any further callbacks
301 if (ctx->output) {
302 [ctx->output setSampleBufferDelegate:nil queue:nil];
303 }
304
305 // Now stop the session
306 if (ctx->session && [ctx->session isRunning]) {
307 [ctx->session stopRunning];
308
309 // Wait longer for session to fully stop
310 usleep(200000); // 200ms - give more time for cleanup
311 }
312
313 // Clean up session and associated objects
314 if (ctx->session) {
315 if (ctx->input && [ctx->session.inputs containsObject:ctx->input]) {
316 [ctx->session removeInput:ctx->input];
317 }
318 if (ctx->output && [ctx->session.outputs containsObject:ctx->output]) {
319 [ctx->session removeOutput:ctx->output];
320 }
321 [ctx->session release];
322 ctx->session = nil;
323 }
324
325 if (ctx->input) {
326 [ctx->input release];
327 ctx->input = nil;
328 }
329
330 if (ctx->output) {
331 [ctx->output release];
332 ctx->output = nil;
333 }
334
335 if (ctx->delegate) {
336 [ctx->delegate release];
337 ctx->delegate = nil;
338 }
339
340 if (ctx->queue) {
341 dispatch_release(ctx->queue);
342 ctx->queue = nil;
343 }
344 }
345
346 SAFE_FREE(ctx);
347 // log_info("AVFoundation webcam cleaned up");
348}
349
351 if (!ctx || !ctx->delegate || !ctx->delegate.isActive)
352 return NULL;
353
354 @autoreleasepool {
355 // Wait for a frame (configurable timeout for different camera speeds)
356 dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, AVFOUNDATION_FRAME_TIMEOUT_NS);
357 if (dispatch_semaphore_wait(ctx->delegate.frameSemaphore, timeout) != 0) {
358 log_error("No webcam frame available within timeout: %0.2f seconds",
359 AVFOUNDATION_FRAME_TIMEOUT_NS / NSEC_PER_SEC);
360 return NULL; // No frame available within timeout
361 }
362
363 // Reset the new frame flag so we can get the next semaphore signal
364 ctx->delegate.hasNewFrame = NO;
365
366 // Lock to safely access the current frame
367 [ctx->delegate.frameLock lock];
368
369 CVPixelBufferRef pixelBuffer = ctx->delegate.currentFrame;
370 if (!pixelBuffer) {
371 [ctx->delegate.frameLock unlock];
372 return NULL;
373 }
374
375 // Retain the buffer while we're using it
376 CVPixelBufferRetain(pixelBuffer);
377
378 [ctx->delegate.frameLock unlock];
379
380 // Lock the pixel buffer
381 if (CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess) {
382 log_error("Failed to lock pixel buffer");
383 CVPixelBufferRelease(pixelBuffer);
384 return NULL;
385 }
386
387 // Get buffer info
388 size_t width = CVPixelBufferGetWidth(pixelBuffer);
389 size_t height = CVPixelBufferGetHeight(pixelBuffer);
390 size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
391 uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer);
392
393 if (!baseAddress) {
394 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
395 CVPixelBufferRelease(pixelBuffer); // Release our retained reference
396 log_error("Failed to get pixel buffer base address");
397 return NULL;
398 }
399
400 // Create image_t structure
401 image_t *img = image_new((int)width, (int)height);
402 if (!img) {
403 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
404 CVPixelBufferRelease(pixelBuffer); // Release our retained reference
405 log_error("Failed to allocate image buffer");
406 return NULL;
407 }
408
409 // Copy pixel data (handle potential row padding)
410 // Calculate frame size with overflow checking
411 size_t frame_size;
412 if (image_calc_rgb_size(width, height, &frame_size) != ASCIICHAT_OK) {
413 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
414 CVPixelBufferRelease(pixelBuffer);
415 image_destroy(img);
416 log_error("Failed to calculate frame size: width=%zu, height=%zu (would overflow)", width, height);
417 return NULL;
418 }
419
420 size_t row_size;
421 if (image_calc_rgb_size(width, 1, &row_size) != ASCIICHAT_OK) {
422 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
423 CVPixelBufferRelease(pixelBuffer);
424 image_destroy(img);
425 log_error("Failed to calculate row size: width=%zu (would overflow)", width);
426 return NULL;
427 }
428
429 if (bytesPerRow == row_size) {
430 // No padding, direct copy
431 memcpy(img->pixels, baseAddress, frame_size);
432 } else {
433 // Copy row by row to handle padding
434 for (size_t y = 0; y < height; y++) {
435 memcpy(&img->pixels[y * row_size], &baseAddress[y * bytesPerRow], row_size);
436 }
437 }
438
439 // Unlock the pixel buffer
440 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
441
442 // Release our retained reference
443 CVPixelBufferRelease(pixelBuffer);
444
445 return img;
446 }
447}
448
449asciichat_error_t webcam_get_dimensions(webcam_context_t *ctx, int *width, int *height) {
450 if (!ctx || !width || !height)
451 return ERROR_INVALID_PARAM;
452
453 *width = ctx->width;
454 *height = ctx->height;
455 return ASCIICHAT_OK;
456}
457
458asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count) {
459 if (!out_devices || !out_count) {
460 return SET_ERRNO(ERROR_INVALID_PARAM, "webcam_list_devices: invalid parameters");
461 }
462
463 *out_devices = NULL;
464 *out_count = 0;
465
466 @autoreleasepool {
467 // Get all video capture devices using the helper that handles deprecated types
468 NSArray *deviceTypes = getSupportedDeviceTypes();
469 AVCaptureDeviceDiscoverySession *discoverySession =
470 [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
471 mediaType:AVMediaTypeVideo
472 position:AVCaptureDevicePositionUnspecified];
473
474 NSArray<AVCaptureDevice *> *av_devices = discoverySession.devices;
475 NSUInteger device_count = [av_devices count];
476
477 if (device_count == 0) {
478 // No devices found - not an error
479 return ASCIICHAT_OK;
480 }
481
482 // Allocate device array
483 webcam_device_info_t *devices =
484 SAFE_CALLOC((size_t)device_count, sizeof(webcam_device_info_t), webcam_device_info_t *);
485 if (!devices) {
486 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate device info array");
487 }
488
489 // Populate device info
490 for (NSUInteger i = 0; i < device_count; i++) {
491 AVCaptureDevice *device = av_devices[i];
492 devices[i].index = (unsigned int)i;
493
494 const char *name = [[device localizedName] UTF8String];
495 if (name) {
496 SAFE_STRNCPY(devices[i].name, name, WEBCAM_DEVICE_NAME_MAX);
497 } else {
498 SAFE_STRNCPY(devices[i].name, "<Unknown>", WEBCAM_DEVICE_NAME_MAX);
499 }
500 }
501
502 *out_devices = devices;
503 *out_count = (unsigned int)device_count;
504 }
505
506 return ASCIICHAT_OK;
507}
508
510 SAFE_FREE(devices);
511}
512
513#endif // __APPLE__
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
#define SAFE_CALLOC(count, size, cast)
Definition common.h:218
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ERROR_MEMORY
Definition error_codes.h:53
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_INVALID_PARAM
@ ERROR_WEBCAM
Definition error_codes.h:61
#define log_error(...)
Log an ERROR message.
#define log_info(...)
Log an INFO message.
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
Definition video/image.c:85
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
Definition video/image.c:36
asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index)
Initialize webcam context for advanced operations.
Definition webcam.c:306
asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count)
Enumerate available webcam devices.
Definition webcam.c:336
void webcam_free_device_list(webcam_device_info_t *devices)
Free device list returned by webcam_list_devices()
Definition webcam.c:344
void webcam_flush_context(webcam_context_t *ctx)
Flush/interrupt pending read operations on webcam context.
Definition webcam.c:318
struct webcam_context_t webcam_context_t
Opaque webcam context structure.
Definition webcam.h:153
asciichat_error_t webcam_get_dimensions(webcam_context_t *ctx, int *width, int *height)
Get webcam frame dimensions.
Definition webcam.c:329
#define WEBCAM_DEVICE_NAME_MAX
Maximum length of webcam device name.
Definition webcam.h:77
image_t * webcam_read_context(webcam_context_t *ctx)
Capture a frame from webcam context.
Definition webcam.c:323
void webcam_cleanup_context(webcam_context_t *ctx)
Clean up webcam context and release resources.
Definition webcam.c:313
Image structure.
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order)
Webcam device information structure.
Definition webcam.h:89
unsigned int index
Device index (use with webcam_init)
Definition webcam.h:90
asciichat_error_t image_calc_rgb_size(size_t width, size_t height, size_t *out_size)
Calculate total RGB buffer size from dimensions.
Definition util/image.c:54
🖼️ Safe overflow-checked buffer size calculations for images and video frames