17#include <linux/videodev2.h>
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>
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
32static image_t *v4l2_cached_frame = NULL;
39struct webcam_context_t {
44 webcam_buffer_t *buffers;
46 image_t *cached_frame;
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) {
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];
74 const int c0 = y0 - 16;
75 const int c1 = y1 - 16;
76 const int d = u - 128;
77 const int e = v - 128;
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;
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));
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;
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));
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;
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);
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);
140 int saved_errno = errno;
143 if (saved_errno == EBUSY) {
144 log_error(
"Failed to set V4L2 format: device is busy (another application is using it)");
149 log_error(
"Failed to set V4L2 format: device supports neither RGB24 nor YUYV (errno=%d: %s)", saved_errno,
150 SAFE_STRERROR(saved_errno));
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;
161 if (ioctl(ctx->fd, VIDIOC_REQBUFS, &req) == -1) {
162 log_error(
"Failed to request V4L2 buffers: %s", SAFE_STRERROR(errno));
167 log_error(
"Insufficient buffer memory");
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;
177 ctx->buffer_count = req.count;
180 ctx->buffers = SAFE_MALLOC(
sizeof(webcam_buffer_t) * ctx->buffer_count, webcam_buffer_t *);
182 log_error(
"Failed to allocate buffer array");
187 memset(ctx->buffers, 0,
sizeof(webcam_buffer_t) * ctx->buffer_count);
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;
195 if (ioctl(ctx->fd, VIDIOC_QUERYBUF, &buf) == -1) {
196 log_error(
"Failed to query buffer %d: %s", i, SAFE_STRERROR(errno));
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);
203 if (ctx->buffers[i].start == MAP_FAILED) {
204 log_error(
"Failed to mmap buffer %d: %s", i, SAFE_STRERROR(errno));
212static int webcam_v4l2_start_streaming(webcam_context_t *ctx) {
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;
220 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
221 log_error(
"Failed to queue buffer %d: %s", i, SAFE_STRERROR(errno));
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));
232 log_dev(
"V4L2 streaming started");
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 *);
240 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate webcam context");
243 memset(context, 0,
sizeof(webcam_context_t));
246 if (device_index > WEBCAM_DEVICE_INDEX_MAX) {
248 return SET_ERRNO(ERROR_WEBCAM,
"Invalid device index: %d (max: %d)", device_index, WEBCAM_DEVICE_INDEX_MAX);
252 char device_path[32];
253 SAFE_SNPRINTF(device_path,
sizeof(device_path),
"/dev/video%d", device_index);
256 context->fd =
platform_open(device_path, PLATFORM_O_RDWR | O_NONBLOCK);
257 if (context->fd == -1) {
259 if (errno == ENOENT) {
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",
267 }
else if (errno == EACCES) {
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.",
274 }
else if (errno == EBUSY) {
276 return SET_ERRNO(ERROR_WEBCAM_IN_USE,
"V4L2 device %s is already in use by another application.", device_path);
279 return SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to open V4L2 device %s", device_path);
284 struct v4l2_capability cap;
285 if (ioctl(context->fd, VIDIOC_QUERYCAP, &cap) == -1) {
288 return SET_ERRNO_SYS(ERROR_WEBCAM,
"Failed to query V4L2 capabilities");
291 if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
294 return SET_ERRNO(ERROR_WEBCAM,
"Device is not a video capture device");
298 if (webcam_v4l2_set_format(context, 640, 480) != 0) {
299 int saved_errno = errno;
303 if (saved_errno == EBUSY) {
304 return SET_ERRNO(ERROR_WEBCAM_IN_USE,
"V4L2 device %s is in use - cannot set format", device_path);
306 return SET_ERRNO(ERROR_WEBCAM,
"Failed to set V4L2 format for device %s", device_path);
310 if (webcam_v4l2_init_buffers(context) != 0) {
313 return SET_ERRNO(ERROR_WEBCAM,
"Failed to initialize V4L2 buffers for device %s", device_path);
317 if (webcam_v4l2_start_streaming(context) != 0) {
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);
324 SAFE_FREE(context->buffers);
327 return SET_ERRNO(ERROR_WEBCAM,
"Failed to start V4L2 streaming for device %s", device_path);
331 log_dev(
"V4L2 webcam initialized successfully on %s", device_path);
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");
344 ioctl(ctx->fd, VIDIOC_STREAMON, &type);
353 if (v4l2_cached_frame) {
355 v4l2_cached_frame = NULL;
359 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
360 ioctl(ctx->fd, VIDIOC_STREAMOFF, &type);
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);
369 SAFE_FREE(ctx->buffers);
374 log_debug(
"V4L2 webcam cleaned up");
381 struct v4l2_buffer buf = {0};
382 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
383 buf.memory = V4L2_MEMORY_MMAP;
387 while (retry_count < WEBCAM_READ_RETRY_COUNT) {
388 if (ioctl(ctx->fd, VIDIOC_DQBUF, &buf) == 0) {
392 if (errno == EAGAIN) {
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));
402 platform_sleep_ns(1000 * 1000);
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);
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);
419 if (!v4l2_cached_frame || v4l2_cached_frame->w != ctx->width || v4l2_cached_frame->h != ctx->height) {
420 if (v4l2_cached_frame) {
423 v4l2_cached_frame =
image_new(ctx->width, ctx->height);
424 if (!v4l2_cached_frame) {
425 log_error(
"Failed to allocate image buffer");
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));
434 image_t *img = v4l2_cached_frame;
437 if (ctx->pixelformat == V4L2_PIX_FMT_YUYV) {
439 yuyv_to_rgb24(ctx->buffers[buf.index].start, (uint8_t *)img->pixels, ctx->width, ctx->height);
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);
448 memcpy(img->pixels, ctx->buffers[buf.index].start, frame_size);
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);
462 if (!ctx || !width || !height)
463 return ERROR_INVALID_PARAM;
466 *height = ctx->height;
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");
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);
484 int fd = open(device_path, O_RDONLY);
489 struct v4l2_capability cap;
490 if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0 && (cap.device_caps & V4L2_CAP_VIDEO_CAPTURE)) {
496 if (device_count == 0) {
502 webcam_device_info_t *devices = SAFE_CALLOC(device_count,
sizeof(webcam_device_info_t), webcam_device_info_t *);
504 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate device info array");
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);
513 int fd = open(device_path, O_RDONLY);
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);
527 *out_devices = devices;
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
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)
int platform_open(const char *pathname, int flags,...)