/*
 * V4L2 像素格式与分辨率枚举示例
 *
 * 本例演示如何通过 V4L2 ioctl 接口:
 *   1. 枚举摄像头设备支持的所有像素格式(VIDIOC_ENUM_FMT)
 *   2. 对每种像素格式,枚举其支持的分辨率/帧尺寸(VIDIOC_ENUM_FRAMESIZES)
 *
 * 典型的 V4L2 枚举模式:
 *   设置结构体 -> 设置 index -> 调用 ioctl -> index++ 直到 ioctl 返回负值
 */

#include <sys/ioctl.h>   /* ioctl() */
#include <fcntl.h>       /* open() */
#include <linux/videodev2.h> /* V4L2 相关结构体与 ioctl 宏 */
#include <unistd.h>      /* close() */
#include <stdio.h>       /* printf() */
#include <stdlib.h>      /* EXIT_FAILURE 等 */
#include <string.h>      /* memset() */

/* 默认摄像头设备节点,可通过修改此变量更换设备 */
static char *dev = "/dev/video0";

/*
 * enum_framesizes - 枚举指定像素格式支持的帧尺寸(分辨率)
 * @fd:            打开的 video 设备文件描述符
 * @pixelformat:   要查询的像素格式 FourCC 码(如 V4L2_PIX_FMT_YUYV)
 *
 * 使用 VIDIOC_ENUM_FRAMESIZES ioctl 查询驱动报告的分辨率信息。
 * v4l2_frmsizeenum.type 有三种可能,分别对应不同的查询方式:
 *
 *   V4L2_FRMSIZE_TYPE_DISCRETE   离散列表
 *     驱动提供一个分辨率列表。需要递增 frmsize.index 反复调用,
 *     每次返回一个 (width, height) 对,直到 ioctl 返回负值(-1)表示枚举结束。
 *     常见于硬件编码器或老式 sensor。
 *
 *   V4L2_FRMSIZE_TYPE_STEPWISE   步进范围
 *     驱动报告一个矩形范围 (min_width~max_width, min_height~max_height)
 *     以及水平和垂直的步进值 (step_width, step_height)。
 *     实际可用分辨率为范围内按步进递增的所有组合。
 *     一次调用即可获得全部信息,无需循环。
 *
 *   V4L2_FRMSIZE_TYPE_CONTINUOUS 连续范围
 *     驱动报告 min~max 范围,此范围内任意分辨率都支持。
 *     实际上与 STEPWISE 使用相同的 union stepwise 字段,但步进视为 1。
 *     一次调用即可获得全部信息,无需循环。
 *
 * 返回值:无(结果直接 printf 打印)
 */
static void enum_framesizes(int fd, unsigned int pixelformat)
{
    struct v4l2_frmsizeenum frmsize;

    printf("       分辨率:\n");

    /*
     * 离散列表需要多次调用。index 从 0 开始递增,
     * 直到 ioctl 返回负值(表示没有更多分辨率了)。
     * 对于 STEPWISE 和 CONTINUOUS,第一次调用就已经拿到结果,
     * 第二次调用 ioctl 就会返回负值,因此也会直接退出循环。
     */
    for (int i = 0; ; i++) {
        memset(&frmsize, 0, sizeof(frmsize));
        frmsize.pixel_format = pixelformat;
        frmsize.index = i;

        /* VIDIOC_ENUM_FRAMESIZES 根据 pixel_format + index 查询分辨率 */
        if (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) < 0)
            break;

        switch (frmsize.type) {
        case V4L2_FRMSIZE_TYPE_DISCRETE:
            /* 离散分辨率:每次返回一个具体的宽高值 */
            printf("          [%d] %dx%d\n", i,
                   frmsize.discrete.width,
                   frmsize.discrete.height);
            break;

        case V4L2_FRMSIZE_TYPE_STEPWISE:
            /*
             * 步进范围:min~max 范围内按 step 递增。
             * 例如 min_width=320, max_width=1920, step_width=160 表示
             * 支持 320, 480, 640, ..., 1920 这些宽度值。
             * 一次调用即得全部信息,return 退出函数,不再继续循环。
             */
            printf("          stepwise: %dx%d ~ %dx%d  step %dx%d\n",
                   frmsize.stepwise.min_width,  frmsize.stepwise.min_height,
                   frmsize.stepwise.max_width,  frmsize.stepwise.max_height,
                   frmsize.stepwise.step_width, frmsize.stepwise.step_height);
            return;

        case V4L2_FRMSIZE_TYPE_CONTINUOUS:
            /*
             * 连续范围:min~max 之间任意分辨率都支持。
             * 实际上驱动通常会限制一定的粒度,这里只是"名义上连续"。
             * 一次调用即得全部信息,return 退出函数。
             */
            printf("          continuous: %dx%d ~ %dx%d\n",
                   frmsize.stepwise.min_width,  frmsize.stepwise.min_height,
                   frmsize.stepwise.max_width,  frmsize.stepwise.max_height);
            return;
        }
    }
}

/*
 * enum_format_with_sizes - 枚举所有像素格式并打印其分辨率
 * @fd: 打开的 video 设备文件描述符
 *
 * 分两步:
 *   1. 使用 VIDIOC_ENUM_FMT 遍历驱动所有支持的像素格式
 *   2. 对每种格式调用 enum_framesizes() 获取分辨率
 *
 * VIDIOC_ENUM_FMT 使用说明:
 *   - 设置 fmt.type 为 V4L2_BUF_TYPE_VIDEO_CAPTURE(捕获设备)
 *   - 从 fmt.index = 0 开始递增调用 ioctl
 *   - 每次返回一种像素格式的 pixelformat(FourCC 码)和 description(可读名称)
 *   - 当 ioctl 返回负值时表示所有格式已枚举完毕
 *   - 每次调用前必须使用 memset 清零结构体,避免内核解析残留数据
 *
 * 为什么需要先清零结构体:
 *   V4L2 ioctl 采用"结构体传入+传出"模式。内核在解析输入字段时,
 *   如果结构体中有未初始化的字段(例如 padding 字节),可能导致
 *   内核在不同内核版本间行为不一致。memset 清零确保所有字段初始
 *   为 0,保证兼容性。
 */
static void enum_format_with_sizes(int fd)
{
    struct v4l2_fmtdesc fmt;

    printf("====== 枚举像素格式及支持的分辨率 ======\n");

    /* memset 清零:避免结构体中的残留数据影响 ioctl 行为 */
    memset(&fmt, 0, sizeof(fmt));

    /* 设置缓冲区类型为视频捕获,其他类型还有 V4L2_BUF_TYPE_VIDEO_OUTPUT 等 */
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    /*
     * index 从 0 开始递增枚举。驱动会按顺序返回每种格式,
     * 当枚举完所有格式后,ioctl 返回负值(errno 通常为 EINVAL)。
     * 这种"递增 index 直到失败"的模式在 V4L2 枚举接口中非常常见
     * (格式、分辨率、帧率、controls 等都采用此模式)。
     */
    for (fmt.index = 0; ; fmt.index++) {
        if (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) < 0)
            break;

        /* fmt.pixelformat 是 FourCC 码(如 0x56595559 即 "YUYV") */
        printf("  [%d] 0x%08x  %s\n",
               fmt.index,
               fmt.pixelformat,
               fmt.description);

        /* 对每种格式查询其支持的分辨率 */
        enum_framesizes(fd, fmt.pixelformat);
    }
    printf("\n");
}

int main(void)
{
    int fd;

    /*
     * 打开 V4L2 设备节点。
     * O_RDWR:读写模式,既可用于查询(G_FMT/ENUM_FMT)也可用于设置(S_FMT)。
     * 部分设备可能只需要 O_RDONLY,但 O_RDWR 兼容性最好。
     */
    fd = open(dev, O_RDWR);
    if (fd < 0) {
        perror("open");  /* perror 会打印 "open: <系统错误信息>" */
        return -1;
    }

    /* 枚举所有像素格式及其支持的分辨率 */
    enum_format_with_sizes(fd);

    close(fd);
    return 0;
}

结果

root@wyl:~/c-std/for-linux/099_other/v4l2# ./002_v4l2_fmt_pix
====== 枚举像素格式及支持的分辨率 ======
  [0] 0x56595559  YUYV 4:2:2
       分辨率:
          [0] 96x200
          [1] 200x96
          [2] 8x8448
          [3] 8448x8
          [4] 4x2321
          [5] 96x100
          [6] 4x2637
          [7] 4x14733
          [8] 4x5188
          [9] 5188x4
          [10] 8x8642
          [11] 8642x8
          [12] 96x96
          [13] 240x240