9#import <AVFoundation/AVFoundation.h>
10#import <CoreMedia/CoreMedia.h>
11#import <CoreVideo/CoreVideo.h>
12#include <dispatch/dispatch.h>
14#include <ascii-chat/video/webcam/webcam.h>
15#include <ascii-chat/common.h>
16#include <ascii-chat/util/image.h>
19#define AVFOUNDATION_FRAME_TIMEOUT_NS 500000000
20#define AVFOUNDATION_INIT_TIMEOUT_NS (3 * NSEC_PER_SEC)
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;
32@implementation WebcamCaptureDelegate
37 _frameSemaphore = dispatch_semaphore_create(0);
43 _frameLock = [[NSLock alloc] init];
51 CVPixelBufferRelease(_currentFrame);
57 if (_frameSemaphore) {
58 dispatch_release(_frameSemaphore);
59 _frameSemaphore = nil;
64- (void)captureOutput:(AVCaptureOutput *)output
65 didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
66 fromConnection:(AVCaptureConnection *)connection {
72 CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
76 [
self.frameLock lock];
79 CVPixelBufferRef oldFrame =
self.currentFrame;
80 self.currentFrame = CVPixelBufferRetain(pixelBuffer);
83 CVPixelBufferRelease(oldFrame);
87 self.frameWidth = (int)CVPixelBufferGetWidth(pixelBuffer);
88 self.frameHeight = (int)CVPixelBufferGetHeight(pixelBuffer);
91 self.hasNewFrame = YES;
93 [
self.frameLock unlock];
95 dispatch_semaphore_signal(
self.frameSemaphore);
104struct webcam_context_t {
105 AVCaptureSession *session;
106 AVCaptureDeviceInput *input;
107 AVCaptureVideoDataOutput *output;
108 WebcamCaptureDelegate *delegate;
109 dispatch_queue_t queue;
112 image_t *cached_frame;
116static NSArray *getSupportedDeviceTypes(
void) {
117 NSMutableArray *deviceTypes = [NSMutableArray array];
120 [deviceTypes addObject:AVCaptureDeviceTypeBuiltInWideAngleCamera];
123 if (@available(macOS 13.0, *)) {
124 [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
128 if (@available(macOS 14.0, *)) {
129 [deviceTypes addObject:AVCaptureDeviceTypeExternal];
132 return [[deviceTypes copy] autorelease];
135asciichat_error_t
webcam_init_context(webcam_context_t **ctx,
unsigned short int device_index) {
136 webcam_context_t *context;
137 context = SAFE_MALLOC(
sizeof(webcam_context_t), webcam_context_t *);
139 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate webcam context");
142 memset(context, 0,
sizeof(webcam_context_t));
146 context->session = [[AVCaptureSession alloc] init];
147 if (!context->session) {
149 return SET_ERRNO(ERROR_WEBCAM,
"Failed to create AVCaptureSession");
153 [context->session setSessionPreset:AVCaptureSessionPreset640x480];
156 AVCaptureDevice *device = nil;
158 NSArray *deviceTypes = getSupportedDeviceTypes();
159 AVCaptureDeviceDiscoverySession *discoverySession =
160 [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
161 mediaType:AVMediaTypeVideo
162 position:AVCaptureDevicePositionUnspecified];
164 NSArray *devices = discoverySession.devices;
166 for (AVCaptureDevice *device in devices) {
167 NSUInteger idx = [devices indexOfObject:device];
168 log_debug(
"Found device %lu: %s (type: %s)", (
unsigned long)idx, [device.localizedName UTF8String],
169 [device.deviceType UTF8String]);
172 if (device_index < [devices count]) {
173 device = [devices objectAtIndex:device_index];
174 }
else if ([devices count] > 0) {
175 device = [devices objectAtIndex:0];
180 device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
184 log_error(
"No camera device found at index %d", device_index);
185 [context->session release];
187 return SET_ERRNO(ERROR_WEBCAM,
"Failed to initialize webcam");
191 NSError *error = nil;
192 context->input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
193 if (!context->input || error) {
194 log_error(
"Failed to create device input: %s", [[error localizedDescription] UTF8String]);
195 [context->session release];
197 return SET_ERRNO(ERROR_WEBCAM,
"Failed to initialize webcam");
201 if ([context->session canAddInput:context->input]) {
202 [context->session addInput:context->input];
204 log_error(
"Cannot add input to capture session");
205 [context->input release];
206 [context->session release];
208 return SET_ERRNO(ERROR_WEBCAM,
"Failed to initialize webcam");
212 context->output = [[AVCaptureVideoDataOutput alloc] init];
213 if (!context->output) {
214 log_error(
"Failed to create video output");
215 [context->input release];
216 [context->session release];
218 return SET_ERRNO(ERROR_WEBCAM,
"Failed to initialize webcam");
222 NSDictionary *videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_24RGB)};
223 [context->output setVideoSettings:videoSettings];
226 context->delegate = [[WebcamCaptureDelegate alloc] init];
227 context->queue = dispatch_queue_create(
"webcam_capture_queue", DISPATCH_QUEUE_SERIAL);
229 [context->output setSampleBufferDelegate:context->delegate queue:context->queue];
232 if ([context->session canAddOutput:context->output]) {
233 [context->session addOutput:context->output];
235 log_error(
"Cannot add output to capture session");
236 [context->delegate release];
237 [context->output release];
238 [context->input release];
239 [context->session release];
240 dispatch_release(context->queue);
242 return SET_ERRNO(ERROR_WEBCAM,
"Failed to initialize webcam");
246 log_debug(
"Starting AVFoundation capture session...");
247 [context->session startRunning];
250 if (![context->session isRunning]) {
251 log_error(
"Failed to start capture session - session not running");
252 [context->delegate release];
253 [context->output release];
254 [context->input release];
255 [context->session release];
256 dispatch_release(context->queue);
258 return SET_ERRNO(ERROR_WEBCAM,
"Failed to initialize webcam");
260 log_debug(
"Capture session started successfully");
263 dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, AVFOUNDATION_INIT_TIMEOUT_NS);
264 if (dispatch_semaphore_wait(context->delegate.frameSemaphore, timeout) == 0) {
265 context->width = context->delegate.frameWidth;
266 context->height = context->delegate.frameHeight;
267 log_debug(
"AVFoundation webcam initialized: %dx%d, delegate isActive=%d", context->width, context->height,
268 context->delegate.isActive);
270 log_error(
"Timeout waiting for first frame");
271 [context->session stopRunning];
272 [context->delegate release];
273 [context->output release];
274 [context->input release];
275 [context->session release];
276 dispatch_release(context->queue);
278 return SET_ERRNO(ERROR_WEBCAM,
"Failed to initialize webcam");
298 [ctx->delegate deactivate];
303 [ctx->output setSampleBufferDelegate:nil queue:nil];
307 if (ctx->session && [ctx->session isRunning]) {
308 [ctx->session stopRunning];
316 if (ctx->input && [ctx->session.inputs containsObject:ctx->input]) {
317 [ctx->session removeInput:ctx->input];
319 if (ctx->output && [ctx->session.outputs containsObject:ctx->output]) {
320 [ctx->session removeOutput:ctx->output];
322 [ctx->session release];
327 [ctx->input release];
332 [ctx->output release];
337 [ctx->delegate release];
342 dispatch_release(ctx->queue);
348 if (ctx->cached_frame) {
350 ctx->cached_frame = NULL;
358 if (!ctx || !ctx->delegate || !ctx->delegate.isActive)
363 dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, AVFOUNDATION_FRAME_TIMEOUT_NS);
364 if (dispatch_semaphore_wait(ctx->delegate.frameSemaphore, timeout) != 0) {
365 log_error(
"No webcam frame available within timeout: %0.2f seconds",
366 AVFOUNDATION_FRAME_TIMEOUT_NS / NSEC_PER_SEC);
371 ctx->delegate.hasNewFrame = NO;
374 [ctx->delegate.frameLock lock];
376 CVPixelBufferRef pixelBuffer = ctx->delegate.currentFrame;
378 [ctx->delegate.frameLock unlock];
383 CVPixelBufferRetain(pixelBuffer);
385 [ctx->delegate.frameLock unlock];
388 if (CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess) {
389 log_error(
"Failed to lock pixel buffer");
390 CVPixelBufferRelease(pixelBuffer);
395 size_t width = CVPixelBufferGetWidth(pixelBuffer);
396 size_t height = CVPixelBufferGetHeight(pixelBuffer);
397 size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
398 uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer);
401 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
402 CVPixelBufferRelease(pixelBuffer);
403 log_error(
"Failed to get pixel buffer base address");
409 if (!ctx->cached_frame) {
410 ctx->cached_frame =
image_new((
int)width, (
int)height);
411 if (!ctx->cached_frame) {
412 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
413 CVPixelBufferRelease(pixelBuffer);
414 log_error(
"Failed to allocate image buffer");
419 image_t *img = ctx->cached_frame;
425 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
426 CVPixelBufferRelease(pixelBuffer);
427 log_error(
"Failed to calculate frame size: width=%zu, height=%zu (would overflow)", width, height);
433 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
434 CVPixelBufferRelease(pixelBuffer);
435 log_error(
"Failed to calculate row size: width=%zu (would overflow)", width);
439 if (bytesPerRow == row_size) {
441 memcpy(img->pixels, baseAddress, frame_size);
444 for (
size_t y = 0; y < height; y++) {
445 memcpy(&img->pixels[y * row_size], &baseAddress[y * bytesPerRow], row_size);
450 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
453 CVPixelBufferRelease(pixelBuffer);
460 if (!ctx || !width || !height)
461 return ERROR_INVALID_PARAM;
464 *height = ctx->height;
468asciichat_error_t
webcam_list_devices(webcam_device_info_t **out_devices,
unsigned int *out_count) {
469 if (!out_devices || !out_count) {
470 return SET_ERRNO(ERROR_INVALID_PARAM,
"webcam_list_devices: invalid parameters");
478 NSArray *deviceTypes = getSupportedDeviceTypes();
479 AVCaptureDeviceDiscoverySession *discoverySession =
480 [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
481 mediaType:AVMediaTypeVideo
482 position:AVCaptureDevicePositionUnspecified];
484 NSArray<AVCaptureDevice *> *av_devices = discoverySession.devices;
485 NSUInteger device_count = [av_devices count];
487 if (device_count == 0) {
493 webcam_device_info_t *devices =
494 SAFE_CALLOC((
size_t)device_count,
sizeof(webcam_device_info_t), webcam_device_info_t *);
496 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate device info array");
500 for (NSUInteger i = 0; i < device_count; i++) {
501 AVCaptureDevice *device = av_devices[i];
502 devices[i].index = (
unsigned int)i;
504 const char *name = [[device localizedName] UTF8String];
506 SAFE_STRNCPY(devices[i].name, name, WEBCAM_DEVICE_NAME_MAX);
508 SAFE_STRNCPY(devices[i].name,
"<Unknown>", WEBCAM_DEVICE_NAME_MAX);
512 *out_devices = devices;
513 *out_count = (
unsigned int)device_count;
asciichat_error_t image_calc_rgb_size(size_t width, size_t height, size_t *out_size)
void image_destroy(image_t *p)
image_t * image_new(size_t width, size_t height)
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)