builtin-programs/camera/enumerate.folk

if {$::tcl_platform(os) ne "linux"} { return }

set cc [C]
$cc include <fcntl.h>
$cc include <unistd.h>
$cc include <sys/ioctl.h>
$cc include <linux/videodev2.h>
$cc proc getInfoForCamera {char* camera} Jim_Obj* {
    int fd = open(camera, O_RDWR);
    FOLK_ENSURE(fd >= 0);

    Jim_Obj *infoObj = Jim_NewDictObj(interp, NULL, 0);

    struct v4l2_capability cap;
    if (ioctl(fd, VIDIOC_QUERYCAP, &cap) >= 0) {
        Jim_DictAddElement(interp, infoObj,
                           Jim_NewStringObj(interp, "card", -1),
                           Jim_NewStringObj(interp, (const char *)cap.card, -1));
    }

    Jim_Obj *formatsList = Jim_NewListObj(interp, NULL, 0);

    // Enumerate pixel formats
    struct v4l2_fmtdesc fmt_desc = {0};
    fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt_desc) == 0) {
        char fourcc_str[5];
        fourcc_str[0] = fmt_desc.pixelformat & 0xFF;
        fourcc_str[1] = (fmt_desc.pixelformat >> 8) & 0xFF;
        fourcc_str[2] = (fmt_desc.pixelformat >> 16) & 0xFF;
        fourcc_str[3] = (fmt_desc.pixelformat >> 24) & 0xFF;
        fourcc_str[4] = '\0';

        Jim_Obj *resolutionsList = Jim_NewListObj(interp, NULL, 0);

        struct v4l2_frmsizeenum frm_size = {0};
        frm_size.pixel_format = fmt_desc.pixelformat;

        while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frm_size) == 0) {
            if (frm_size.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
                Jim_Obj *frameratesList = Jim_NewListObj(interp, NULL, 0);

                struct v4l2_frmivalenum frm_interval = {0};
                frm_interval.pixel_format = fmt_desc.pixelformat;
                frm_interval.width = frm_size.discrete.width;
                frm_interval.height = frm_size.discrete.height;

                while (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frm_interval) == 0) {
                    if (frm_interval.type == V4L2_FRMIVAL_TYPE_DISCRETE) {
                        double fps = (double)frm_interval.discrete.denominator /
                                    frm_interval.discrete.numerator;
                        Jim_ListAppendElement(interp, frameratesList, Jim_NewDoubleObj(interp, fps));
                    } else if (frm_interval.type == V4L2_FRMIVAL_TYPE_STEPWISE) {
                        double min_fps = (double)frm_interval.stepwise.max.denominator /
                                        frm_interval.stepwise.max.numerator;
                        double max_fps = (double)frm_interval.stepwise.min.denominator /
                                        frm_interval.stepwise.min.numerator;

                        Jim_Obj *rangeDict = Jim_ObjPrintf("min %f max %f", min_fps, max_fps);
                        Jim_ListAppendElement(interp, frameratesList, rangeDict);
                    }
                    frm_interval.index++;
                }

                int frameratesLen;
                const char *frameratesStr = Jim_GetString(frameratesList, &frameratesLen);
                Jim_Obj *resDict = Jim_ObjPrintf("width %u height %u framerates {%s}",
                    frm_size.discrete.width,
                    frm_size.discrete.height,
                    frameratesStr);

                Jim_ListAppendElement(interp, resolutionsList, resDict);
            }
            frm_size.index++;
        }

        int resolutionsLen;
        const char *resolutionsStr = Jim_GetString(resolutionsList, &resolutionsLen);
        Jim_Obj *formatDict = Jim_ObjPrintf("fourcc {%s} description {%s} resolutions {%s}",
            fourcc_str,
            (char*)fmt_desc.description,
            resolutionsStr);

        Jim_ListAppendElement(interp, formatsList, formatDict);

        fmt_desc.index++;
    }
    Jim_DictAddElement(interp, infoObj,
                       Jim_NewStringObj(interp, "formats", -1),
                       formatsList);

    close(fd);
    return infoObj;
}
set formatsLib [$cc compile]

set camerasByCanonicalName [dict create]
set cameras [glob -nocomplain "/dev/v4l/by-path/*"]
# sort first so the order (and therefore which dedupe wins) is
# consistent across boots.
set cameras [lsort $cameras]
foreach camera $cameras {
    # I would prefer to use by-id, but not all cameras show up in
    # by-id (webcam on my Dell laptop does not, for instance).
    try {
        set canonicalName [file readlink $camera]
    } on error e {
        set canonicalName $camera
    }
    if {[dict exists $camerasByCanonicalName $canonicalName]} {
        # Skip cameras that we already have by another name, so there
        # aren't dupes in the enumeration (in particular, by-path
        # often has both -usb- and -usbv2- copies of a camera).
        continue
    }
    dict set camerasByCanonicalName $canonicalName $camera

    set info [$formatsLib getInfoForCamera $camera]
    Claim $::thisNode has camera $camera with {*}$info
}