ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
webcam_v4l2.c
Go to the documentation of this file.
1
7#ifdef __linux__
8
9#include <stdio.h>
10#include <stdlib.h>
11#include <string.h>
12#include <fcntl.h>
13#include <unistd.h>
14#include <errno.h>
15#include <sys/mman.h>
16#include <sys/ioctl.h>
17#include <linux/videodev2.h>
18
19#include <ascii-chat/video/webcam/webcam.h>
20#include <ascii-chat/common.h>
21#include <ascii-chat/platform/filesystem.h>
22#include <ascii-chat/platform/util.h>
23#include <ascii-chat/util/overflow.h>
24#include <ascii-chat/util/image.h>
25
26#define WEBCAM_BUFFER_COUNT_DEFAULT 4
27#define WEBCAM_BUFFER_COUNT_MAX 8
28#define WEBCAM_DEVICE_INDEX_MAX 99
29#define WEBCAM_READ_RETRY_COUNT 3
30
31// Module-scope cached frame (freed in webcam_cleanup_context when webcam closes)
32static image_t *v4l2_cached_frame = NULL;
33
34typedef struct {
35 void *start;
36 size_t length;
37} webcam_buffer_t;
38
39struct webcam_context_t {
40 int fd;
41 int width;
42 int height;
43 uint32_t pixelformat; // Actual pixel format from driver (RGB24 or YUYV)
44 webcam_buffer_t *buffers;
45 int buffer_count;
46 image_t *cached_frame; // Reusable frame buffer (allocated once, reused for each read)
47};
48
60static void yuyv_to_rgb24(const uint8_t *yuyv, uint8_t *rgb, int width, int height) {
61 const int num_pixels = width * height;
62 for (int i = 0; i < num_pixels; i += 2) {
63 // Each 4 bytes of YUYV contains 2 pixels
64 const int yuyv_idx = i * 2;
65 const int y0 = yuyv[yuyv_idx + 0];
66 const int u = yuyv[yuyv_idx + 1];
67 const int y1 = yuyv[yuyv_idx + 2];
68 const int v = yuyv[yuyv_idx + 3];
69
70 // Convert YUV to RGB using standard formula
71 // R = Y + 1.402 * (V - 128)
72 // G = Y - 0.344 * (U - 128) - 0.714 * (V - 128)
73 // B = Y + 1.772 * (U - 128)
74 const int c0 = y0 - 16;
75 const int c1 = y1 - 16;
76 const int d = u - 128;
77 const int e = v - 128;
78
79 // First pixel
80 int r = (298 * c0 + 409 * e + 128) >> 8;
81 int g = (298 * c0 - 100 * d - 208 * e + 128) >> 8;
82 int b = (298 * c0 + 516 * d + 128) >> 8;
83
84 const int rgb_idx0 = i * 3;
85 rgb[rgb_idx0 + 0] = (uint8_t)(r < 0 ? 0 : (r > 255 ? 255 : r));
86 rgb[rgb_idx0 + 1] = (uint8_t)(g < 0 ? 0 : (g > 255 ? 255 : g));
87 rgb[rgb_idx0 + 2] = (uint8_t)(b < 0 ? 0 : (b > 255 ? 255 : b));
88
89 // Second pixel
90 r = (298 * c1 + 409 * e + 128) >> 8;
91 g = (298 * c1 - 100 * d - 208 * e + 128) >> 8;
92 b = (298 * c1 + 516 * d + 128) >> 8;
93
94 const int rgb_idx1 = (i + 1) * 3;
95 rgb[rgb_idx1 + 0] = (uint8_t)(r < 0 ? 0 : (r > 255 ? 255 : r));
96 rgb[rgb_idx1 + 1] = (uint8_t)(g < 0 ? 0 : (g > 255 ? 255 : g));
97 rgb[rgb_idx1 + 2] = (uint8_t)(b < 0 ? 0 : (b > 255 ? 255 : b));
98 }
99}
100
112static int webcam_v4l2_set_format(webcam_context_t *ctx, int width, int height) {
113 struct v4l2_format fmt = {0};
114 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
115 fmt.fmt.pix.width = width;
116 fmt.fmt.pix.height = height;
117 fmt.fmt.pix.field = V4L2_FIELD_ANY;
118
119 // Try RGB24 first (ideal - no conversion needed)
120 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24;
121 if (ioctl(ctx->fd, VIDIOC_S_FMT, &fmt) == 0 && fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_RGB24) {
122 ctx->pixelformat = V4L2_PIX_FMT_RGB24;
123 ctx->width = fmt.fmt.pix.width;
124 ctx->height = fmt.fmt.pix.height;
125 log_debug("V4L2 format set to RGB24 %dx%d", ctx->width, ctx->height);
126 return 0;
127 }
128
129 // Fall back to YUYV (most webcams support this natively)
130 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
131 if (ioctl(ctx->fd, VIDIOC_S_FMT, &fmt) == 0 && fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUYV) {
132 ctx->pixelformat = V4L2_PIX_FMT_YUYV;
133 ctx->width = fmt.fmt.pix.width;
134 ctx->height = fmt.fmt.pix.height;
135 log_debug("V4L2 format set to YUYV %dx%d (will convert to RGB)", ctx->width, ctx->height);
136 return 0;
137 }
138
139 // Save errno before log_error() clears it
140 int saved_errno = errno;
141
142 // Check if format setting failed because device is busy
143 if (saved_errno == EBUSY) {
144 log_error("Failed to set V4L2 format: device is busy (another application is using it)");
145 errno = saved_errno; // Restore errno for caller to check
146 return -1;
147 }
148
149 log_error("Failed to set V4L2 format: device supports neither RGB24 nor YUYV (errno=%d: %s)", saved_errno,
150 SAFE_STRERROR(saved_errno));
151 errno = saved_errno; // Restore errno for caller
152 return -1;
153}
154
155static int webcam_v4l2_init_buffers(webcam_context_t *ctx) {
156 struct v4l2_requestbuffers req = {0};
157 req.count = WEBCAM_BUFFER_COUNT_DEFAULT;
158 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
159 req.memory = V4L2_MEMORY_MMAP;
160
161 if (ioctl(ctx->fd, VIDIOC_REQBUFS, &req) == -1) {
162 log_error("Failed to request V4L2 buffers: %s", SAFE_STRERROR(errno));
163 return -1;
164 }
165
166 if (req.count < 2) {
167 log_error("Insufficient buffer memory");
168 return -1;
169 }
170
171 // Ensure we don't exceed our maximum buffer count
172 if (req.count > WEBCAM_BUFFER_COUNT_MAX) {
173 log_warn("Driver requested %d buffers, limiting to %d", req.count, WEBCAM_BUFFER_COUNT_MAX);
174 req.count = WEBCAM_BUFFER_COUNT_MAX;
175 }
176
177 ctx->buffer_count = req.count;
178
179 // Allocate buffer array
180 ctx->buffers = SAFE_MALLOC(sizeof(webcam_buffer_t) * ctx->buffer_count, webcam_buffer_t *);
181 if (!ctx->buffers) {
182 log_error("Failed to allocate buffer array");
183 return -1;
184 }
185
186 // Initialize buffer array
187 memset(ctx->buffers, 0, sizeof(webcam_buffer_t) * ctx->buffer_count);
188
189 for (int i = 0; i < ctx->buffer_count; i++) {
190 struct v4l2_buffer buf = {0};
191 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
192 buf.memory = V4L2_MEMORY_MMAP;
193 buf.index = i;
194
195 if (ioctl(ctx->fd, VIDIOC_QUERYBUF, &buf) == -1) {
196 log_error("Failed to query buffer %d: %s", i, SAFE_STRERROR(errno));
197 return -1;
198 }
199
200 ctx->buffers[i].length = buf.length;
201 ctx->buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, ctx->fd, buf.m.offset);
202
203 if (ctx->buffers[i].start == MAP_FAILED) {
204 log_error("Failed to mmap buffer %d: %s", i, SAFE_STRERROR(errno));
205 return -1;
206 }
207 }
208
209 return 0;
210}
211
212static int webcam_v4l2_start_streaming(webcam_context_t *ctx) {
213 // Queue all buffers
214 for (int i = 0; i < ctx->buffer_count; i++) {
215 struct v4l2_buffer buf = {0};
216 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
217 buf.memory = V4L2_MEMORY_MMAP;
218 buf.index = i;
219
220 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
221 log_error("Failed to queue buffer %d: %s", i, SAFE_STRERROR(errno));
222 return -1;
223 }
224 }
225
226 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
227 if (ioctl(ctx->fd, VIDIOC_STREAMON, &type) == -1) {
228 log_error("Failed to start V4L2 streaming: %s", SAFE_STRERROR(errno));
229 return -1;
230 }
231
232 log_dev("V4L2 streaming started");
233 return 0;
234}
235
236asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index) {
237 webcam_context_t *context;
238 context = SAFE_MALLOC(sizeof(webcam_context_t), webcam_context_t *);
239 if (!context) {
240 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate webcam context");
241 }
242
243 memset(context, 0, sizeof(webcam_context_t));
244
245 // Validate device index
246 if (device_index > WEBCAM_DEVICE_INDEX_MAX) {
247 SAFE_FREE(context);
248 return SET_ERRNO(ERROR_WEBCAM, "Invalid device index: %d (max: %d)", device_index, WEBCAM_DEVICE_INDEX_MAX);
249 }
250
251 // Open V4L2 device
252 char device_path[32];
253 SAFE_SNPRINTF(device_path, sizeof(device_path), "/dev/video%d", device_index);
254
255 // Check if device file exists before trying to open it
256 context->fd = platform_open(device_path, PLATFORM_O_RDWR | O_NONBLOCK);
257 if (context->fd == -1) {
258 // Provide more helpful error messages based on errno
259 if (errno == ENOENT) {
260 SAFE_FREE(context);
261 return SET_ERRNO(ERROR_WEBCAM,
262 "V4L2 device %s does not exist.\n"
263 "No webcam found. Try:\n"
264 " 1. Check if camera is connected: ls /dev/video*\n"
265 " 2. Use test pattern instead: --test-pattern",
266 device_path);
267 } else if (errno == EACCES) {
268 SAFE_FREE(context);
269 return SET_ERRNO(ERROR_WEBCAM_PERMISSION,
270 "Permission denied accessing %s.\n"
271 "Try: sudo usermod -a -G video $USER\n"
272 "Then log out and log back in.",
273 device_path);
274 } else if (errno == EBUSY) {
275 SAFE_FREE(context);
276 return SET_ERRNO(ERROR_WEBCAM_IN_USE, "V4L2 device %s is already in use by another application.", device_path);
277 } else {
278 SAFE_FREE(context);
279 return SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to open V4L2 device %s", device_path);
280 }
281 }
282
283 // Check if it's a video capture device
284 struct v4l2_capability cap;
285 if (ioctl(context->fd, VIDIOC_QUERYCAP, &cap) == -1) {
286 close(context->fd);
287 SAFE_FREE(context);
288 return SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to query V4L2 capabilities");
289 }
290
291 if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
292 close(context->fd);
293 SAFE_FREE(context);
294 return SET_ERRNO(ERROR_WEBCAM, "Device is not a video capture device");
295 }
296
297 // Set format (try 640x480 first, fallback to whatever the device supports)
298 if (webcam_v4l2_set_format(context, 640, 480) != 0) {
299 int saved_errno = errno; // Save errno before close() potentially changes it
300 close(context->fd);
301 SAFE_FREE(context);
302 // If format setting failed because device is busy, return ERROR_WEBCAM_IN_USE
303 if (saved_errno == EBUSY) {
304 return SET_ERRNO(ERROR_WEBCAM_IN_USE, "V4L2 device %s is in use - cannot set format", device_path);
305 }
306 return SET_ERRNO(ERROR_WEBCAM, "Failed to set V4L2 format for device %s", device_path);
307 }
308
309 // Initialize buffers
310 if (webcam_v4l2_init_buffers(context) != 0) {
311 close(context->fd);
312 SAFE_FREE(context);
313 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize V4L2 buffers for device %s", device_path);
314 }
315
316 // Start streaming
317 if (webcam_v4l2_start_streaming(context) != 0) {
318 // Cleanup buffers
319 for (int i = 0; i < context->buffer_count; i++) {
320 if (context->buffers[i].start != MAP_FAILED) {
321 munmap(context->buffers[i].start, context->buffers[i].length);
322 }
323 }
324 SAFE_FREE(context->buffers);
325 close(context->fd);
326 SAFE_FREE(context);
327 return SET_ERRNO(ERROR_WEBCAM, "Failed to start V4L2 streaming for device %s", device_path);
328 }
329
330 *ctx = context;
331 log_dev("V4L2 webcam initialized successfully on %s", device_path);
332 return 0;
333}
334
335void webcam_flush_context(webcam_context_t *ctx) {
336 if (!ctx)
337 return;
338
339 // Stop streaming to interrupt any blocking reads
340 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
341 if (ioctl(ctx->fd, VIDIOC_STREAMOFF, &type) == 0) {
342 log_debug("V4L2 streaming stopped for flush");
343 // Restart streaming so reads can continue if needed
344 ioctl(ctx->fd, VIDIOC_STREAMON, &type);
345 }
346}
347
348void webcam_cleanup_context(webcam_context_t *ctx) {
349 if (!ctx)
350 return;
351
352 // Free cached frame if it was allocated
353 if (v4l2_cached_frame) {
354 image_destroy(v4l2_cached_frame);
355 v4l2_cached_frame = NULL;
356 }
357
358 // Stop streaming
359 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
360 ioctl(ctx->fd, VIDIOC_STREAMOFF, &type);
361
362 // Unmap buffers
363 if (ctx->buffers) {
364 for (int i = 0; i < ctx->buffer_count; i++) {
365 if (ctx->buffers[i].start != MAP_FAILED) {
366 munmap(ctx->buffers[i].start, ctx->buffers[i].length);
367 }
368 }
369 SAFE_FREE(ctx->buffers);
370 }
371
372 close(ctx->fd);
373 SAFE_FREE(ctx);
374 log_debug("V4L2 webcam cleaned up");
375}
376
377image_t *webcam_read_context(webcam_context_t *ctx) {
378 if (!ctx)
379 return NULL;
380
381 struct v4l2_buffer buf = {0};
382 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
383 buf.memory = V4L2_MEMORY_MMAP;
384
385 // Dequeue a buffer with retry logic for transient errors
386 int retry_count = 0;
387 while (retry_count < WEBCAM_READ_RETRY_COUNT) {
388 if (ioctl(ctx->fd, VIDIOC_DQBUF, &buf) == 0) {
389 break; // Success
390 }
391
392 if (errno == EAGAIN) {
393 return NULL; // No frame available - this is normal
394 }
395
396 retry_count++;
397 if (retry_count >= WEBCAM_READ_RETRY_COUNT) {
398 log_error("Failed to dequeue V4L2 buffer after %d retries: %s", retry_count, SAFE_STRERROR(errno));
399 return NULL;
400 }
401
402 platform_sleep_ns(1000 * 1000); // 1ms
403 }
404
405 // Validate buffer index to prevent crashes
406 if (buf.index >= (unsigned int)ctx->buffer_count) {
407 log_error("V4L2 returned invalid buffer index %u (max: %d)", buf.index, ctx->buffer_count - 1);
408 return NULL;
409 }
410
411 // Validate buffer pointer
412 if (!ctx->buffers || !ctx->buffers[buf.index].start) {
413 log_error("V4L2 buffer %u not initialized (start=%p, buffers=%p)", buf.index,
414 ctx->buffers ? ctx->buffers[buf.index].start : NULL, ctx->buffers);
415 return NULL;
416 }
417
418 // Allocate or reallocate cached frame if needed
419 if (!v4l2_cached_frame || v4l2_cached_frame->w != ctx->width || v4l2_cached_frame->h != ctx->height) {
420 if (v4l2_cached_frame) {
421 image_destroy(v4l2_cached_frame);
422 }
423 v4l2_cached_frame = image_new(ctx->width, ctx->height);
424 if (!v4l2_cached_frame) {
425 log_error("Failed to allocate image buffer");
426 // Re-queue the buffer - use safe error handling
427 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
428 log_error("Failed to re-queue buffer after image allocation failure: %s", SAFE_STRERROR(errno));
429 }
430 return NULL;
431 }
432 }
433
434 image_t *img = v4l2_cached_frame;
435
436 // Copy and convert frame data based on pixel format
437 if (ctx->pixelformat == V4L2_PIX_FMT_YUYV) {
438 // Convert YUYV to RGB24
439 yuyv_to_rgb24(ctx->buffers[buf.index].start, (uint8_t *)img->pixels, ctx->width, ctx->height);
440 } else {
441 // RGB24 - direct copy with overflow checking
442 size_t frame_size;
443 if (image_calc_rgb_size((size_t)ctx->width, (size_t)ctx->height, &frame_size) != ASCIICHAT_OK) {
444 log_error("Failed to calculate frame size: width=%d, height=%d (would overflow)", ctx->width, ctx->height);
445 image_destroy(img);
446 return NULL;
447 }
448 memcpy(img->pixels, ctx->buffers[buf.index].start, frame_size);
449 }
450
451 // Re-queue the buffer for future use
452 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
453 log_error("Failed to re-queue V4L2 buffer %u: %s (fd=%d, type=%d, memory=%d)", buf.index, SAFE_STRERROR(errno),
454 ctx->fd, buf.type, buf.memory);
455 // Still return the image - the frame data was already copied
456 }
457
458 return img;
459}
460
461asciichat_error_t webcam_get_dimensions(webcam_context_t *ctx, int *width, int *height) {
462 if (!ctx || !width || !height)
463 return ERROR_INVALID_PARAM;
464
465 *width = ctx->width;
466 *height = ctx->height;
467 return ASCIICHAT_OK;
468}
469
470asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count) {
471 if (!out_devices || !out_count) {
472 return SET_ERRNO(ERROR_INVALID_PARAM, "webcam_list_devices: invalid parameters");
473 }
474
475 *out_devices = NULL;
476 *out_count = 0;
477
478 // First pass: count valid video devices
479 unsigned int device_count = 0;
480 for (int i = 0; i <= WEBCAM_DEVICE_INDEX_MAX; i++) {
481 char device_path[32];
482 safe_snprintf(device_path, sizeof(device_path), "/dev/video%d", i);
483
484 int fd = open(device_path, O_RDONLY);
485 if (fd < 0) {
486 continue;
487 }
488
489 struct v4l2_capability cap;
490 if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0 && (cap.device_caps & V4L2_CAP_VIDEO_CAPTURE)) {
491 device_count++;
492 }
493 close(fd);
494 }
495
496 if (device_count == 0) {
497 // No devices found - not an error
498 return ASCIICHAT_OK;
499 }
500
501 // Allocate device array
502 webcam_device_info_t *devices = SAFE_CALLOC(device_count, sizeof(webcam_device_info_t), webcam_device_info_t *);
503 if (!devices) {
504 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate device info array");
505 }
506
507 // Second pass: populate device info
508 unsigned int idx = 0;
509 for (int i = 0; i <= WEBCAM_DEVICE_INDEX_MAX && idx < device_count; i++) {
510 char device_path[32];
511 safe_snprintf(device_path, sizeof(device_path), "/dev/video%d", i);
512
513 int fd = open(device_path, O_RDONLY);
514 if (fd < 0) {
515 continue;
516 }
517
518 struct v4l2_capability cap;
519 if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0 && (cap.device_caps & V4L2_CAP_VIDEO_CAPTURE)) {
520 devices[idx].index = (unsigned int)i;
521 SAFE_STRNCPY(devices[idx].name, (const char *)cap.card, WEBCAM_DEVICE_NAME_MAX);
522 idx++;
523 }
524 close(fd);
525 }
526
527 *out_devices = devices;
528 *out_count = idx;
529
530 return ASCIICHAT_OK;
531}
532
533void webcam_free_device_list(webcam_device_info_t *devices) {
534 SAFE_FREE(devices);
535}
536
537#endif // __linux__
platform_mmap_t mmap
Definition mmap.c:34
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
asciichat_error_t image_calc_rgb_size(size_t width, size_t height, size_t *out_size)
Definition util/image.c:54
void image_destroy(image_t *p)
Definition video/image.c:85
image_t * image_new(size_t width, size_t height)
Definition video/image.c:36
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)
int platform_open(const char *pathname, int flags,...)