ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
webcam_v4l2.c
Go to the documentation of this file.
1
11#ifdef __linux__
12
13#include <stdio.h>
14#include <stdlib.h>
15#include <string.h>
16#include <fcntl.h>
17#include <unistd.h>
18#include <errno.h>
19#include <poll.h>
20#include <sys/mman.h>
21#include <sys/ioctl.h>
22#include <linux/videodev2.h>
23
24#include <libswscale/swscale.h>
25#include <libavutil/pixfmt.h>
26#include <libavcodec/avcodec.h>
27#include <libavformat/avformat.h>
28
31#include <ascii-chat/common.h>
39#include <ascii-chat/atomic.h>
40#include <ascii-chat/log/io.h>
41
42#define WEBCAM_BUFFER_COUNT_DEFAULT 4
43#define WEBCAM_BUFFER_COUNT_MAX 8
44#define WEBCAM_DEVICE_INDEX_MAX 99
45#define WEBCAM_READ_RETRY_COUNT 3
46
47// Module-scope cached frame (freed in webcam_cleanup_context when webcam closes)
48static image_t *v4l2_cached_frame = NULL;
49
50typedef struct {
51 void *start;
52 size_t length;
53} webcam_buffer_t;
54
55struct webcam_context_t {
56 int fd;
57 int width;
58 int height;
59 uint32_t pixelformat; // Actual pixel format from driver (RGB24, YUYV, NV12, I420, MJPEG, UYVY)
60 webcam_buffer_t *buffers;
61 int buffer_count;
62 image_t *cached_frame; // Reusable frame buffer (allocated once, reused for each read)
63 struct SwsContext *sws_ctx; // libswscale context for format conversion (if needed)
64 enum AVPixelFormat av_pixel_format; // FFmpeg pixel format for swscale
65 AVCodecContext *mjpeg_codec_ctx; // MJPEG JPEG decompression context (if using MJPEG format)
66 AVFrame *mjpeg_decoded_frame; // Decoded JPEG frame buffer
67
68 // Async camera reading (non-blocking)
69 lifecycle_t async_lifecycle; // Lifecycle state machine for camera thread
70 asciichat_thread_t camera_thread; // Background thread for continuous frame capture
71 atomic_ptr_t latest_frame; // Latest frame from camera (atomic swap)
72 image_t *async_cached_frame; // Last frame returned to caller (returned when no new frame available)
73};
74
81static void *webcam_camera_thread_func(void *arg) {
83
84 log_debug("Camera background thread started");
85
86 while (lifecycle_is_initialized(&ctx->async_lifecycle)) {
87 // Read frame (blocking on camera)
89
90 if (frame) {
91 // Copy the frame data (webcam_read_context returns ptr to module-level static buffer)
92 image_t *frame_copy = image_new_copy(frame);
93 if (frame_copy) {
94 // Atomic swap: replace latest_frame with new frame
95 image_t *old_frame = (image_t *)atomic_ptr_exchange(&ctx->latest_frame, frame_copy);
96 if (old_frame) {
97 image_destroy(old_frame);
98 }
99 }
100 }
101 }
102
103 log_debug("Camera background thread stopped");
104 return NULL;
105}
106
113static int webcam_v4l2_init_swscale(webcam_context_t *ctx, enum AVPixelFormat src_fmt) {
114 if (src_fmt == AV_PIX_FMT_NONE) {
115 log_error("Invalid pixel format for swscale");
116 return -1;
117 }
118
119 // Free existing context if present
120 if (ctx->sws_ctx) {
121 sws_freeContext(ctx->sws_ctx);
122 ctx->sws_ctx = NULL;
123 }
124
125 // Create new swscale context for conversion to RGB24
126 LOG_IO("swscaler", {
127 ctx->sws_ctx = sws_getContext(ctx->width, ctx->height, src_fmt, ctx->width, ctx->height, AV_PIX_FMT_RGB24,
128 SWS_BILINEAR, NULL, NULL, NULL);
129 });
130
131 if (!ctx->sws_ctx) {
132 log_error("Failed to create swscale context for format conversion");
133 return -1;
134 }
135
136 ctx->av_pixel_format = src_fmt;
137 return 0;
138}
139
148static int webcam_v4l2_init_mjpeg_decoder(webcam_context_t *ctx) {
149 // Find JPEG codec
150 const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG);
151 if (!codec) {
152 log_error("MJPEG codec not found in FFmpeg");
153 return -1;
154 }
155
156 // Allocate codec context
157 ctx->mjpeg_codec_ctx = avcodec_alloc_context3(codec);
158 if (!ctx->mjpeg_codec_ctx) {
159 log_error("Failed to allocate MJPEG codec context");
160 return -1;
161 }
162
163 // Set expected frame dimensions (V4L2 tells us these)
164 ctx->mjpeg_codec_ctx->width = ctx->width;
165 ctx->mjpeg_codec_ctx->height = ctx->height;
166
167 // Open codec
168 if (avcodec_open2(ctx->mjpeg_codec_ctx, codec, NULL) < 0) {
169 log_error("Failed to open MJPEG codec");
170 avcodec_free_context(&ctx->mjpeg_codec_ctx);
171 return -1;
172 }
173
174 // Allocate frame for decoded output
175 ctx->mjpeg_decoded_frame = av_frame_alloc();
176 if (!ctx->mjpeg_decoded_frame) {
177 log_error("Failed to allocate MJPEG decoded frame");
178 avcodec_free_context(&ctx->mjpeg_codec_ctx);
179 return -1;
180 }
181
182 // Initialize swscale for converting decoded MJPEG (typically YUV420P) to RGB24
183 if (webcam_v4l2_init_swscale(ctx, AV_PIX_FMT_YUV420P) != 0) {
184 log_error("Failed to initialize swscale for MJPEG decoded frames");
185 av_frame_free(&ctx->mjpeg_decoded_frame);
186 avcodec_free_context(&ctx->mjpeg_codec_ctx);
187 return -1;
188 }
189
190 return 0;
191}
192
211static int webcam_v4l2_set_format(webcam_context_t *ctx, int width, int height) {
212 struct v4l2_format fmt = {0};
213 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
214 fmt.fmt.pix.width = width;
215 fmt.fmt.pix.height = height;
216 fmt.fmt.pix.field = V4L2_FIELD_ANY;
217
218 // Try RGB24 first (ideal - no conversion needed)
219 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24;
220 if (ioctl(ctx->fd, VIDIOC_S_FMT, &fmt) == 0 && fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_RGB24) {
221 ctx->pixelformat = V4L2_PIX_FMT_RGB24;
222 ctx->width = fmt.fmt.pix.width;
223 ctx->height = fmt.fmt.pix.height;
224 ctx->sws_ctx = NULL;
225 log_debug("V4L2 format set to RGB24 %dx%d", ctx->width, ctx->height);
226 return 0;
227 }
228
229 // Try NV12 (Raspberry Pi cameras, modern USB cameras)
230 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_NV12;
231 if (ioctl(ctx->fd, VIDIOC_S_FMT, &fmt) == 0 && fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_NV12) {
232 ctx->pixelformat = V4L2_PIX_FMT_NV12;
233 ctx->width = fmt.fmt.pix.width;
234 ctx->height = fmt.fmt.pix.height;
235 if (webcam_v4l2_init_swscale(ctx, AV_PIX_FMT_NV12) == 0) {
236 log_debug("V4L2 format set to NV12 %dx%d (will convert to RGB with swscale)", ctx->width, ctx->height);
237 return 0;
238 }
239 }
240
241 // Try I420 (planar YUV)
242 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUV420;
243 if (ioctl(ctx->fd, VIDIOC_S_FMT, &fmt) == 0 && fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUV420) {
244 ctx->pixelformat = V4L2_PIX_FMT_YUV420;
245 ctx->width = fmt.fmt.pix.width;
246 ctx->height = fmt.fmt.pix.height;
247 if (webcam_v4l2_init_swscale(ctx, AV_PIX_FMT_YUV420P) == 0) {
248 log_debug("V4L2 format set to I420 %dx%d (will convert to RGB with swscale)", ctx->width, ctx->height);
249 return 0;
250 }
251 }
252
253 // Try MJPEG (Motion-JPEG - compressed format supporting 60fps on many cameras)
254 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
255 int mjpeg_ret = ioctl(ctx->fd, VIDIOC_S_FMT, &fmt);
256 if (mjpeg_ret == 0 && fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG) {
257 ctx->pixelformat = V4L2_PIX_FMT_MJPEG;
258 ctx->width = fmt.fmt.pix.width;
259 ctx->height = fmt.fmt.pix.height;
260 if (webcam_v4l2_init_mjpeg_decoder(ctx) == 0) {
261 log_debug("V4L2 format set to MJPEG %dx%d (60fps compressed - will decompress JPEG with FFmpeg)", ctx->width,
262 ctx->height);
263 log_info("MJPEG format selected: pixelformat=0x%x", ctx->pixelformat);
264 return 0;
265 } else {
266 log_warn("MJPEG decoder init failed, trying next format");
267 }
268 } else {
269 log_debug("MJPEG format not supported (ioctl returned %d, got format 0x%x)", mjpeg_ret, fmt.fmt.pix.pixelformat);
270 }
271
272 // Try YUYV (YUV 4:2:2 - most webcams support this)
273 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
274 if (ioctl(ctx->fd, VIDIOC_S_FMT, &fmt) == 0 && fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUYV) {
275 ctx->pixelformat = V4L2_PIX_FMT_YUYV;
276 ctx->width = fmt.fmt.pix.width;
277 ctx->height = fmt.fmt.pix.height;
278 if (webcam_v4l2_init_swscale(ctx, AV_PIX_FMT_YUYV422) == 0) {
279 log_debug("V4L2 format set to YUYV %dx%d (will convert to RGB with swscale)", ctx->width, ctx->height);
280 return 0;
281 }
282 }
283
284 // Try UYVY (YUV 4:2:2 variant)
285 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_UYVY;
286 if (ioctl(ctx->fd, VIDIOC_S_FMT, &fmt) == 0 && fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_UYVY) {
287 ctx->pixelformat = V4L2_PIX_FMT_UYVY;
288 ctx->width = fmt.fmt.pix.width;
289 ctx->height = fmt.fmt.pix.height;
290 if (webcam_v4l2_init_swscale(ctx, AV_PIX_FMT_UYVY422) == 0) {
291 log_debug("V4L2 format set to UYVY %dx%d (will convert to RGB with swscale)", ctx->width, ctx->height);
292 return 0;
293 }
294 }
295
296 // Save errno before log_error() clears it
297 int saved_errno = errno;
298
299 // Check if format setting failed because device is busy
300 if (saved_errno == EBUSY) {
301 log_error("Failed to set V4L2 format: device is busy (another application is using it)");
302 errno = saved_errno;
303 return -1;
304 }
305
306 log_error("Failed to set V4L2 format: device supports none of (RGB24, NV12, I420, YUYV, UYVY) (errno=%d: %s)",
307 saved_errno, SAFE_STRERROR(saved_errno));
308 errno = saved_errno;
309 return -1;
310}
311
312static int webcam_v4l2_init_buffers(webcam_context_t *ctx) {
313 struct v4l2_requestbuffers req = {0};
314 req.count = WEBCAM_BUFFER_COUNT_DEFAULT;
315 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
316 req.memory = V4L2_MEMORY_MMAP;
317
318 if (ioctl(ctx->fd, VIDIOC_REQBUFS, &req) == -1) {
319 log_error("Failed to request V4L2 buffers: %s", SAFE_STRERROR(errno));
320 return -1;
321 }
322
323 if (req.count < 2) {
324 log_error("Insufficient buffer memory");
325 return -1;
326 }
327
328 // Ensure we don't exceed our maximum buffer count
329 if (req.count > WEBCAM_BUFFER_COUNT_MAX) {
330 log_warn("Driver requested %d buffers, limiting to %d", req.count, WEBCAM_BUFFER_COUNT_MAX);
331 req.count = WEBCAM_BUFFER_COUNT_MAX;
332 }
333
334 ctx->buffer_count = req.count;
335
336 // Allocate buffer array
337 ctx->buffers = SAFE_MALLOC(sizeof(webcam_buffer_t) * ctx->buffer_count, webcam_buffer_t *);
338 if (!ctx->buffers) {
339 log_error("Failed to allocate buffer array");
340 return -1;
341 }
342
343 // Initialize buffer array
344 memset(ctx->buffers, 0, sizeof(webcam_buffer_t) * ctx->buffer_count);
345
346 for (int i = 0; i < ctx->buffer_count; i++) {
347 struct v4l2_buffer buf = {0};
348 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
349 buf.memory = V4L2_MEMORY_MMAP;
350 buf.index = i;
351
352 if (ioctl(ctx->fd, VIDIOC_QUERYBUF, &buf) == -1) {
353 log_error("Failed to query buffer %d: %s", i, SAFE_STRERROR(errno));
354 return -1;
355 }
356
357 ctx->buffers[i].length = buf.length;
358 ctx->buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, ctx->fd, buf.m.offset);
359
360 if (ctx->buffers[i].start == MAP_FAILED) {
361 log_error("Failed to mmap buffer %d: %s", i, SAFE_STRERROR(errno));
362 return -1;
363 }
364 }
365
366 return 0;
367}
368
369static int webcam_v4l2_start_streaming(webcam_context_t *ctx) {
370 // Queue all buffers
371 for (int i = 0; i < ctx->buffer_count; i++) {
372 struct v4l2_buffer buf = {0};
373 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
374 buf.memory = V4L2_MEMORY_MMAP;
375 buf.index = i;
376
377 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
378 log_error("Failed to queue buffer %d: %s", i, SAFE_STRERROR(errno));
379 return -1;
380 }
381 }
382
383 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
384 if (ioctl(ctx->fd, VIDIOC_STREAMON, &type) == -1) {
385 log_error("Failed to start V4L2 streaming: %s", SAFE_STRERROR(errno));
386 return -1;
387 }
388
389 log_dev("V4L2 streaming started");
390 return 0;
391}
392
393asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index) {
394 webcam_context_t *context;
395 context = SAFE_MALLOC(sizeof(webcam_context_t), webcam_context_t *);
396 if (!context) {
397 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate webcam context");
398 }
399
400 memset(context, 0, sizeof(webcam_context_t));
401
402 // Validate device index
403 if (device_index > WEBCAM_DEVICE_INDEX_MAX) {
404 SAFE_FREE(context);
405 return SET_ERRNO(ERROR_WEBCAM, "Invalid device index: %d (max: %d)", device_index, WEBCAM_DEVICE_INDEX_MAX);
406 }
407
408 // Open V4L2 device
409 char device_path[32];
410 SAFE_SNPRINTF(device_path, sizeof(device_path), "/dev/video%d", device_index);
411
412 // Check if device file exists before trying to open it
413 context->fd = platform_open("webcam_device", device_path, PLATFORM_O_RDWR | O_NONBLOCK);
414 if (context->fd == -1) {
415 // Provide more helpful error messages based on errno
416 if (errno == ENOENT) {
417 SAFE_FREE(context);
418 return SET_ERRNO(ERROR_WEBCAM,
419 "V4L2 device %s does not exist.\n"
420 "No webcam found. Try:\n"
421 " 1. Check if camera is connected: ls /dev/video*\n"
422 " 2. Use test pattern instead: --test-pattern",
423 device_path);
424 } else if (errno == EACCES) {
425 SAFE_FREE(context);
427 "Permission denied accessing %s.\n"
428 "Try: sudo usermod -a -G video $USER\n"
429 "Then log out and log back in.",
430 device_path);
431 } else if (errno == EBUSY) {
432 SAFE_FREE(context);
433 return SET_ERRNO(ERROR_WEBCAM_IN_USE, "V4L2 device %s is already in use by another application.", device_path);
434 } else {
435 SAFE_FREE(context);
436 return SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to open V4L2 device %s", device_path);
437 }
438 }
439
440 // Check if it's a video capture device
441 struct v4l2_capability cap;
442 if (ioctl(context->fd, VIDIOC_QUERYCAP, &cap) == -1) {
443 close(context->fd);
444 SAFE_FREE(context);
445 return SET_ERRNO_SYS(ERROR_WEBCAM, "Failed to query V4L2 capabilities");
446 }
447
448 if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
449 close(context->fd);
450 SAFE_FREE(context);
451 return SET_ERRNO(ERROR_WEBCAM, "Device is not a video capture device");
452 }
453
454 // Set format (try 320x240 first for better performance, fallback to whatever the device supports)
455 if (webcam_v4l2_set_format(context, 320, 240) != 0) {
456 int saved_errno = errno; // Save errno before close() potentially changes it
457 close(context->fd);
458 SAFE_FREE(context);
459 // If format setting failed because device is busy, return ERROR_WEBCAM_IN_USE
460 if (saved_errno == EBUSY) {
461 return SET_ERRNO(ERROR_WEBCAM_IN_USE, "V4L2 device %s is in use - cannot set format", device_path);
462 }
463 return SET_ERRNO(ERROR_WEBCAM, "Failed to set V4L2 format for device %s", device_path);
464 }
465
466 // Initialize buffers
467 if (webcam_v4l2_init_buffers(context) != 0) {
468 close(context->fd);
469 SAFE_FREE(context);
470 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize V4L2 buffers for device %s", device_path);
471 }
472
473 // Start streaming
474 if (webcam_v4l2_start_streaming(context) != 0) {
475 // Cleanup buffers
476 for (int i = 0; i < context->buffer_count; i++) {
477 if (context->buffers[i].start != MAP_FAILED) {
478 munmap(context->buffers[i].start, context->buffers[i].length);
479 }
480 }
481 SAFE_FREE(context->buffers);
482 close(context->fd);
483 SAFE_FREE(context);
484 return SET_ERRNO(ERROR_WEBCAM, "Failed to start V4L2 streaming for device %s", device_path);
485 }
486
487 // Request target FPS via VIDIOC_S_PARM AFTER streaming starts
488 // Some devices require streaming to be active before frame rate control works
489 uint32_t target_fps = (uint32_t)GET_OPTION(fps);
490 if (target_fps == 0)
491 target_fps = 60; // Default to 60 if not set
492
493 struct v4l2_streamparm parm = {0};
494 parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
495 parm.parm.capture.timeperframe.numerator = 1;
496 parm.parm.capture.timeperframe.denominator = target_fps;
497
498 if (ioctl(context->fd, VIDIOC_S_PARM, &parm) == 0) {
499 // Get back what the driver actually set
500 if (ioctl(context->fd, VIDIOC_G_PARM, &parm) == 0) {
501 int actual_fps = parm.parm.capture.timeperframe.denominator /
502 (parm.parm.capture.timeperframe.numerator ? parm.parm.capture.timeperframe.numerator : 1);
503 log_info("V4L2 frame rate set: requested %u FPS, got %d FPS", target_fps, actual_fps);
504 }
505 } else {
506 log_debug("V4L2 device does not support VIDIOC_S_PARM (frame rate control)");
507 }
508
509 // Initialize async lifecycle for camera thread
510 if (!lifecycle_init(&context->async_lifecycle, "webcam_camera")) {
511 // Cleanup on lifecycle init failure
512 for (int i = 0; i < context->buffer_count; i++) {
513 if (context->buffers[i].start != MAP_FAILED) {
514 munmap(context->buffers[i].start, context->buffers[i].length);
515 }
516 }
517 SAFE_FREE(context->buffers);
518 close(context->fd);
519 SAFE_FREE(context);
520 return SET_ERRNO(ERROR_INIT, "Failed to initialize camera thread lifecycle");
521 }
522
523 // Start background camera thread
524 asciichat_error_t thread_err =
525 asciichat_thread_create(&context->camera_thread, "webcam_camera", webcam_camera_thread_func, context);
526 if (thread_err != ASCIICHAT_OK) {
527 lifecycle_shutdown(&context->async_lifecycle);
528 for (int i = 0; i < context->buffer_count; i++) {
529 if (context->buffers[i].start != MAP_FAILED) {
530 munmap(context->buffers[i].start, context->buffers[i].length);
531 }
532 }
533 SAFE_FREE(context->buffers);
534 close(context->fd);
535 SAFE_FREE(context);
536 return thread_err;
537 }
538
539 *ctx = context;
540 log_dev("V4L2 webcam initialized successfully on %s with async camera thread", device_path);
541
542 /* Register webcam context with named registry */
543 NAMED_REGISTER(context, device_path, "webcam", "0x%tx", NULL);
544
545 return 0;
546}
547
549 if (!ctx)
550 return;
551
552 // Stop streaming to interrupt any blocking reads
553 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
554 if (ioctl(ctx->fd, VIDIOC_STREAMOFF, &type) == 0) {
555 log_debug("V4L2 streaming stopped for flush");
556 // Restart streaming so reads can continue if needed
557 ioctl(ctx->fd, VIDIOC_STREAMON, &type);
558 }
559}
560
562 if (!ctx)
563 return;
564
565 NAMED_UNREGISTER(ctx);
566
567 // Stop camera thread by shutting down lifecycle
568 if (lifecycle_shutdown(&ctx->async_lifecycle)) {
569 // This caller won the shutdown race, join the thread
570 asciichat_thread_join(&ctx->camera_thread, NULL);
571 log_debug("Camera background thread joined");
572 }
573
574 // Clean up any remaining frames in the atomic and cached buffers
575 image_t *leftover_frame = (image_t *)atomic_ptr_exchange(&ctx->latest_frame, NULL);
576 if (leftover_frame) {
577 image_destroy(leftover_frame);
578 }
579
580 if (ctx->async_cached_frame) {
581 image_destroy(ctx->async_cached_frame);
582 ctx->async_cached_frame = NULL;
583 }
584
585 // Free swscale context if it was allocated
586 if (ctx->sws_ctx) {
587 sws_freeContext(ctx->sws_ctx);
588 ctx->sws_ctx = NULL;
589 }
590
591 // Free MJPEG codec context if it was allocated
592 if (ctx->mjpeg_codec_ctx) {
593 avcodec_free_context(&ctx->mjpeg_codec_ctx);
594 ctx->mjpeg_codec_ctx = NULL;
595 }
596
597 // Free MJPEG decoded frame if it was allocated
598 if (ctx->mjpeg_decoded_frame) {
599 av_frame_free(&ctx->mjpeg_decoded_frame);
600 ctx->mjpeg_decoded_frame = NULL;
601 }
602
603 // Free cached frame if it was allocated
604 if (v4l2_cached_frame) {
605 image_destroy(v4l2_cached_frame);
606 v4l2_cached_frame = NULL;
607 }
608
609 // Stop streaming
610 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
611 ioctl(ctx->fd, VIDIOC_STREAMOFF, &type);
612
613 // Unmap buffers
614 if (ctx->buffers) {
615 for (int i = 0; i < ctx->buffer_count; i++) {
616 if (ctx->buffers[i].start != MAP_FAILED) {
617 munmap(ctx->buffers[i].start, ctx->buffers[i].length);
618 }
619 }
620 SAFE_FREE(ctx->buffers);
621 }
622
623 close(ctx->fd);
624 SAFE_FREE(ctx);
625 log_debug("V4L2 webcam cleaned up");
626}
627
629 if (!ctx)
630 return NULL;
631
632 struct v4l2_buffer buf = {0};
633 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
634 buf.memory = V4L2_MEMORY_MMAP;
635
636 // Use poll() to efficiently wait for frame available (500ms timeout)
637 // At 30fps, frames arrive every ~33ms; at 60fps every ~16ms
638 struct pollfd pfd = {.fd = ctx->fd, .events = POLLIN};
639
640 int poll_ret = poll(&pfd, 1, 500); // 500ms timeout for slower cameras
641
642 if (poll_ret < 0) {
643 log_error("poll() failed on V4L2 device: %s", SAFE_STRERROR(errno));
644 return NULL;
645 }
646
647 if (poll_ret == 0) {
648 // Timeout - no frame available within 100ms
649 return NULL;
650 }
651
652 // poll() returned > 0: data is available
653 if (!(pfd.revents & POLLIN)) {
654 // Some other event occurred (error/disconnect)
655 log_error("V4L2 device error: poll revents=0x%x", pfd.revents);
656 return NULL;
657 }
658
659 // Dequeue the buffer (should succeed now)
660 if (ioctl(ctx->fd, VIDIOC_DQBUF, &buf) != 0) {
661 log_error("Failed to dequeue V4L2 buffer after poll(): %s", SAFE_STRERROR(errno));
662 return NULL;
663 }
664
665 // Validate buffer index to prevent crashes
666 if (buf.index >= (unsigned int)ctx->buffer_count) {
667 log_error("V4L2 returned invalid buffer index %u (max: %d)", buf.index, ctx->buffer_count - 1);
668 return NULL;
669 }
670
671 // Validate buffer pointer
672 if (!ctx->buffers || !ctx->buffers[buf.index].start) {
673 log_error("V4L2 buffer %u not initialized (start=%p, buffers=%p)", buf.index,
674 ctx->buffers ? ctx->buffers[buf.index].start : NULL, ctx->buffers);
675 return NULL;
676 }
677
678 // Allocate or reallocate cached frame if needed
679 if (!v4l2_cached_frame || v4l2_cached_frame->w != ctx->width || v4l2_cached_frame->h != ctx->height) {
680 if (v4l2_cached_frame) {
681 image_destroy(v4l2_cached_frame);
682 }
683 v4l2_cached_frame = image_new(ctx->width, ctx->height);
684 if (!v4l2_cached_frame) {
685 log_error("Failed to allocate image buffer");
686 // Re-queue the buffer - use safe error handling
687 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
688 log_error("Failed to re-queue buffer after image allocation failure: %s", SAFE_STRERROR(errno));
689 }
690 return NULL;
691 }
692 }
693
694 image_t *img = v4l2_cached_frame;
695
696 // Handle MJPEG decompression if using MJPEG format
697 if (ctx->pixelformat == V4L2_PIX_FMT_MJPEG) {
698 if (!ctx->mjpeg_codec_ctx) {
699 log_error("MJPEG codec context not initialized");
700 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
701 log_error("Failed to re-queue buffer after MJPEG codec error: %s", SAFE_STRERROR(errno));
702 }
703 return NULL;
704 }
705
706 // Create packet from MJPEG frame data
707 AVPacket pkt = {0};
708 av_new_packet(&pkt, buf.bytesused);
709 memcpy(pkt.data, ctx->buffers[buf.index].start, buf.bytesused);
710
711 // Send packet to decoder
712 if (avcodec_send_packet(ctx->mjpeg_codec_ctx, &pkt) < 0) {
713 log_warn_every(1000000000LL, "Failed to send MJPEG packet (size=%u) to decoder", buf.bytesused);
714 av_packet_unref(&pkt);
715 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
716 log_error("Failed to re-queue buffer after MJPEG decode error: %s", SAFE_STRERROR(errno));
717 }
718 return NULL;
719 }
720
721 // Receive decoded frame (JPEG decompression happens here)
722 if (avcodec_receive_frame(ctx->mjpeg_codec_ctx, ctx->mjpeg_decoded_frame) < 0) {
723 log_warn_every(1000000000LL, "Failed to decode MJPEG frame from %u bytes", buf.bytesused);
724 av_packet_unref(&pkt);
725 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
726 log_error("Failed to re-queue buffer after MJPEG receive error: %s", SAFE_STRERROR(errno));
727 }
728 return NULL;
729 }
730
731 av_packet_unref(&pkt);
732
733 // Convert decoded frame to RGB24 using swscale
734 if (!ctx->sws_ctx) {
735 log_error("Swscale context not initialized for MJPEG decoded frame");
736 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
737 log_error("Failed to re-queue buffer after swscale init error: %s", SAFE_STRERROR(errno));
738 }
739 return NULL;
740 }
741
742 uint8_t *dst_data[1] = {(uint8_t *)img->pixels};
743 int dst_linesize[1] = {ctx->width * 3};
744
745 sws_scale(ctx->sws_ctx, (const uint8_t *const *)ctx->mjpeg_decoded_frame->data, ctx->mjpeg_decoded_frame->linesize,
746 0, ctx->height, dst_data, dst_linesize);
747
748 // Re-queue the buffer for future use
749 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
750 log_error("Failed to re-queue V4L2 buffer %u after MJPEG decode: %s", buf.index, SAFE_STRERROR(errno));
751 }
752
753 return img;
754 }
755
756 // Copy and convert frame data based on pixel format
757 if (ctx->sws_ctx) {
758 // Use libswscale for format conversion (NV12, I420, YUYV, UYVY)
759 const uint8_t *src_data[1] = {(const uint8_t *)ctx->buffers[buf.index].start};
760 int src_linesize[1] = {0};
761
762 // Calculate source linesize based on format
763 switch (ctx->pixelformat) {
764 case V4L2_PIX_FMT_NV12:
765 case V4L2_PIX_FMT_YUV420:
766 // For planar formats, linesize equals width
767 src_linesize[0] = ctx->width;
768 break;
769 case V4L2_PIX_FMT_YUYV:
770 case V4L2_PIX_FMT_UYVY:
771 // For packed YUV 4:2:2, each pixel is 2 bytes
772 src_linesize[0] = ctx->width * 2;
773 break;
774 default:
775 src_linesize[0] = ctx->width;
776 }
777
778 uint8_t *dst_data[1] = {(uint8_t *)img->pixels};
779 int dst_linesize[1] = {ctx->width * 3};
780
781 sws_scale(ctx->sws_ctx, src_data, src_linesize, 0, ctx->height, dst_data, dst_linesize);
782 } else {
783 // RGB24 - direct copy with overflow checking
784 size_t frame_size;
785 if (image_calc_rgb_size((size_t)ctx->width, (size_t)ctx->height, &frame_size) != ASCIICHAT_OK) {
786 log_error("Failed to calculate frame size: width=%d, height=%d (would overflow)", ctx->width, ctx->height);
787 image_destroy(img);
788 return NULL;
789 }
790 memcpy(img->pixels, ctx->buffers[buf.index].start, frame_size);
791 }
792
793 // Re-queue the buffer for future use
794 if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {
795 log_error("Failed to re-queue V4L2 buffer %u: %s (fd=%d, type=%d, memory=%d)", buf.index, SAFE_STRERROR(errno),
796 ctx->fd, buf.type, buf.memory);
797 // Still return the image - the frame data was already copied
798 }
799
800 return img;
801}
802
813 if (!ctx || !lifecycle_is_initialized(&ctx->async_lifecycle)) {
814 return NULL;
815 }
816
817 // Check if there's a new frame from the camera thread
818 image_t *new_frame = (image_t *)atomic_ptr_exchange(&ctx->latest_frame, NULL);
819
820 if (new_frame) {
821 // New frame available - update cache and return it
822 // (The background thread allocated this frame, we take ownership)
823 if (ctx->async_cached_frame) {
824 image_destroy(ctx->async_cached_frame);
825 }
826 ctx->async_cached_frame = new_frame;
827 return new_frame;
828 }
829
830 // No new frame - return the last cached frame (smooth playback without gaps)
831 return ctx->async_cached_frame;
832}
833
834asciichat_error_t webcam_get_dimensions(webcam_context_t *ctx, int *width, int *height) {
835 if (!ctx || !width || !height)
836 return ERROR_INVALID_PARAM;
837
838 *width = ctx->width;
839 *height = ctx->height;
840 return ASCIICHAT_OK;
841}
842
843asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count) {
844 if (!out_devices || !out_count) {
845 return SET_ERRNO(ERROR_INVALID_PARAM, "webcam_list_devices: invalid parameters");
846 }
847
848 *out_devices = NULL;
849 *out_count = 0;
850
851 // First pass: count valid video devices
852 unsigned int device_count = 0;
853 for (int i = 0; i <= WEBCAM_DEVICE_INDEX_MAX; i++) {
854 char device_path[32];
855 safe_snprintf(device_path, sizeof(device_path), "/dev/video%d", i);
856
857 int fd = open(device_path, O_RDONLY);
858 if (fd < 0) {
859 continue;
860 }
861
862 struct v4l2_capability cap;
863 if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0 && (cap.device_caps & V4L2_CAP_VIDEO_CAPTURE)) {
864 device_count++;
865 }
866 close(fd);
867 }
868
869 if (device_count == 0) {
870 // No devices found - not an error
871 return ASCIICHAT_OK;
872 }
873
874 // Allocate device array
876 if (!devices) {
877 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate device info array");
878 }
879
880 // Second pass: populate device info
881 unsigned int idx = 0;
882 for (int i = 0; i <= WEBCAM_DEVICE_INDEX_MAX && idx < device_count; i++) {
883 char device_path[32];
884 safe_snprintf(device_path, sizeof(device_path), "/dev/video%d", i);
885
886 int fd = open(device_path, O_RDONLY);
887 if (fd < 0) {
888 continue;
889 }
890
891 struct v4l2_capability cap;
892 if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0 && (cap.device_caps & V4L2_CAP_VIDEO_CAPTURE)) {
893 devices[idx].index = (unsigned int)i;
894 SAFE_STRNCPY(devices[idx].name, (const char *)cap.card, WEBCAM_DEVICE_NAME_MAX);
895 idx++;
896 }
897 close(fd);
898 }
899
900 *out_devices = devices;
901 *out_count = idx;
902
903 return ASCIICHAT_OK;
904}
905
907 SAFE_FREE(devices);
908}
909
910#endif // __linux__
void * atomic_ptr_exchange(atomic_ptr_t *a, void *new_value)
Atomically exchange a pointer and return the old value.
Definition atomic.c:295
⚛️ Atomic operations abstraction layer with debug tracking
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
Cross-platform error handling utilities.
Cross-platform filesystem operations.
unsigned int uint32_t
Definition common.h:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
#define SAFE_STRERROR(errnum)
Definition common.h:465
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned char uint8_t
Definition common.h:56
#define NAMED_REGISTER(ptr, name, type, fmt, parent_ptr)
Register any pointer with base name, type, format spec, and location (auto-suffix)
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
#define SET_ERRNO_SYS(code, context_msg,...)
Set error code with custom message and system error context, returning the error code.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_WEBCAM_IN_USE
Definition error_codes.h:65
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INIT
Definition error_codes.h:61
@ ERROR_WEBCAM_PERMISSION
Definition error_codes.h:66
@ ERROR_INVALID_PARAM
@ ERROR_WEBCAM
Definition error_codes.h:64
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
int platform_open(const char *name, const char *pathname, int flags,...)
Safe file open (open replacement)
#define PLATFORM_O_RDWR
Definition filesystem.h:84
int errno
image_t * image_new_copy(const image_t *source)
Create a copy of an existing image.
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.
image_t * webcam_read_async(webcam_context_t *ctx)
Get the most recent frame from async camera thread (non-blocking)
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.
Definition webcam.h:150
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.
Definition webcam.h:74
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.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🧵 Cross-platform thread interface for ascii-chat
Capture stdout/stderr and redirect to logging system.
#define LOG_IO(prefix, block)
Capture output from a code block and log it.
Definition io.h:81
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
void * asciichat_thread_t
#define asciichat_thread_join(thread, timeout_ms)
bool lifecycle_is_initialized(const lifecycle_t *lc)
Definition lifecycle.c:155
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108
Lock-free module lifecycle state machine using pure stdatomic.
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:708
platform_mmap_t mmap
Definition mmap.c:34
✅ Safe Integer Arithmetic and Overflow Detection
int frame
Definition splash.c:99
Atomic pointer wrapper.
Definition atomic.h:106
Image structure.
int w
Image width in pixels (must be > 0)
int h
Image height in pixels (must be > 0)
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order)
Webcam device information structure.
Definition webcam.h:86
unsigned int index
Device index (use with webcam_init_context)
Definition webcam.h:87
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