root/usr.bin/videoctl/videoctl.c
/* $NetBSD: videoctl.c,v 1.4 2025/04/12 07:54:39 mlelstv Exp $ */

/*-
 * Copyright (c) 2010 Jared D. McNeill <jmcneill@invisible.ca>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/cdefs.h>
__COPYRIGHT("@(#) Copyright (c) 2010\
 Jared D. McNeill <jmcneill@invisible.ca>. All rights reserved.");
__RCSID("$NetBSD: videoctl.c,v 1.4 2025/04/12 07:54:39 mlelstv Exp $");

#include <sys/types.h>
#include <sys/endian.h>
#include <sys/ioctl.h>
#include <sys/videoio.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <paths.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include <util.h>
#include <vis.h>

__dead static void      usage(void);
static void             video_print(const char *);
static void             video_print_all(void);
static bool             video_print_caps(const char *);
static bool             video_print_formats(const char *);
static bool             video_print_inputs(const char *);
static bool             video_print_audios(const char *);
static bool             video_print_standards(const char *);
static bool             video_print_tuners(const char *);
static bool             video_print_ctrl(uint32_t);
static void             video_set(const char *);
static bool             video_set_ctrl(uint32_t, int32_t);
static const char *     video_cid2name(uint32_t);
static uint32_t         video_name2cid(const char *);

static const char *video_dev = NULL;
static int video_fd = -1;
static bool aflag = false;
static bool wflag = false;

static const struct {
        uint32_t        id;
        const char      *name;
} videoctl_cid_names[] = {
        { V4L2_CID_BRIGHTNESS,          "brightness" },
        { V4L2_CID_CONTRAST,            "contrast" },
        { V4L2_CID_SATURATION,          "saturation" },
        { V4L2_CID_HUE,                 "hue" },
        { V4L2_CID_AUDIO_VOLUME,        "audio_volume" },
        { V4L2_CID_AUDIO_BALANCE,       "audio_balance" },
        { V4L2_CID_AUDIO_BASS,          "audio_bass" },
        { V4L2_CID_AUDIO_TREBLE,        "audio_treble" },
        { V4L2_CID_AUDIO_MUTE,          "audio_mute" },
        { V4L2_CID_AUDIO_LOUDNESS,      "audio_loudness" },
        { V4L2_CID_BLACK_LEVEL,         "black_level" },
        { V4L2_CID_AUTO_WHITE_BALANCE,  "auto_white_balance" },
        { V4L2_CID_DO_WHITE_BALANCE,    "do_white_balance" },
        { V4L2_CID_RED_BALANCE,         "red_balance" },
        { V4L2_CID_BLUE_BALANCE,        "blue_balance" },
        { V4L2_CID_GAMMA,               "gamma" },
        { V4L2_CID_WHITENESS,           "whiteness" },
        { V4L2_CID_EXPOSURE,            "exposure" },
        { V4L2_CID_AUTOGAIN,            "autogain" },
        { V4L2_CID_GAIN,                "gain" },
        { V4L2_CID_HFLIP,               "hflip" },
        { V4L2_CID_VFLIP,               "vflip" },
        { V4L2_CID_HCENTER,             "hcenter" },
        { V4L2_CID_VCENTER,             "vcenter" },
        { V4L2_CID_POWER_LINE_FREQUENCY, "power_line_frequency" },
        { V4L2_CID_HUE_AUTO,            "hue_auto" },
        { V4L2_CID_WHITE_BALANCE_TEMPERATURE, "white_balance_temperature" },
        { V4L2_CID_SHARPNESS,           "sharpness" },
        { V4L2_CID_BACKLIGHT_COMPENSATION, "backlight_compensation" },
};

int
main(int argc, char *argv[])
{
        int ch;

        setprogname(argv[0]);

        while ((ch = getopt(argc, argv, "ad:w")) != -1) {
                switch (ch) {
                case 'a':
                        aflag = true;
                        break;
                case 'd':
                        video_dev = strdup(optarg);
                        break;
                case 'w':
                        wflag = true;
                        break;
                default:
                        usage();
                        /* NOTREACHED */
                }
        }
        argc -= optind;
        argv += optind;

        if (wflag && aflag)
                usage();
                /* NOTREACHED */
        if (wflag && argc == 0)
                usage();
                /* NOTREACHED */
        if (aflag && argc > 0)
                usage();
                /* NOTREACHED */
        if (!wflag && !aflag && argc == 0)
                usage();
                /* NOTREACHED */

        if (video_dev == NULL)
                video_dev = _PATH_VIDEO0;

        video_fd = open(video_dev, wflag ? O_RDWR : O_RDONLY);
        if (video_fd == -1)
                err(EXIT_FAILURE, "couldn't open '%s'", video_dev);

        if (aflag) {
                video_print_all();
        } else if (wflag) {
                while (argc > 0) {
                        video_set(argv[0]);
                        --argc;
                        ++argv;
                }
        } else {
                while (argc > 0) {
                        video_print(argv[0]);
                        --argc;
                        ++argv;
                }
        }

        close(video_fd);

        return EXIT_SUCCESS;
}

static void
usage(void)
{
        fprintf(stderr, "usage: %s [-d file] name ...\n", getprogname());
        fprintf(stderr, "usage: %s [-d file] -w name=value ...\n",
            getprogname());
        fprintf(stderr, "usage: %s [-d file] -a\n", getprogname());
        exit(EXIT_FAILURE);
}

static void
video_print_all(void)
{
        video_print_caps(NULL);
        video_print_formats(NULL);
        video_print_inputs(NULL);
        video_print_audios(NULL);
        video_print_standards(NULL);
        video_print_tuners(NULL);
        video_print_ctrl(0);
}

static bool
video_print_caps(const char *name)
{
        struct v4l2_capability cap;
        char capbuf[128];
        int error;
        bool found = false;

        if (strtok(NULL, ".") != NULL)
                return false;

        /* query capabilities */
        error = ioctl(video_fd, VIDIOC_QUERYCAP, &cap);
        if (error == -1)
                err(EXIT_FAILURE, "VIDIOC_QUERYCAP failed");

        if (!name || strcmp(name, "card") == 0) {
                printf("info.cap.card=%s\n", cap.card);
                found = true;
        }
        if (!name || strcmp(name, "driver") == 0) {
                printf("info.cap.driver=%s\n", cap.driver);
                found = true;
        }
        if (!name || strcmp(name, "bus_info") == 0) {
                printf("info.cap.bus_info=%s\n", cap.bus_info);
                found = true;
        }
        if (!name || strcmp(name, "version") == 0) {
                printf("info.cap.version=%u.%u.%u\n",
                    (cap.version >> 16) & 0xff,
                    (cap.version >> 8) & 0xff,
                    cap.version & 0xff);
                found = true;
        }
        if (!name || strcmp(name, "capabilities") == 0) {
                snprintb(capbuf, sizeof(capbuf), V4L2_CAP_BITMASK,
                    cap.capabilities);
                printf("info.cap.capabilities=%s\n", capbuf);
                found = true;
        }

        return found;
}

static bool
video_print_one_format(unsigned long fmtnum, unsigned long sizenum)
{
        struct v4l2_fmtdesc fmtdesc;
        struct v4l2_frmsizeenum framesize;
        unsigned long n;
        int error;

        fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        fmtdesc.index = fmtnum;
        error = ioctl(video_fd, VIDIOC_ENUM_FMT, &fmtdesc);
        if (error)
                return false;

        printf("info.format.%u=%s\n", fmtdesc.index,
            fmtdesc.description);

        if (sizenum == ULONG_MAX)
                n = 0;
        else
                n = sizenum;
                
        while (n <= sizenum) {
                framesize.index = n++;
                framesize.pixel_format = fmtdesc.pixelformat;
                error = ioctl(video_fd, VIDIOC_ENUM_FRAMESIZES, &framesize);
                if (error)
                        break;

                switch (framesize.type) {
                case V4L2_FRMIVAL_TYPE_DISCRETE:
                case V4L2_FRMIVAL_TYPE_CONTINUOUS:
                        printf("info.format.%u.size.%u=%ux%u\n",
                                fmtdesc.index, framesize.index,
                                framesize.discrete.width,
                                framesize.discrete.height);
                        break;
                case V4L2_FRMIVAL_TYPE_STEPWISE:
                        printf("info.format.%u.size.%u=(%u,%u,%u)x(%u,%u,%u)\n",
                                fmtdesc.index, framesize.index,
                                framesize.stepwise.min_width,
                                framesize.stepwise.max_width,
                                framesize.stepwise.step_width,
                                framesize.stepwise.min_height,
                                framesize.stepwise.max_height,
                                framesize.stepwise.step_height);
                        break;
                default:
                        printf("info.format.%u.size.%u=type %u\n",
                                fmtdesc.index, framesize.index,
                                framesize.type);
                }
        }

        return true;
}

static bool
video_print_formats(const char *name)
{
        unsigned long n, m = ULONG_MAX;
        const char *p;

        if (name == NULL) {
                /* enumerate formats */
                for (n = 0; ; n++) {
                        if (!video_print_one_format(n, m))
                                break;
                }
        } else {
                p = strtok(NULL, ".");
                n = strtoul(name, NULL, 10);
                if (n == ULONG_MAX)
                        return false;

                if (p != NULL) {
                        if (strcmp(p, "size") == 0) {
                                p = strtok(NULL, ".");
                                if (p != NULL) {
                                        m = strtoul(p, NULL, 10);
                                        if (m == ULONG_MAX)
                                                return false;
                                }
                        } else
                                return false;
                }


                video_print_one_format(n, m);
        }

        return true;
}

static bool
video_print_inputs(const char *name)
{
        struct v4l2_input input;
        int error;

        if (name == NULL) {
                /* enumerate inputs */
                for (input.index = 0; ; input.index++) {
                        error = ioctl(video_fd, VIDIOC_ENUMINPUT, &input);
                        if (error)
                                break;
                        printf("info.input.%u=%s\n", input.index, input.name);
                        printf("info.input.%u.type=", input.index);
                        switch (input.type) {
                        case V4L2_INPUT_TYPE_TUNER:
                                printf("tuner\n");
                                break;
                        case V4L2_INPUT_TYPE_CAMERA:
                                printf("baseband\n");
                                break;
                        default:
                                printf("unknown (%d)\n", input.type);
                                break;
                        }
                }
        } else {
                unsigned long n;
                char *s;

                n = strtoul(name, NULL, 10);
                if (n == ULONG_MAX)
                        return false;
                input.index = n;
                error = ioctl(video_fd, VIDIOC_ENUMINPUT, &input);
                if (error)
                        return false;

                s = strtok(NULL, ".");
                if (s == NULL) {
                        printf("info.input.%u=%s\n", input.index, input.name);
                } else if (strcmp(s, "type") == 0) {
                        if (strtok(NULL, ".") != NULL)
                                return false;
                        printf("info.input.%u.type=", input.index);
                        switch (input.type) {
                        case V4L2_INPUT_TYPE_TUNER:
                                printf("tuner\n");
                                break;
                        case V4L2_INPUT_TYPE_CAMERA:
                                printf("baseband\n");
                                break;
                        default:
                                printf("unknown (%d)\n", input.type);
                                break;
                        }
                } else
                        return false;
        }

        return true;
}

static bool
video_print_audios(const char *name)
{
        struct v4l2_audio audio;
        int error;

        if (name == NULL) {
                /* enumerate audio */
                for (audio.index = 0; ; audio.index++) {
                        error = ioctl(video_fd, VIDIOC_ENUMAUDIO, &audio);
                        if (error)
                                break;
                        printf("info.audio.%u=%s\n", audio.index, audio.name);
                        printf("info.audio.%u.stereo=%d\n", audio.index,
                            audio.capability & V4L2_AUDCAP_STEREO ? 1 : 0);
                        printf("info.audio.%u.avl=%d\n", audio.index,
                            audio.capability & V4L2_AUDCAP_AVL ? 1 : 0);
                }
        } else {
                unsigned long n;
                char *s;

                n = strtoul(name, NULL, 10);
                if (n == ULONG_MAX)
                        return false;
                audio.index = n;
                error = ioctl(video_fd, VIDIOC_ENUMAUDIO, &audio);
                if (error)
                        return false;

                s = strtok(NULL, ".");
                if (s == NULL) {
                        printf("info.audio.%u=%s\n", audio.index, audio.name);
                } else if (strcmp(s, "stereo") == 0) {
                        if (strtok(NULL, ".") != NULL)
                                return false;
                        printf("info.audio.%u.stereo=%d\n", audio.index,
                            audio.capability & V4L2_AUDCAP_STEREO ? 1 : 0);
                } else if (strcmp(s, "avl") == 0) {
                        if (strtok(NULL, ".") != NULL)
                                return false;
                        printf("info.audio.%u.avl=%d\n", audio.index,
                            audio.capability & V4L2_AUDCAP_AVL ? 1 : 0);
                } else
                        return false;
        }

        return true;
}

static bool
video_print_standards(const char *name)
{
        struct v4l2_standard std;
        int error;

        if (name == NULL) {
                /* enumerate standards */
                for (std.index = 0; ; std.index++) {
                        error = ioctl(video_fd, VIDIOC_ENUMSTD, &std);
                        if (error)
                                break;
                        printf("info.standard.%u=%s\n", std.index, std.name);
                }
        } else {
                unsigned long n;

                if (strtok(NULL, ".") != NULL)
                        return false;

                n = strtoul(name, NULL, 10);
                if (n == ULONG_MAX)
                        return false;
                std.index = n;
                error = ioctl(video_fd, VIDIOC_ENUMSTD, &std);
                if (error)
                        return false;
                printf("info.standard.%u=%s\n", std.index, std.name);
        }

        return true;
}

static bool
video_print_tuners(const char *name)
{
        struct v4l2_tuner tuner;
        int error;

        if (name == NULL) {
                /* enumerate tuners */
                for (tuner.index = 0; ; tuner.index++) {
                        error = ioctl(video_fd, VIDIOC_G_TUNER, &tuner);
                        if (error)
                                break;
                        printf("info.tuner.%u=%s\n", tuner.index, tuner.name);
                }
        } else {
                unsigned long n;

                if (strtok(NULL, ".") != NULL)
                        return false;

                n = strtoul(name, NULL, 10);
                if (n == ULONG_MAX)
                        return false;
                tuner.index = n;
                error = ioctl(video_fd, VIDIOC_G_TUNER, &tuner);
                if (error)
                        return false;
                printf("info.tuner.%u=%s\n", tuner.index, tuner.name);
        }

        return true;
}

static void
video_print(const char *name)
{
        char *buf, *s, *s2 = NULL;
        bool found = false;

        buf = strdup(name);
        s = strtok(buf, ".");
        if (s == NULL)
                return;

        if (strcmp(s, "info") == 0) {
                s = strtok(NULL, ".");
                if (s)
                        s2 = strtok(NULL, ".");
                if (s == NULL || strcmp(s, "cap") == 0) {
                        found = video_print_caps(s2);
                }
                if (s == NULL || strcmp(s, "format") == 0) {
                        found = video_print_formats(s2);
                }
                if (s == NULL || strcmp(s, "input") == 0) {
                        found = video_print_inputs(s2);
                }
                if (s == NULL || strcmp(s, "audio") == 0) {
                        found = video_print_audios(s2);
                }
                if (s == NULL || strcmp(s, "standard") == 0) {
                        found = video_print_standards(s2);
                }
                if (s == NULL || strcmp(s, "tuner") == 0) {
                        found = video_print_tuners(s2);
                }
        } else if (strcmp(s, "ctrl") == 0) {
                s = strtok(NULL, ".");
                if (s)
                        s2 = strtok(NULL, ".");

                if (s == NULL)
                        found = video_print_ctrl(0);
                else if (s && !s2)
                        found = video_print_ctrl(video_name2cid(s));
        }

        free(buf);
        if (!found)
                fprintf(stderr, "%s: field %s does not exist\n",
                    getprogname(), name);
}

static bool
video_print_ctrl(uint32_t ctrl_id)
{
        struct v4l2_control ctrl;
        const char *ctrlname;
        bool found = false;
        int error;

        for (ctrl.id = V4L2_CID_BASE; ctrl.id != V4L2_CID_LASTP1; ctrl.id++) {
                if (ctrl_id != 0 && ctrl_id != ctrl.id)
                        continue;
                error = ioctl(video_fd, VIDIOC_G_CTRL, &ctrl);
                if (error)
                        continue;
                ctrlname = video_cid2name(ctrl.id);
                if (ctrlname)
                        printf("ctrl.%s=%d\n", ctrlname, ctrl.value);
                else
                        printf("ctrl.%08x=%d\n", ctrl.id, ctrl.value);
                found = true;
        }

        return found;
}

static void
video_set(const char *name)
{
        char *buf, *key, *value;
        bool found = false;
        long n;

        if (strchr(name, '=') == NULL) {
                fprintf(stderr, "%s: No '=' in %s\n", getprogname(), name);
                exit(EXIT_FAILURE);
        }

        buf = strdup(name);
        key = strtok(buf, "=");
        if (key == NULL)
                usage();
                /* NOTREACHED */
        value = strtok(NULL, "");
        if (value == NULL)
                usage();
                /* NOTREACHED */

        if (strncmp(key, "info.", strlen("info.")) == 0) {
                fprintf(stderr, "'info' subtree read-only\n");
                found = true;
                goto done;
        }
        if (strncmp(key, "ctrl.", strlen("ctrl.")) == 0) {
                char *ctrlname = key + strlen("ctrl.");
                uint32_t ctrl_id = video_name2cid(ctrlname);

                n = strtol(value, NULL, 0);
                if (n == LONG_MIN || n == LONG_MAX)
                        goto done;
                found = video_set_ctrl(ctrl_id, n);
        }

done:
        free(buf);
        if (!found)
                fprintf(stderr, "%s: field %s does not exist\n",
                    getprogname(), name);
}

static bool
video_set_ctrl(uint32_t ctrl_id, int32_t value)
{
        struct v4l2_control ctrl;
        const char *ctrlname;
        int32_t ovalue;
        int error;

        ctrlname = video_cid2name(ctrl_id);

        ctrl.id = ctrl_id;
        error = ioctl(video_fd, VIDIOC_G_CTRL, &ctrl);
        if (error)
                return false;
        ovalue = ctrl.value;
        ctrl.value = value;
        error = ioctl(video_fd, VIDIOC_S_CTRL, &ctrl);
        if (error)
                err(EXIT_FAILURE, "VIDIOC_S_CTRL failed for '%s'", ctrlname);
        error = ioctl(video_fd, VIDIOC_G_CTRL, &ctrl);
        if (error)
                err(EXIT_FAILURE, "VIDIOC_G_CTRL failed for '%s'", ctrlname);

        if (ctrlname)
                printf("ctrl.%s: %d -> %d\n", ctrlname, ovalue, ctrl.value);
        else
                printf("ctrl.%08x: %d -> %d\n", ctrl.id, ovalue, ctrl.value);

        return true;
}

static const char *
video_cid2name(uint32_t id)
{
        unsigned int i;

        for (i = 0; i < __arraycount(videoctl_cid_names); i++)
                if (videoctl_cid_names[i].id == id)
                        return videoctl_cid_names[i].name;

        return NULL;
}

static uint32_t
video_name2cid(const char *name)
{
        unsigned int i;

        for (i = 0; i < __arraycount(videoctl_cid_names); i++)
                if (strcmp(name, videoctl_cid_names[i].name) == 0)
                        return videoctl_cid_names[i].id;

        return (uint32_t)-1;
}