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