#include #include #include #include #include #include #include #include #include "v4l2_fmt.h" #include "xorg.h" #define N_BUFS 4 /* ------------------------------------------------------------------ */ /* YUYV → planar YUV420 conversion */ /* ------------------------------------------------------------------ */ static void yuyv_to_yuv420(const uint8_t *yuyv, int stride, int w, int h, uint8_t *y_out, uint8_t *cb_out, uint8_t *cr_out) { for (int row = 0; row < h; row++) { const uint8_t *src = yuyv + row * stride; uint8_t *dst = y_out + row * w; for (int col = 0; col < w; col++) { dst[col] = src[col * 2]; } } for (int row = 0; row < h; row += 2) { const uint8_t *src = yuyv + row * stride; int c_row = row / 2; for (int col = 0; col < w; col += 2) { cb_out[c_row * (w / 2) + col / 2] = src[col * 2 + 1]; cr_out[c_row * (w / 2) + col / 2] = src[col * 2 + 3]; } } } /* ------------------------------------------------------------------ */ /* Mmap buffers */ /* ------------------------------------------------------------------ */ typedef struct { void *start; size_t length; } Mmap_Buf; /* ------------------------------------------------------------------ */ /* Usage */ /* ------------------------------------------------------------------ */ static void usage(void) { fprintf(stderr, "usage: v4l2_view_cli [--device PATH]\n" " [--width N --height N]\n" " [--format mjpeg|yuyv]\n" " [--scale stretch|fit|fill|1:1]\n" " [--anchor center|topleft]\n" " [--x N] [--y N]\n" "\n" "Opens a V4L2 capture device and displays the live feed.\n" "Without --width/--height, selects the highest-FPS mode\n" "and within that the largest resolution.\n" "Q or Escape closes the window.\n" "\n" "defaults: /dev/video0 auto fit center at 0,0\n"); } /* ------------------------------------------------------------------ */ /* Main */ /* ------------------------------------------------------------------ */ int main(int argc, char **argv) { const char *device = "/dev/video0"; int req_width = 0; int req_height = 0; int win_x = 0; int win_y = 0; Xorg_Scale scale = XORG_SCALE_FIT; Xorg_Anchor anchor = XORG_ANCHOR_CENTER; uint32_t fmt_filter = 0; /* 0 = auto */ for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--device") == 0 && i + 1 < argc) { device = argv[++i]; } else if (strcmp(argv[i], "--width") == 0 && i + 1 < argc) { req_width = atoi(argv[++i]); } else if (strcmp(argv[i], "--height") == 0 && i + 1 < argc) { req_height = atoi(argv[++i]); } else if (strcmp(argv[i], "--x") == 0 && i + 1 < argc) { win_x = atoi(argv[++i]); } else if (strcmp(argv[i], "--y") == 0 && i + 1 < argc) { win_y = atoi(argv[++i]); } else if (strcmp(argv[i], "--format") == 0 && i + 1 < argc) { i++; if (strcmp(argv[i], "mjpeg") == 0) { fmt_filter = V4L2_PIX_FMT_MJPEG; } else if (strcmp(argv[i], "yuyv") == 0) { fmt_filter = V4L2_PIX_FMT_YUYV; } else { fprintf(stderr, "unknown format: %s\n", argv[i]); usage(); return 1; } } else if (strcmp(argv[i], "--scale") == 0 && i + 1 < argc) { i++; if (strcmp(argv[i], "stretch") == 0) { scale = XORG_SCALE_STRETCH; } else if (strcmp(argv[i], "fit") == 0) { scale = XORG_SCALE_FIT; } else if (strcmp(argv[i], "fill") == 0) { scale = XORG_SCALE_FILL; } else if (strcmp(argv[i], "1:1") == 0) { scale = XORG_SCALE_1_1; } else { fprintf(stderr, "unknown scale: %s\n", argv[i]); usage(); return 1; } } else if (strcmp(argv[i], "--anchor") == 0 && i + 1 < argc) { i++; if (strcmp(argv[i], "center") == 0) { anchor = XORG_ANCHOR_CENTER; } else if (strcmp(argv[i], "topleft") == 0) { anchor = XORG_ANCHOR_TOP_LEFT; } else { fprintf(stderr, "unknown anchor: %s\n", argv[i]); usage(); return 1; } } else { usage(); return 1; } } if (!xorg_available()) { fprintf(stderr, "v4l2_view_cli: built without HAVE_GLFW — viewer not available\n"); return 1; } /* ---------------------------------------------------------------- */ /* Open V4L2 device */ /* ---------------------------------------------------------------- */ int fd = open(device, O_RDWR | O_NONBLOCK); if (fd < 0) { perror(device); return 1; } struct v4l2_capability cap = {0}; if (v4l2_xioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) { perror("VIDIOC_QUERYCAP"); close(fd); return 1; } if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { fprintf(stderr, "%s: not a capture device\n", device); close(fd); return 1; } if (!(cap.capabilities & V4L2_CAP_STREAMING)) { fprintf(stderr, "%s: does not support streaming\n", device); close(fd); return 1; } /* ---------------------------------------------------------------- */ /* Format selection */ /* ---------------------------------------------------------------- */ int width, height, stride; int use_mjpeg; int sel_fps_n, sel_fps_d; if (req_width > 0 && req_height > 0) { /* * Explicit size requested — skip enumeration, negotiate directly. * Try MJPEG first (or whatever fmt_filter says), fall back to YUYV. */ struct v4l2_format fmt = {0}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = (uint32_t)req_width; fmt.fmt.pix.height = (uint32_t)req_height; fmt.fmt.pix.field = V4L2_FIELD_ANY; use_mjpeg = 0; if (fmt_filter != V4L2_PIX_FMT_YUYV) { fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; if (v4l2_xioctl(fd, VIDIOC_S_FMT, &fmt) == 0 && fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG) { use_mjpeg = 1; } } if (!use_mjpeg) { fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; if (v4l2_xioctl(fd, VIDIOC_S_FMT, &fmt) < 0 || fmt.fmt.pix.pixelformat != V4L2_PIX_FMT_YUYV) { fprintf(stderr, "%s: could not set %dx%d in MJPEG or YUYV\n", device, req_width, req_height); close(fd); return 1; } } width = (int)fmt.fmt.pix.width; height = (int)fmt.fmt.pix.height; stride = (int)fmt.fmt.pix.bytesperline; sel_fps_n = 0; sel_fps_d = 1; /* unknown until G_PARM below */ } else { /* Enumerate all supported modes and pick the best. */ V4l2_Fmt_Option *opts = malloc(V4L2_FMT_MAX_OPTS * sizeof(*opts)); if (!opts) { fprintf(stderr, "out of memory\n"); close(fd); return 1; } int n = v4l2_enumerate_formats(fd, opts, V4L2_FMT_MAX_OPTS, fmt_filter); if (n == 0) { fprintf(stderr, "%s: no usable formats found (MJPEG/YUYV)\n", device); free(opts); close(fd); return 1; } const V4l2_Fmt_Option *best = v4l2_select_best(opts, n); /* Apply the selected format. */ struct v4l2_format fmt = {0}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.pixelformat = best->pixfmt; fmt.fmt.pix.width = (uint32_t)best->w; fmt.fmt.pix.height = (uint32_t)best->h; fmt.fmt.pix.field = V4L2_FIELD_ANY; if (v4l2_xioctl(fd, VIDIOC_S_FMT, &fmt) < 0) { perror("VIDIOC_S_FMT"); free(opts); close(fd); return 1; } use_mjpeg = (fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG); width = (int)fmt.fmt.pix.width; height = (int)fmt.fmt.pix.height; stride = (int)fmt.fmt.pix.bytesperline; sel_fps_n = best->fps_n; sel_fps_d = best->fps_d; free(opts); } /* Request the selected frame rate (driver may ignore, but try). */ { struct v4l2_streamparm parm = {0}; parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; parm.parm.capture.timeperframe.numerator = (uint32_t)sel_fps_d; parm.parm.capture.timeperframe.denominator = (uint32_t)sel_fps_n; v4l2_xioctl(fd, VIDIOC_S_PARM, &parm); /* Read back what the driver actually set. */ if (v4l2_xioctl(fd, VIDIOC_G_PARM, &parm) == 0 && parm.parm.capture.timeperframe.denominator > 0) { sel_fps_n = (int)parm.parm.capture.timeperframe.denominator; sel_fps_d = (int)parm.parm.capture.timeperframe.numerator; } } printf("device: %s (%s)\n", device, (char *)cap.card); printf("format: %s %dx%d stride=%d target=%.1f fps\n", use_mjpeg ? "MJPEG" : "YUYV", width, height, stride, sel_fps_d > 0 ? (double)sel_fps_n / sel_fps_d : 0.0); /* ---------------------------------------------------------------- */ /* Mmap buffers + stream on */ /* ---------------------------------------------------------------- */ struct v4l2_requestbuffers req = {0}; req.count = N_BUFS; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (v4l2_xioctl(fd, VIDIOC_REQBUFS, &req) < 0) { perror("VIDIOC_REQBUFS"); close(fd); return 1; } Mmap_Buf bufs[N_BUFS] = {0}; for (unsigned i = 0; i < req.count; i++) { struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (v4l2_xioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) { perror("VIDIOC_QUERYBUF"); close(fd); return 1; } bufs[i].length = buf.length; bufs[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (bufs[i].start == MAP_FAILED) { perror("mmap"); close(fd); return 1; } } for (unsigned i = 0; i < req.count; i++) { struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (v4l2_xioctl(fd, VIDIOC_QBUF, &buf) < 0) { perror("VIDIOC_QBUF"); close(fd); return 1; } } enum v4l2_buf_type stream_type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (v4l2_xioctl(fd, VIDIOC_STREAMON, &stream_type) < 0) { perror("VIDIOC_STREAMON"); close(fd); return 1; } /* ---------------------------------------------------------------- */ /* Open viewer */ /* ---------------------------------------------------------------- */ Xorg_Viewer *v = xorg_viewer_open(win_x, win_y, width, height, "v4l2_view_cli"); if (!v) { fprintf(stderr, "v4l2_view_cli: failed to open viewer window\n"); v4l2_xioctl(fd, VIDIOC_STREAMOFF, &stream_type); close(fd); return 1; } xorg_viewer_set_scale(v, scale); xorg_viewer_set_anchor(v, anchor); /* ---------------------------------------------------------------- */ /* YUYV conversion buffer */ /* ---------------------------------------------------------------- */ uint8_t *yuv420_buf = NULL; if (!use_mjpeg) { yuv420_buf = malloc((size_t)(width * height * 3 / 2)); if (!yuv420_buf) { fprintf(stderr, "v4l2_view_cli: out of memory\n"); xorg_viewer_close(v); v4l2_xioctl(fd, VIDIOC_STREAMOFF, &stream_type); close(fd); return 1; } } /* ---------------------------------------------------------------- */ /* Capture loop */ /* ---------------------------------------------------------------- */ struct timespec t_fps; clock_gettime(CLOCK_MONOTONIC, &t_fps); int fps_frame_count = 0; float displayed_fps = 0.0f; /* Set initial info overlay; fps will be filled in once measured. */ const char *fmt_name = use_mjpeg ? "MJPEG" : "YUYV"; { char info[64]; snprintf(info, sizeof(info), "%s %dx%d @ --.- fps", fmt_name, width, height); xorg_viewer_set_overlay_text(v, 0, 10, 10, info, 1.0f, 1.0f, 0.8f); } while (1) { fd_set fds; FD_ZERO(&fds); FD_SET(fd, &fds); struct timeval tv = {1, 0}; int r = select(fd + 1, &fds, NULL, NULL, &tv); if (r < 0) { if (errno == EINTR) { continue; } perror("select"); break; } if (r == 0) { fprintf(stderr, "v4l2_view_cli: select timeout — no frames\n"); continue; } struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (v4l2_xioctl(fd, VIDIOC_DQBUF, &buf) < 0) { if (errno == EAGAIN) { continue; } perror("VIDIOC_DQBUF"); break; } const uint8_t *data = bufs[buf.index].start; if (use_mjpeg) { xorg_viewer_push_mjpeg(v, data, buf.bytesused); } else { uint8_t *y_p = yuv420_buf; uint8_t *cb_p = y_p + width * height; uint8_t *cr_p = cb_p + width * height / 4; yuyv_to_yuv420(data, stride, width, height, y_p, cb_p, cr_p); xorg_viewer_push_yuv420(v, y_p, cb_p, cr_p, width, height); } if (v4l2_xioctl(fd, VIDIOC_QBUF, &buf) < 0) { perror("VIDIOC_QBUF"); break; } /* Update FPS overlay every 0.5s. */ fps_frame_count++; struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); double elapsed = (now.tv_sec - t_fps.tv_sec) + (now.tv_nsec - t_fps.tv_nsec) * 1e-9; if (elapsed >= 0.5) { displayed_fps = (float)(fps_frame_count / elapsed); fps_frame_count = 0; t_fps = now; char info[64]; snprintf(info, sizeof(info), "%s %dx%d @ %.1f fps", fmt_name, width, height, displayed_fps); xorg_viewer_set_overlay_text(v, 0, 10, 10, info, 1.0f, 1.0f, 0.8f); } if (!xorg_viewer_handle_events(v)) { break; } } /* ---------------------------------------------------------------- */ /* Cleanup */ /* ---------------------------------------------------------------- */ xorg_viewer_close(v); v4l2_xioctl(fd, VIDIOC_STREAMOFF, &stream_type); for (unsigned i = 0; i < req.count; i++) { if (bufs[i].start && bufs[i].start != MAP_FAILED) { munmap(bufs[i].start, bufs[i].length); } } free(yuv420_buf); close(fd); return 0; }