9#import <AVFoundation/AVFoundation.h>
10#import <CoreMedia/CoreMedia.h>
11#import <CoreVideo/CoreVideo.h>
12#include <dispatch/dispatch.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);
105 AVCaptureSession *session;
106 AVCaptureDeviceInput *input;
107 AVCaptureVideoDataOutput *output;
108 WebcamCaptureDelegate *delegate;
109 dispatch_queue_t queue;
115static NSArray *getSupportedDeviceTypes(
void) {
116 NSMutableArray *deviceTypes = [NSMutableArray array];
119 [deviceTypes addObject:AVCaptureDeviceTypeBuiltInWideAngleCamera];
122 if (@available(macOS 13.0, *)) {
123 [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
127 if (@available(macOS 14.0, *)) {
128 [deviceTypes addObject:AVCaptureDeviceTypeExternal];
131 return [[deviceTypes copy] autorelease];
145 context->session = [[AVCaptureSession alloc] init];
146 if (!context->session) {
152 [context->session setSessionPreset:AVCaptureSessionPreset640x480];
155 AVCaptureDevice *device = nil;
157 NSArray *deviceTypes = getSupportedDeviceTypes();
158 AVCaptureDeviceDiscoverySession *discoverySession =
159 [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
160 mediaType:AVMediaTypeVideo
161 position:AVCaptureDevicePositionUnspecified];
163 NSArray *devices = discoverySession.devices;
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]);
171 if (device_index < [devices count]) {
172 device = [devices objectAtIndex:device_index];
173 }
else if ([devices count] > 0) {
174 device = [devices objectAtIndex:0];
179 device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
183 log_error(
"No camera device found at index %d", device_index);
184 [context->session release];
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];
200 if ([context->session canAddInput:context->input]) {
201 [context->session addInput:context->input];
203 log_error(
"Cannot add input to capture session");
204 [context->input release];
205 [context->session release];
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];
221 NSDictionary *videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_24RGB)};
222 [context->output setVideoSettings:videoSettings];
225 context->delegate = [[WebcamCaptureDelegate alloc] init];
226 context->queue = dispatch_queue_create(
"webcam_capture_queue", DISPATCH_QUEUE_SERIAL);
228 [context->output setSampleBufferDelegate:context->delegate queue:context->queue];
231 if ([context->session canAddOutput:context->output]) {
232 [context->session addOutput:context->output];
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);
245 log_info(
"Starting AVFoundation capture session...");
246 [context->session startRunning];
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);
259 log_info(
"Capture session started successfully");
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);
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);
297 [ctx->delegate deactivate];
302 [ctx->output setSampleBufferDelegate:nil queue:nil];
306 if (ctx->session && [ctx->session isRunning]) {
307 [ctx->session stopRunning];
315 if (ctx->input && [ctx->session.inputs containsObject:ctx->input]) {
316 [ctx->session removeInput:ctx->input];
318 if (ctx->output && [ctx->session.outputs containsObject:ctx->output]) {
319 [ctx->session removeOutput:ctx->output];
321 [ctx->session release];
326 [ctx->input release];
331 [ctx->output release];
336 [ctx->delegate release];
341 dispatch_release(ctx->queue);
351 if (!ctx || !ctx->delegate || !ctx->delegate.isActive)
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);
364 ctx->delegate.hasNewFrame = NO;
367 [ctx->delegate.frameLock lock];
369 CVPixelBufferRef pixelBuffer = ctx->delegate.currentFrame;
371 [ctx->delegate.frameLock unlock];
376 CVPixelBufferRetain(pixelBuffer);
378 [ctx->delegate.frameLock unlock];
381 if (CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess) {
382 log_error(
"Failed to lock pixel buffer");
383 CVPixelBufferRelease(pixelBuffer);
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);
394 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
395 CVPixelBufferRelease(pixelBuffer);
396 log_error(
"Failed to get pixel buffer base address");
403 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
404 CVPixelBufferRelease(pixelBuffer);
405 log_error(
"Failed to allocate image buffer");
413 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
414 CVPixelBufferRelease(pixelBuffer);
416 log_error(
"Failed to calculate frame size: width=%zu, height=%zu (would overflow)", width, height);
422 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
423 CVPixelBufferRelease(pixelBuffer);
425 log_error(
"Failed to calculate row size: width=%zu (would overflow)", width);
429 if (bytesPerRow == row_size) {
431 memcpy(img->
pixels, baseAddress, frame_size);
434 for (
size_t y = 0; y < height; y++) {
435 memcpy(&img->
pixels[y * row_size], &baseAddress[y * bytesPerRow], row_size);
440 CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
443 CVPixelBufferRelease(pixelBuffer);
450 if (!ctx || !width || !height)
454 *height = ctx->height;
459 if (!out_devices || !out_count) {
468 NSArray *deviceTypes = getSupportedDeviceTypes();
469 AVCaptureDeviceDiscoverySession *discoverySession =
470 [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
471 mediaType:AVMediaTypeVideo
472 position:AVCaptureDevicePositionUnspecified];
474 NSArray<AVCaptureDevice *> *av_devices = discoverySession.devices;
475 NSUInteger device_count = [av_devices count];
477 if (device_count == 0) {
490 for (NSUInteger i = 0; i < device_count; i++) {
491 AVCaptureDevice *device = av_devices[i];
492 devices[i].
index = (
unsigned int)i;
494 const char *name = [[device localizedName] UTF8String];
502 *out_devices = devices;
503 *out_count = (
unsigned int)device_count;
#define SAFE_STRNCPY(dst, src, size)
#define SAFE_MALLOC(size, cast)
#define SAFE_CALLOC(count, size, cast)
#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)
#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()
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.
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.
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.
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order)
Webcam device information structure.
unsigned int index
Device index (use with webcam_init)
asciichat_error_t image_calc_rgb_size(size_t width, size_t height, size_t *out_size)
Calculate total RGB buffer size from dimensions.
🖼️ Safe overflow-checked buffer size calculations for images and video frames