Ver código fonte

First pass at pipeline implementation

Benjamin Schaaf 4 anos atrás
pai
commit
0ba1a6844e
9 arquivos alterados com 1564 adições e 1 exclusões
  1. 665 0
      camera.c
  2. 68 0
      camera.h
  3. 402 0
      device.c
  4. 48 0
      device.h
  5. 5 1
      meson.build
  6. 137 0
      pipeline.c
  7. 17 0
      pipeline.h
  8. 51 0
      tools/list_devices.c
  9. 171 0
      tools/test_camera.c

+ 665 - 0
camera.c

@@ -0,0 +1,665 @@
+#include "camera.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <glib.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+
+#define MAX_VIDEO_BUFFERS 20
+
+static const char *pixel_format_names[MP_PIXEL_FMT_MAX] = {
+    "unsupported",
+    "BGGR8",
+    "GBRG8",
+    "GRBG8",
+    "RGGB8",
+};
+
+const char *mp_pixel_format_to_str(uint32_t pixel_format)
+{
+    g_return_val_if_fail(pixel_format < MP_PIXEL_FMT_MAX, "INVALID");
+    return pixel_format_names[pixel_format];
+}
+
+MPPixelFormat mp_pixel_format_from_str(const char *name)
+{
+    for (MPPixelFormat i = 0; i < MP_PIXEL_FMT_MAX; ++i) {
+        if (strcasecmp(pixel_format_names[i], name) == 0) {
+            return i;
+        }
+    }
+    g_return_val_if_reached(MP_PIXEL_FMT_UNSUPPORTED);
+}
+
+static const uint32_t pixel_format_v4l_pixel_formats[MP_PIXEL_FMT_MAX] = {
+    0,
+    V4L2_PIX_FMT_SBGGR8,
+    V4L2_PIX_FMT_SGBRG8,
+    V4L2_PIX_FMT_SGRBG8,
+    V4L2_PIX_FMT_SRGGB8,
+};
+
+uint32_t mp_pixel_format_to_v4l_pixel_format(MPPixelFormat pixel_format)
+{
+    g_return_val_if_fail(pixel_format < MP_PIXEL_FMT_MAX, 0);
+    return pixel_format_v4l_pixel_formats[pixel_format];
+}
+
+MPPixelFormat mp_pixel_format_from_v4l_pixel_format(uint32_t v4l_pixel_format)
+{
+    for (MPPixelFormat i = 0; i < MP_PIXEL_FMT_MAX; ++i) {
+        if (pixel_format_v4l_pixel_formats[i] == v4l_pixel_format) {
+            return i;
+        }
+    }
+    return MP_PIXEL_FMT_UNSUPPORTED;
+}
+
+static const uint32_t pixel_format_v4l_bus_codes[MP_PIXEL_FMT_MAX] = {
+    0,
+    MEDIA_BUS_FMT_SBGGR8_1X8,
+    MEDIA_BUS_FMT_SGBRG8_1X8,
+    MEDIA_BUS_FMT_SGRBG8_1X8,
+    MEDIA_BUS_FMT_SRGGB8_1X8,
+};
+
+uint32_t mp_pixel_format_to_v4l_bus_code(MPPixelFormat pixel_format)
+{
+    g_return_val_if_fail(pixel_format < MP_PIXEL_FMT_MAX, 0);
+    return pixel_format_v4l_bus_codes[pixel_format];
+}
+
+MPPixelFormat mp_pixel_format_from_v4l_bus_code(uint32_t v4l_bus_code)
+{
+    for (MPPixelFormat i = 0; i < MP_PIXEL_FMT_MAX; ++i) {
+        if (pixel_format_v4l_bus_codes[i] == v4l_bus_code) {
+            return i;
+        }
+    }
+    return MP_PIXEL_FMT_UNSUPPORTED;
+}
+
+uint32_t mp_pixel_format_bytes_per_pixel(MPPixelFormat pixel_format)
+{
+    g_return_val_if_fail(pixel_format < MP_PIXEL_FMT_MAX, 0);
+    switch (pixel_format) {
+        case MP_PIXEL_FMT_BGGR8:
+        case MP_PIXEL_FMT_GBRG8:
+        case MP_PIXEL_FMT_GRBG8:
+        case MP_PIXEL_FMT_RGGB8: return 1;
+        default: return 0;
+    }
+}
+
+bool mp_camera_mode_is_equivalent(const MPCameraMode *m1, const MPCameraMode *m2)
+{
+    return m1->pixel_format == m2->pixel_format
+        && m1->frame_interval.numerator == m2->frame_interval.numerator
+        && m1->frame_interval.denominator == m2->frame_interval.denominator
+        && m1->width == m2->width
+        && m1->height == m2->height;
+}
+
+struct video_buffer {
+    uint32_t length;
+    uint8_t *data;
+};
+
+struct _MPCamera {
+    int video_fd;
+    int subdev_fd;
+
+    bool has_set_mode;
+    MPCameraMode current_mode;
+
+    struct video_buffer buffers[MAX_VIDEO_BUFFERS];
+    uint32_t num_buffers;
+};
+
+MPCamera *mp_camera_new(int video_fd, int subdev_fd)
+{
+    g_return_val_if_fail(video_fd != -1, NULL);
+
+    MPCamera *camera = malloc(sizeof(MPCamera));
+    camera->video_fd = video_fd;
+    camera->subdev_fd = subdev_fd;
+    camera->has_set_mode = false;
+    camera->num_buffers = 0;
+    return camera;
+}
+
+void mp_camera_free(MPCamera *camera)
+{
+    g_warn_if_fail(camera->num_buffers == 0);
+    if (camera->num_buffers != 0) {
+        mp_camera_stop_capture(camera);
+    }
+
+    free(camera);
+}
+
+bool mp_camera_is_subdev(MPCamera *camera)
+{
+    return camera->subdev_fd != -1;
+}
+
+int mp_camera_get_video_fd(MPCamera *camera)
+{
+    return camera->video_fd;
+}
+
+int mp_camera_get_subdev_fd(MPCamera *camera)
+{
+    return camera->subdev_fd;
+}
+
+static void errno_printerr(const char *s)
+{
+    g_printerr("MPCamera: %s error %d, %s\n", s, errno, strerror(errno));
+}
+
+static int xioctl(int fd, int request, void *arg)
+{
+    int r;
+    do {
+        r = ioctl(fd, request, arg);
+    } while (r == -1 && errno == EINTR);
+    return r;
+}
+
+bool mp_camera_try_mode(MPCamera *camera, MPCameraMode *mode)
+{
+    struct v4l2_format fmt = {};
+    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    fmt.fmt.pix.width = mode->width;
+    fmt.fmt.pix.height = mode->height;
+    fmt.fmt.pix.pixelformat = mp_pixel_format_from_v4l_pixel_format(mode->pixel_format);
+    fmt.fmt.pix.field = V4L2_FIELD_ANY;
+    if (xioctl(camera->video_fd, VIDIOC_TRY_FMT, &fmt) == -1) {
+        errno_printerr("VIDIOC_S_FMT");
+        return false;
+    }
+
+    mode->width = fmt.fmt.pix.width;
+    mode->height = fmt.fmt.pix.height;
+    mode->pixel_format = mp_pixel_format_from_v4l_pixel_format(fmt.fmt.pix.pixelformat);
+
+    return true;
+}
+
+const MPCameraMode *mp_camera_get_mode(const MPCamera *camera)
+{
+    return &camera->current_mode;
+}
+
+bool mp_camera_set_mode(MPCamera *camera, MPCameraMode *mode)
+{
+    // Set the mode in the subdev the camera is one
+    if (mp_camera_is_subdev(camera))
+    {
+        struct v4l2_subdev_frame_interval interval = {};
+        interval.pad = 0;
+        interval.interval = mode->frame_interval;
+        if (xioctl(camera->subdev_fd, VIDIOC_SUBDEV_S_FRAME_INTERVAL, &interval) == -1) {
+            errno_printerr("VIDIOC_SUBDEV_S_FRAME_INTERVAL");
+            return false;
+        }
+
+        bool did_set_frame_rate =
+            interval.interval.numerator == mode->frame_interval.numerator
+            && interval.interval.denominator == mode->frame_interval.denominator;
+
+        struct v4l2_subdev_format fmt = {};
+        fmt.pad = 0;
+        fmt.which = V4L2_SUBDEV_FORMAT_ACTIVE;
+        fmt.format.width = mode->width;
+        fmt.format.height = mode->height;
+        fmt.format.code = mp_pixel_format_to_v4l_bus_code(mode->pixel_format);
+        fmt.format.field = V4L2_FIELD_ANY;
+        if (xioctl(camera->subdev_fd, VIDIOC_SUBDEV_S_FMT, &fmt) == -1) {
+            errno_printerr("VIDIOC_SUBDEV_S_FMT");
+            return false;
+        }
+
+        // Some drivers like ov5640 don't allow you to set the frame format with
+        // too high a frame-rate, but that means the frame-rate won't be set
+        // after the format change. So we need to try again here if we didn't
+        // succeed before. Ideally we'd be able to set both at once.
+        if (!did_set_frame_rate)
+        {
+            interval.interval = mode->frame_interval;
+            if (xioctl(camera->subdev_fd, VIDIOC_SUBDEV_S_FRAME_INTERVAL, &interval) == -1) {
+                errno_printerr("VIDIOC_SUBDEV_S_FRAME_INTERVAL");
+                return false;
+            }
+        }
+
+        // Update the mode
+        mode->pixel_format = mp_pixel_format_from_v4l_bus_code(fmt.format.code);
+        mode->frame_interval = interval.interval;
+        mode->width = fmt.format.width;
+        mode->height = fmt.format.height;
+    }
+
+    // Set the mode for the video device
+    {
+        struct v4l2_format fmt = {};
+        fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+        fmt.fmt.pix.width = mode->width;
+        fmt.fmt.pix.height = mode->height;
+        fmt.fmt.pix.pixelformat = mp_pixel_format_to_v4l_pixel_format(mode->pixel_format);
+        fmt.fmt.pix.field = V4L2_FIELD_ANY;
+        if (xioctl(camera->video_fd, VIDIOC_S_FMT, &fmt) == -1) {
+            errno_printerr("VIDIOC_S_FMT");
+            return false;
+        }
+
+        // Update the mode
+        mode->pixel_format = mp_pixel_format_from_v4l_pixel_format(fmt.fmt.pix.pixelformat);
+        mode->width = fmt.fmt.pix.width;
+        mode->height = fmt.fmt.pix.height;
+    }
+
+    camera->has_set_mode = true;
+    camera->current_mode = *mode;
+
+    return true;
+}
+
+bool mp_camera_start_capture(MPCamera *camera)
+{
+    g_return_val_if_fail(camera->has_set_mode, false);
+    g_return_val_if_fail(camera->num_buffers == 0, false);
+
+    // Start by requesting buffers
+    struct v4l2_requestbuffers req = {};
+    req.count = MAX_VIDEO_BUFFERS;
+    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    req.memory = V4L2_MEMORY_MMAP;
+
+    if (xioctl(camera->video_fd, VIDIOC_REQBUFS, &req) == -1) {
+        errno_printerr("VIDIOC_REQBUFS");
+        return false;
+    }
+
+    if (req.count < 2) {
+        g_printerr("Insufficient buffer memory. Only %d buffers available.\n",
+                   req.count);
+        goto error;
+    }
+
+    for (uint32_t i = 0; i < req.count; ++i) {
+        // Query each buffer and mmap it
+        struct v4l2_buffer buf = {
+            .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
+            .memory = V4L2_MEMORY_MMAP,
+            .index = i,
+        };
+
+        if (xioctl(camera->video_fd, VIDIOC_QUERYBUF, &buf) == -1) {
+            errno_printerr("VIDIOC_QUERYBUF");
+            break;
+        }
+
+        camera->buffers[i].length = buf.length;
+        camera->buffers[i].data = mmap(
+            NULL,
+            buf.length,
+            PROT_READ,
+            MAP_SHARED,
+            camera->video_fd,
+            buf.m.offset);
+
+        if (camera->buffers[i].data == MAP_FAILED) {
+            errno_printerr("mmap");
+            break;
+        }
+
+        ++camera->num_buffers;
+    }
+
+    if (camera->num_buffers != req.count) {
+        g_printerr("Unable to map all buffers\n");
+        goto error;
+    }
+
+    for (uint32_t i = 0; i < camera->num_buffers; ++i) {
+        struct v4l2_buffer buf = {
+            .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
+            .memory = V4L2_MEMORY_MMAP,
+            .index = i,
+        };
+
+        // Queue the buffer for capture
+        if (xioctl(camera->video_fd, VIDIOC_QBUF, &buf) == -1) {
+            errno_printerr("VIDIOC_QBUF");
+            goto error;
+        }
+    }
+
+    // Start capture
+    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    if (xioctl(camera->video_fd, VIDIOC_STREAMON, &type) == -1) {
+        errno_printerr("VIDIOC_STREAMON");
+        goto error;
+    }
+
+    return true;
+
+error:
+    // Unmap any mapped buffers
+    assert(camera->num_buffers <= MAX_VIDEO_BUFFERS);
+    for (uint32_t i = 0; i < camera->num_buffers; ++i) {
+        if (munmap(camera->buffers[i].data, camera->buffers[i].length) == -1) {
+            errno_printerr("munmap");
+        }
+    }
+
+    // Reset allocated buffers
+    {
+        struct v4l2_requestbuffers req = {};
+        req.count = 0;
+        req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+        req.memory = V4L2_MEMORY_MMAP;
+
+        if (xioctl(camera->video_fd, VIDIOC_REQBUFS, &req) == -1) {
+            errno_printerr("VIDIOC_REQBUFS");
+        }
+    }
+
+    return false;
+}
+
+bool mp_camera_stop_capture(MPCamera *camera)
+{
+    g_return_val_if_fail(camera->num_buffers > 0, false);
+
+    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    if (xioctl(camera->video_fd, VIDIOC_STREAMOFF, &type) == -1) {
+        errno_printerr("VIDIOC_STREAMOFF");
+    }
+
+    assert(camera->num_buffers <= MAX_VIDEO_BUFFERS);
+    for (int i = 0; i < camera->num_buffers; ++i) {
+        if (munmap(camera->buffers[i].data, camera->buffers[i].length) == -1) {
+            errno_printerr("munmap");
+        }
+    }
+
+    camera->num_buffers = 0;
+
+    struct v4l2_requestbuffers req = {};
+    req.count = 0;
+    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    req.memory = V4L2_MEMORY_MMAP;
+    if (xioctl(camera->video_fd, VIDIOC_REQBUFS, &req) == -1) {
+        errno_printerr("VIDIOC_REQBUFS");
+    }
+
+    return true;
+}
+
+bool mp_camera_is_capturing(MPCamera *camera)
+{
+    return camera->num_buffers > 0;
+}
+
+bool mp_camera_capture_image(MPCamera *camera, void (*callback)(MPImage, void *), void *user_data)
+{
+    struct v4l2_buffer buf = {};
+    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    buf.memory = V4L2_MEMORY_MMAP;
+    if (xioctl(camera->video_fd, VIDIOC_DQBUF, &buf) == -1) {
+        switch (errno) {
+            case EAGAIN:
+                return true;
+            case EIO:
+                /* Could ignore EIO, see spec. */
+                /* fallthrough */
+            default:
+                errno_printerr("VIDIOC_DQBUF");
+                return false;
+        }
+    }
+
+    uint32_t pixel_format = camera->current_mode.pixel_format;
+    uint32_t width = camera->current_mode.width;
+    uint32_t height = camera->current_mode.height;
+
+    assert(buf.bytesused == mp_pixel_format_bytes_per_pixel(pixel_format) * width * height);
+    assert(buf.bytesused == camera->buffers[buf.index].length);
+
+    MPImage image = {
+        .pixel_format = pixel_format,
+        .width = width,
+        .height = height,
+        .data = camera->buffers[buf.index].data,
+    };
+
+    callback(image, user_data);
+
+    // The callback may have stopped the capture, only queue the buffer if we're
+    // still capturing.
+    if (mp_camera_is_capturing(camera)) {
+        if (xioctl(camera->video_fd, VIDIOC_QBUF, &buf) == -1) {
+            errno_printerr("VIDIOC_QBUF");
+            return false;
+        }
+    }
+
+    return true;
+}
+
+struct _MPCameraModeList {
+    MPCameraMode mode;
+    MPCameraModeList *next;
+};
+
+static MPCameraModeList *
+get_subdev_modes(MPCamera *camera, bool (*check)(MPCamera *, MPCameraMode *))
+{
+    MPCameraModeList *item = NULL;
+
+    for (uint32_t fmt_index = 0;; ++fmt_index) {
+        struct v4l2_subdev_mbus_code_enum fmt = {};
+        fmt.index = fmt_index;
+        fmt.pad = 0;
+        fmt.which = V4L2_SUBDEV_FORMAT_TRY;
+        if (xioctl(camera->subdev_fd, VIDIOC_SUBDEV_ENUM_MBUS_CODE, &fmt) == -1) {
+            if (errno != EINVAL) {
+                errno_printerr("VIDIOC_SUBDEV_ENUM_MBUS_CODE");
+            }
+            break;
+        }
+
+        // Skip unsupported formats
+        uint32_t format = mp_pixel_format_from_v4l_bus_code(fmt.code);
+        if (format == MP_PIXEL_FMT_UNSUPPORTED) {
+            continue;
+        }
+
+        for (uint32_t frame_index = 0;; ++frame_index) {
+            struct v4l2_subdev_frame_size_enum frame = {};
+            frame.index = frame_index;
+            frame.pad = 0;
+            frame.code = fmt.code;
+            frame.which = V4L2_SUBDEV_FORMAT_TRY;
+            if (xioctl(camera->subdev_fd, VIDIOC_SUBDEV_ENUM_FRAME_SIZE, &frame) == -1) {
+                if (errno != EINVAL) {
+                    errno_printerr("VIDIOC_SUBDEV_ENUM_FRAME_SIZE");
+                }
+                break;
+            }
+
+            // TODO: Handle other types
+            if (frame.min_width != frame.max_width
+                || frame.min_height != frame.max_height) {
+                break;
+            }
+
+            for (uint32_t interval_index = 0;; ++interval_index) {
+                struct v4l2_subdev_frame_interval_enum interval = {};
+                interval.index = interval_index;
+                interval.pad = 0;
+                interval.code = fmt.code;
+                interval.width = frame.max_width;
+                interval.height = frame.max_height;
+                interval.which = V4L2_SUBDEV_FORMAT_TRY;
+                if (xioctl(camera->subdev_fd, VIDIOC_SUBDEV_ENUM_FRAME_INTERVAL, &interval) == -1) {
+                    if (errno != EINVAL) {
+                        errno_printerr("VIDIOC_SUBDEV_ENUM_FRAME_INTERVAL");
+                    }
+                    break;
+                }
+
+                MPCameraMode mode = {
+                    .pixel_format = format,
+                    .frame_interval = interval.interval,
+                    .width = frame.max_width,
+                    .height = frame.max_height,
+                };
+
+                if (!check(camera, &mode)) {
+                    continue;
+                }
+
+                MPCameraModeList *new_item = malloc(sizeof(MPCameraModeList));
+                new_item->mode = mode;
+                new_item->next = item;
+                item = new_item;
+            }
+        }
+    }
+
+    return item;
+}
+
+static MPCameraModeList *
+get_video_modes(MPCamera *camera, bool (*check)(MPCamera *, MPCameraMode *))
+{
+    MPCameraModeList *item = NULL;
+
+    for (uint32_t fmt_index = 0;; ++fmt_index) {
+        struct v4l2_fmtdesc fmt = {};
+        fmt.index = fmt_index;
+        fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+        if (xioctl(camera->video_fd, VIDIOC_ENUM_FMT, &fmt) == -1) {
+            if (errno != EINVAL) {
+                errno_printerr("VIDIOC_ENUM_FMT");
+            }
+            break;
+        }
+
+        // Skip unsupported formats
+        uint32_t format = mp_pixel_format_from_v4l_pixel_format(fmt.pixelformat);
+        if (format == MP_PIXEL_FMT_UNSUPPORTED) {
+            continue;
+        }
+
+        for (uint32_t frame_index = 0;; ++frame_index) {
+            struct v4l2_frmsizeenum frame = {};
+            frame.index = frame_index;
+            frame.pixel_format = fmt.pixelformat;
+            if (xioctl(camera->video_fd, VIDIOC_ENUM_FRAMESIZES, &frame) == -1) {
+                if (errno != EINVAL) {
+                    errno_printerr("VIDIOC_ENUM_FRAMESIZES");
+                }
+                break;
+            }
+
+            // TODO: Handle other types
+            if (frame.type != V4L2_FRMSIZE_TYPE_DISCRETE) {
+                break;
+            }
+
+            for (uint32_t interval_index = 0;; ++interval_index) {
+                struct v4l2_frmivalenum interval = {};
+                interval.index = interval_index;
+                interval.pixel_format = fmt.pixelformat;
+                interval.width = frame.discrete.width;
+                interval.height = frame.discrete.height;
+                if (xioctl(camera->video_fd, VIDIOC_ENUM_FRAMEINTERVALS, &interval) == -1) {
+                    if (errno != EINVAL) {
+                        errno_printerr("VIDIOC_ENUM_FRAMESIZES");
+                    }
+                    break;
+                }
+
+                // TODO: Handle other types
+                if (interval.type != V4L2_FRMIVAL_TYPE_DISCRETE) {
+                    break;
+                }
+
+                MPCameraMode mode = {
+                    .pixel_format = format,
+                    .frame_interval = interval.discrete,
+                    .width = frame.discrete.width,
+                    .height = frame.discrete.height,
+                };
+
+                if (!check(camera, &mode)) {
+                    continue;
+                }
+
+                MPCameraModeList *new_item = malloc(sizeof(MPCameraModeList));
+                new_item->mode = mode;
+                new_item->next = item;
+                item = new_item;
+            }
+        }
+    }
+
+    return item;
+}
+
+static bool all_modes(MPCamera *camera, MPCameraMode *mode)
+{
+    return true;
+}
+
+static bool available_modes(MPCamera *camera, MPCameraMode *mode)
+{
+    MPCameraMode attempt = *mode;
+    return mp_camera_try_mode(camera, &attempt)
+        && mp_camera_mode_is_equivalent(mode, &attempt);
+}
+
+MPCameraModeList *mp_camera_list_supported_modes(MPCamera *camera)
+{
+    if (mp_camera_is_subdev(camera)) {
+        return get_subdev_modes(camera, all_modes);
+    } else {
+        return get_video_modes(camera, all_modes);
+    }
+}
+
+MPCameraModeList *mp_camera_list_available_modes(MPCamera *camera)
+{
+    if (mp_camera_is_subdev(camera)) {
+        return get_subdev_modes(camera, available_modes);
+    } else {
+        return get_video_modes(camera, available_modes);
+    }
+}
+
+MPCameraMode *mp_camera_mode_list_get(MPCameraModeList *list)
+{
+    g_return_val_if_fail(list, NULL);
+    return &list->mode;
+}
+
+MPCameraModeList *mp_camera_mode_list_next(MPCameraModeList *list)
+{
+    g_return_val_if_fail(list, NULL);
+    return list->next;
+}
+
+void mp_camera_mode_list_free(MPCameraModeList *list)
+{
+    while (list) {
+        MPCameraModeList *tmp = list;
+        list = tmp->next;
+        free(tmp);
+    }
+}

+ 68 - 0
camera.h

@@ -0,0 +1,68 @@
+#pragma once
+
+#include <linux/v4l2-subdev.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+typedef enum {
+    MP_PIXEL_FMT_UNSUPPORTED,
+    MP_PIXEL_FMT_BGGR8,
+    MP_PIXEL_FMT_GBRG8,
+    MP_PIXEL_FMT_GRBG8,
+    MP_PIXEL_FMT_RGGB8,
+
+    MP_PIXEL_FMT_MAX,
+} MPPixelFormat;
+
+MPPixelFormat mp_pixel_format_from_str(const char *str);
+const char *mp_pixel_format_to_str(MPPixelFormat pixel_format);
+
+MPPixelFormat mp_pixel_format_from_v4l_pixel_format(uint32_t v4l_pixel_format);
+MPPixelFormat mp_pixel_format_from_v4l_bus_code(uint32_t v4l_bus_code);
+uint32_t mp_pixel_format_to_v4l_pixel_format(MPPixelFormat pixel_format);
+uint32_t mp_pixel_format_to_v4l_bus_code(MPPixelFormat pixel_format);
+
+uint32_t mp_pixel_format_bytes_per_pixel(MPPixelFormat pixel_format);
+
+typedef struct {
+    MPPixelFormat pixel_format;
+
+    struct v4l2_fract frame_interval;
+    uint32_t width;
+    uint32_t height;
+} MPCameraMode;
+
+bool mp_camera_mode_is_equivalent(const MPCameraMode *m1, const MPCameraMode *m2);
+
+typedef struct {
+    uint32_t pixel_format;
+    uint32_t width;
+    uint32_t height;
+    uint8_t *data;
+} MPImage;
+
+typedef struct _MPCamera MPCamera;
+
+MPCamera *mp_camera_new(int video_fd, int subdev_fd);
+void mp_camera_free(MPCamera *camera);
+
+bool mp_camera_is_subdev(MPCamera *camera);
+int mp_camera_get_video_fd(MPCamera *camera);
+int mp_camera_get_subdev_fd(MPCamera *camera);
+
+const MPCameraMode *mp_camera_get_mode(const MPCamera *camera);
+bool mp_camera_try_mode(MPCamera *camera, MPCameraMode *mode);
+
+bool mp_camera_set_mode(MPCamera *camera, MPCameraMode *mode);
+bool mp_camera_start_capture(MPCamera *camera);
+bool mp_camera_stop_capture(MPCamera *camera);
+bool mp_camera_is_capturing(MPCamera *camera);
+bool mp_camera_capture_image(MPCamera *camera, void (*callback)(MPImage, void *), void *user_data);
+
+typedef struct _MPCameraModeList MPCameraModeList;
+
+MPCameraModeList *mp_camera_list_supported_modes(MPCamera *camera);
+MPCameraModeList *mp_camera_list_available_modes(MPCamera *camera);
+MPCameraMode *mp_camera_mode_list_get(MPCameraModeList *list);
+MPCameraModeList *mp_camera_mode_list_next(MPCameraModeList *list);
+void mp_camera_mode_list_free(MPCameraModeList *list);

+ 402 - 0
device.c

@@ -0,0 +1,402 @@
+#include "device.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <glib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+bool mp_find_device_path(struct media_v2_intf_devnode devnode, char *path, int length)
+{
+    char uevent_path[256];
+    snprintf(uevent_path, 256, "/sys/dev/char/%d:%d/uevent", devnode.major, devnode.minor);
+
+    FILE *f = fopen(uevent_path, "r");
+    if (!f) {
+        return false;
+    }
+
+    char line[512];
+    while (fgets(line, 512, f)) {
+        if (strncmp(line, "DEVNAME=", 8) == 0) {
+            // Drop newline
+            int length = strlen(line);
+            if (line[length - 1] == '\n')
+                line[length - 1] = '\0';
+
+            snprintf(path, length, "/dev/%s", line + 8);
+            return true;
+        }
+    }
+
+    fclose(f);
+
+    return false;
+}
+
+struct _MPDevice {
+    int fd;
+
+    struct media_device_info info;
+
+    struct media_v2_entity *entities;
+    size_t num_entities;
+    struct media_v2_interface *interfaces;
+    size_t num_interfaces;
+    struct media_v2_pad *pads;
+    size_t num_pads;
+    struct media_v2_link *links;
+    size_t num_links;
+};
+
+static void errno_printerr(const char *s)
+{
+    g_printerr("MPDevice: %s error %d, %s\n", s, errno, strerror(errno));
+}
+
+static int xioctl(int fd, int request, void *arg)
+{
+    int r;
+    do {
+        r = ioctl(fd, request, arg);
+    } while (r == -1 && errno == EINTR);
+    return r;
+}
+
+MPDevice *mp_device_find(const char *driver_name)
+{
+    MPDevice *found_device = NULL;
+
+    int length = strlen(driver_name);
+    MPDeviceList *list = mp_device_list_new();
+
+    for (MPDeviceList *item = list; item; item = mp_device_list_next(item)) {
+        MPDevice *device = mp_device_list_get(item);
+        const struct media_device_info *info = mp_device_get_info(device);
+
+        if (strncmp(info->driver, driver_name, length) == 0) {
+            found_device = mp_device_list_remove(&item);
+            break;
+        }
+    }
+
+    mp_device_list_free(list);
+
+    return found_device;
+}
+
+MPDevice *mp_device_open(const char *path)
+{
+    int fd = open(path, O_RDWR);
+    if (fd == -1) {
+        errno_printerr("open");
+        return NULL;
+    }
+
+    return mp_device_new(fd);
+}
+
+MPDevice *mp_device_new(int fd)
+{
+    // Get the topology of the media device
+    struct media_v2_topology topology = {};
+    if (xioctl(fd, MEDIA_IOC_G_TOPOLOGY, &topology) == -1
+        || topology.num_entities == 0) {
+        close(fd);
+        return NULL;
+    }
+
+    // Create the device
+    MPDevice *device = calloc(1, sizeof(MPDevice));
+    device->fd = fd;
+    device->entities = calloc(topology.num_entities, sizeof(struct media_v2_entity));
+    device->num_entities = topology.num_entities;
+    device->interfaces = calloc(topology.num_interfaces, sizeof(struct media_v2_interface));
+    device->num_interfaces = topology.num_interfaces;
+    device->pads = calloc(topology.num_pads, sizeof(struct media_v2_pad));
+    device->num_pads = topology.num_pads;
+    device->links = calloc(topology.num_links, sizeof(struct media_v2_link));
+    device->num_links = topology.num_links;
+
+    // Get the actual devices and interfaces
+    topology.ptr_entities = (uint64_t)device->entities;
+    topology.ptr_interfaces = (uint64_t)device->interfaces;
+    topology.ptr_pads = (uint64_t)device->pads;
+    topology.ptr_links = (uint64_t)device->links;
+    if (xioctl(fd, MEDIA_IOC_G_TOPOLOGY, &topology) == -1) {
+        errno_printerr("MEDIA_IOC_G_TOPOLOGY");
+        mp_device_close(device);
+        return NULL;
+    }
+
+    // Get device info
+    if (xioctl(fd, MEDIA_IOC_DEVICE_INFO, &device->info) == -1) {
+        errno_printerr("MEDIA_IOC_DEVICE_INFO");
+        mp_device_close(device);
+        return NULL;
+    }
+
+    return device;
+}
+
+void mp_device_close(MPDevice *device)
+{
+    close(device->fd);
+    free(device->entities);
+    free(device->interfaces);
+    free(device->pads);
+    free(device->links);
+    free(device);
+}
+
+bool mp_device_setup_link(MPDevice *device, uint32_t source_pad_id, uint32_t sink_pad_id, bool enabled)
+{
+    const struct media_v2_pad *source_pad = mp_device_get_pad(device, source_pad_id);
+    g_return_val_if_fail(source_pad, false);
+
+    const struct media_v2_pad *sink_pad = mp_device_get_pad(device, sink_pad_id);
+    g_return_val_if_fail(sink_pad, false);
+
+    struct media_link_desc link = {};
+    link.flags = enabled ? MEDIA_LNK_FL_ENABLED : 0;
+    link.source.entity = source_pad->entity_id;
+    link.source.index = 0;
+    link.sink.entity = sink_pad->entity_id;
+    link.sink.index = 0;
+    if (xioctl(device->fd, MEDIA_IOC_SETUP_LINK, &link) == -1) {
+        errno_printerr("MEDIA_IOC_SETUP_LINK");
+        return false;
+    }
+
+    return true;
+}
+
+const struct media_v2_entity *mp_device_find_entity(const MPDevice *device, const char *driver_name)
+{
+    int length = strlen(driver_name);
+
+    // Find the entity from the name
+    for (uint32_t i = 0; i < device->num_entities; ++i) {
+        if (strncmp(device->entities[i].name, driver_name, length) == 0) {
+            return &device->entities[i];
+        }
+    }
+    return NULL;
+}
+
+const struct media_device_info *mp_device_get_info(const MPDevice *device)
+{
+    return &device->info;
+}
+
+const struct media_v2_entity *mp_device_get_entity(const MPDevice *device, uint32_t id)
+{
+    for (int i = 0; i < device->num_entities; ++i) {
+        if (device->entities[i].id == id) {
+            return &device->entities[i];
+        }
+    }
+    return NULL;
+}
+
+const struct media_v2_entity *mp_device_get_entities(const MPDevice *device)
+{
+    return device->entities;
+}
+
+size_t mp_device_get_num_entities(const MPDevice *device)
+{
+    return device->num_entities;
+}
+
+const struct media_v2_interface *mp_device_find_entity_interface(const MPDevice *device, uint32_t entity_id)
+{
+    // Find the interface through the link
+    const struct media_v2_link *link = mp_device_find_link_to(device, entity_id);
+    if (!link) {
+        return NULL;
+    }
+    return mp_device_get_interface(device, link->source_id);
+}
+
+const struct media_v2_interface *mp_device_get_interface(const MPDevice *device, uint32_t id)
+{
+    for (int i = 0; i < device->num_interfaces; ++i) {
+        if (device->interfaces[i].id == id) {
+            return &device->interfaces[i];
+        }
+    }
+    return NULL;
+}
+
+const struct media_v2_interface *mp_device_get_interfaces(const MPDevice *device)
+{
+    return device->interfaces;
+}
+
+size_t mp_device_get_num_interfaces(const MPDevice *device)
+{
+    return device->num_interfaces;
+}
+
+const struct media_v2_pad *mp_device_get_pad_from_entity(const MPDevice *device, uint32_t entity_id)
+{
+    for (int i = 0; i < device->num_pads; ++i) {
+        if (device->pads[i].entity_id == entity_id) {
+            return &device->pads[i];
+        }
+    }
+    return NULL;
+}
+
+const struct media_v2_pad *mp_device_get_pad(const MPDevice *device, uint32_t id)
+{
+    for (int i = 0; i < device->num_pads; ++i) {
+        if (device->pads[i].id == id) {
+            return &device->pads[i];
+        }
+    }
+    return NULL;
+}
+
+const struct media_v2_pad *mp_device_get_pads(const MPDevice *device)
+{
+    return device->pads;
+}
+
+size_t mp_device_get_num_pads(const MPDevice *device)
+{
+    return device->num_pads;
+}
+
+const struct media_v2_link *mp_device_find_entity_link(const MPDevice *device, uint32_t entity_id)
+{
+    const struct media_v2_pad *pad = mp_device_get_pad_from_entity(device, entity_id);
+    const struct media_v2_link *link = mp_device_find_link_to(device, pad->id);
+    if (link) {
+        return link;
+    }
+    return mp_device_find_link_from(device, pad->id);
+}
+
+const struct media_v2_link *mp_device_find_link_from(const MPDevice *device, uint32_t source)
+{
+    for (int i = 0; i < device->num_links; ++i) {
+        if (device->links[i].source_id == source) {
+            return &device->links[i];
+        }
+    }
+    return NULL;
+}
+
+const struct media_v2_link *mp_device_find_link_to(const MPDevice *device, uint32_t sink)
+{
+    for (int i = 0; i < device->num_links; ++i) {
+        if (device->links[i].sink_id == sink) {
+            return &device->links[i];
+        }
+    }
+    return NULL;
+}
+
+const struct media_v2_link *mp_device_find_link_between(const MPDevice *device, uint32_t source, uint32_t sink)
+{
+    for (int i = 0; i < device->num_links; ++i) {
+        if (device->links[i].source_id == source
+            && device->links[i].sink_id == sink) {
+            return &device->links[i];
+        }
+    }
+    return NULL;
+}
+
+const struct media_v2_link *mp_device_get_link(const MPDevice *device, uint32_t id)
+{
+    for (int i = 0; i < device->num_links; ++i) {
+        if (device->links[i].id == id) {
+            return &device->links[i];
+        }
+    }
+    return NULL;
+}
+
+const struct media_v2_link *mp_device_get_links(const MPDevice *device)
+{
+    return device->links;
+}
+
+size_t mp_device_get_num_links(const MPDevice *device)
+{
+    return device->num_links;
+}
+
+struct _MPDeviceList {
+    MPDevice *device;
+    MPDeviceList *next;
+};
+
+MPDeviceList *mp_device_list_new()
+{
+    MPDeviceList *current = NULL;
+
+    // Enumerate media device files
+    struct dirent *dir;
+    DIR *d = opendir("/dev");
+    while ((dir = readdir(d)) != NULL) {
+        if (strncmp(dir->d_name, "media", 5) == 0) {
+            char path[261];
+            snprintf(path, 261, "/dev/%s", dir->d_name);
+
+            MPDevice *device = mp_device_open(path);
+
+            if (device) {
+                MPDeviceList *next = malloc(sizeof(MPDeviceList));
+                next->device = device;
+                next->next = current;
+                current = next;
+            }
+        }
+    }
+    closedir(d);
+
+    return current;
+}
+
+void mp_device_list_free(MPDeviceList *device_list)
+{
+    while (device_list) {
+        MPDeviceList *tmp = device_list;
+        device_list = tmp->next;
+
+        mp_device_close(tmp->device);
+        free(tmp);
+    }
+}
+
+MPDevice *mp_device_list_remove(MPDeviceList **device_list)
+{
+    MPDevice *device = (*device_list)->device;
+
+    if ((*device_list)->next) {
+        MPDeviceList *tmp = (*device_list)->next;
+        **device_list = *tmp;
+        free(tmp);
+    } else {
+        free(*device_list);
+        *device_list = NULL;
+    }
+
+    return device;
+}
+
+MPDevice *mp_device_list_get(const MPDeviceList *device_list)
+{
+    return device_list->device;
+}
+
+MPDeviceList *mp_device_list_next(const MPDeviceList *device_list)
+{
+    return device_list->next;
+}

+ 48 - 0
device.h

@@ -0,0 +1,48 @@
+#pragma once
+
+#include <linux/media.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+bool mp_find_device_path(struct media_v2_intf_devnode devnode, char *path, int length);
+
+typedef struct _MPDevice MPDevice;
+
+MPDevice *mp_device_find(const char *driver_name);
+MPDevice *mp_device_open(const char *path);
+MPDevice *mp_device_new(int fd);
+void mp_device_close(MPDevice *device);
+
+bool mp_device_setup_link(MPDevice *device, uint32_t source_pad_id, uint32_t sink_pad_id, bool enabled);
+
+const struct media_device_info *mp_device_get_info(const MPDevice *device);
+const struct media_v2_entity *mp_device_find_entity(const MPDevice *device, const char *driver_name);
+const struct media_v2_entity *mp_device_get_entity(const MPDevice *device, uint32_t id);
+const struct media_v2_entity *mp_device_get_entities(const MPDevice *device);
+size_t mp_device_get_num_entities(const MPDevice *device);
+const struct media_v2_interface *mp_device_find_entity_interface(const MPDevice *device, uint32_t entity_id);
+const struct media_v2_interface *mp_device_get_interface(const MPDevice *device, uint32_t id);
+const struct media_v2_interface *mp_device_get_interfaces(const MPDevice *device);
+size_t mp_device_get_num_interfaces(const MPDevice *device);
+const struct media_v2_pad *mp_device_get_pad_from_entity(const MPDevice *device, uint32_t entity_id);
+const struct media_v2_pad *mp_device_get_pad(const MPDevice *device, uint32_t id);
+const struct media_v2_pad *mp_device_get_pads(const MPDevice *device);
+size_t mp_device_get_num_pads(const MPDevice *device);
+const struct media_v2_link *mp_device_find_entity_link(const MPDevice *device, uint32_t entity_id);
+const struct media_v2_link *mp_device_find_link_from(const MPDevice *device, uint32_t source);
+const struct media_v2_link *mp_device_find_link_to(const MPDevice *device, uint32_t sink);
+const struct media_v2_link *mp_device_find_link_between(const MPDevice *device, uint32_t source, uint32_t sink);
+const struct media_v2_link *mp_device_get_link(const MPDevice *device, uint32_t id);
+const struct media_v2_link *mp_device_get_links(const MPDevice *device);
+size_t mp_device_get_num_links(const MPDevice *device);
+
+typedef struct _MPDeviceList MPDeviceList;
+
+MPDeviceList *mp_device_list_new();
+void mp_device_list_free(MPDeviceList *device_list);
+
+MPDevice *mp_device_list_remove(MPDeviceList **device_list);
+
+MPDevice *mp_device_list_get(const MPDeviceList *device_list);
+MPDeviceList *mp_device_list_next(const MPDeviceList *device_list);

+ 5 - 1
meson.build

@@ -2,6 +2,7 @@ project('megapixels', 'c')
 gnome = import('gnome')
 gtkdep = dependency('gtk+-3.0')
 tiff = dependency('libtiff-4')
+threads = dependency('threads')
 
 cc = meson.get_compiler('c')
 libm = cc.find_library('m', required: false)
@@ -15,7 +16,7 @@ configure_file(
   output: 'config.h',
   configuration: conf )
 
-executable('megapixels', 'main.c', 'ini.c', 'quickpreview.c', resources, dependencies : [gtkdep, libm, tiff], install : true)
+executable('megapixels', 'main.c', 'ini.c', 'quickpreview.c', 'camera.c', 'device.c', 'pipeline.c', resources, dependencies : [gtkdep, libm, tiff, threads], install : true)
 
 install_data(['data/org.postmarketos.Megapixels.desktop'],
              install_dir : get_option('datadir') / 'applications')
@@ -38,3 +39,6 @@ install_data([
 install_data(['postprocess.sh'],
   install_dir : get_option('datadir') / 'megapixels/',
   install_mode: 'rwxr-xr-x')
+
+executable('list_devices', 'tools/list_devices.c', 'device.c', dependencies: [gtkdep])
+executable('test_camera', 'tools/test_camera.c', 'camera.c', 'device.c', dependencies: [gtkdep])

+ 137 - 0
pipeline.c

@@ -0,0 +1,137 @@
+#include "pipeline.h"
+
+#include <gtk/gtk.h>
+#include <glib-unix.h>
+#include <assert.h>
+
+struct _MPPipeline {
+    GMainContext *main_context;
+    GMainLoop *main_loop;
+    pthread_t thread;
+};
+
+static void *thread_main_loop(void *arg)
+{
+    MPPipeline *pipeline = arg;
+
+    g_main_loop_run(pipeline->main_loop);
+    return NULL;
+}
+
+MPPipeline *mp_pipeline_new()
+{
+    MPPipeline *pipeline = malloc(sizeof(MPPipeline));
+    pipeline->main_context = g_main_context_new();
+    pipeline->main_loop = g_main_loop_new(pipeline->main_context, false);
+    int res = pthread_create(
+        &pipeline->thread, NULL, thread_main_loop, pipeline);
+    assert(res == 0);
+
+    return pipeline;
+}
+
+struct invoke_args {
+    MPPipeline *pipeline;
+    void (*callback)(MPPipeline *, void *);
+};
+
+static bool invoke_impl(struct invoke_args *args)
+{
+    args->callback(args->pipeline, args + 1);
+    return false;
+}
+
+void mp_pipeline_invoke(MPPipeline *pipeline, MPPipelineCallback callback, void *data, size_t size)
+{
+    if (pthread_self() != pipeline->thread) {
+        struct invoke_args *args = malloc(sizeof(struct invoke_args) + size);
+        args->pipeline = pipeline;
+        args->callback = callback;
+
+        if (size > 0) {
+            memcpy(args + 1, data, size);
+        }
+
+        g_main_context_invoke_full(
+            pipeline->main_context,
+            G_PRIORITY_DEFAULT,
+            (GSourceFunc)invoke_impl,
+            args,
+            free);
+    } else {
+        callback(pipeline, data);
+    }
+}
+
+void mp_pipeline_free(MPPipeline *pipeline)
+{
+    g_main_loop_quit(pipeline->main_loop);
+
+    // Force the main thread loop to wake up, otherwise we might not exit
+    g_main_context_wakeup(pipeline->main_context);
+
+    void *r;
+    pthread_join(pipeline->thread, &r);
+    free(pipeline);
+}
+
+struct _MPPipelineCapture {
+    MPPipeline *pipeline;
+    MPCamera *camera;
+
+    void (*callback)(MPImage, void *);
+    void *user_data;
+    GSource *video_source;
+};
+
+static bool on_capture(int fd, GIOCondition condition, MPPipelineCapture *capture)
+{
+    mp_camera_capture_image(capture->camera, capture->callback, capture->user_data);
+    return true;
+}
+
+static void capture_start_impl(MPPipeline *pipeline, MPPipelineCapture **_capture)
+{
+    MPPipelineCapture *capture = *_capture;
+
+    mp_camera_start_capture(capture->camera);
+
+    // Start watching for new captures
+    int video_fd = mp_camera_get_video_fd(capture->camera);
+    capture->video_source = g_unix_fd_source_new(video_fd, G_IO_IN);
+    g_source_set_callback(
+        capture->video_source,
+        (GSourceFunc)on_capture,
+        capture,
+        NULL);
+    g_source_attach(capture->video_source, capture->pipeline->main_context);
+}
+
+MPPipelineCapture *mp_pipeline_capture_start(MPPipeline *pipeline, MPCamera *camera, void (*callback)(MPImage, void *), void *user_data)
+{
+    MPPipelineCapture *capture = malloc(sizeof(MPPipelineCapture));
+    capture->pipeline = pipeline;
+    capture->camera = camera;
+    capture->callback = callback;
+    capture->user_data = user_data;
+    capture->video_source = NULL;
+
+    mp_pipeline_invoke(pipeline, (MPPipelineCallback)capture_start_impl, &capture, sizeof(MPPipelineCapture *));
+
+    return capture;
+}
+
+static void capture_end_impl(MPPipeline *pipeline, MPPipelineCapture **_capture)
+{
+    MPPipelineCapture *capture = *_capture;
+
+    mp_camera_stop_capture(capture->camera);
+    g_source_destroy(capture->video_source);
+
+    free(capture);
+}
+
+void mp_pipeline_capture_end(MPPipelineCapture *capture)
+{
+    mp_pipeline_invoke(capture->pipeline, (MPPipelineCallback)capture_end_impl, &capture, sizeof(MPPipelineCapture *));
+}

+ 17 - 0
pipeline.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include "camera.h"
+#include "device.h"
+
+typedef struct _MPPipeline MPPipeline;
+
+typedef void (*MPPipelineCallback)(MPPipeline *, void *);
+
+MPPipeline *mp_pipeline_new();
+void mp_pipeline_invoke(MPPipeline *pipeline, MPPipelineCallback callback, void *data, size_t size);
+void mp_pipeline_free(MPPipeline *pipeline);
+
+typedef struct _MPPipelineCapture MPPipelineCapture;
+
+MPPipelineCapture *mp_pipeline_capture_start(MPPipeline *pipeline, MPCamera *camera, void (*capture)(MPImage, void *), void *data);
+void mp_pipeline_capture_end(MPPipelineCapture *capture);

+ 51 - 0
tools/list_devices.c

@@ -0,0 +1,51 @@
+#include "device.h"
+#include <linux/media.h>
+#include <stdio.h>
+
+int main(int argc, char *argv[]) {
+    MPDeviceList *list = mp_device_list_new();
+
+    while (list) {
+        MPDevice *device = mp_device_list_get(list);
+
+        const struct media_device_info *info = mp_device_get_info(device);
+        printf("%s (%s) %s\n", info->model, info->driver, info->serial);
+        printf("  Bus Info: %s\n", info->bus_info);
+        printf("  Media Version: %d\n", info->media_version);
+        printf("  HW Revision: %d\n", info->hw_revision);
+        printf("  Driver Version: %d\n", info->driver_version);
+
+
+        const struct media_v2_entity *entities = mp_device_get_entities(device);
+        size_t num = mp_device_get_num_entities(device);
+        printf("  Entities (%ld):\n", num);
+        for (int i = 0; i < num; ++i) {
+            printf("    %d %s (%d)\n", entities[i].id, entities[i].name, entities[i].function);
+        }
+
+        const struct media_v2_interface *interfaces = mp_device_get_interfaces(device);
+        num = mp_device_get_num_interfaces(device);
+        printf("  Interfaces (%ld):\n", num);
+        for (int i = 0; i < num; ++i) {
+            printf("    %d (%d - %d) devnode %d:%d\n", interfaces[i].id, interfaces[i].intf_type, interfaces[i].flags, interfaces[i].devnode.major, interfaces[i].devnode.minor);
+        }
+
+        const struct media_v2_pad *pads = mp_device_get_pads(device);
+        num = mp_device_get_num_pads(device);
+        printf("  Pads (%ld):\n", num);
+        for (int i = 0; i < num; ++i) {
+            printf("    %d for device:%d (%d)\n", pads[i].id, pads[i].entity_id, pads[i].flags);
+        }
+
+        const struct media_v2_link *links = mp_device_get_links(device);
+        num = mp_device_get_num_links(device);
+        printf("  Links (%ld):\n", num);
+        for (int i = 0; i < num; ++i) {
+            printf("    %d from:%d to:%d (%d)\n", links[i].id, links[i].source_id, links[i].sink_id, links[i].flags);
+        }
+
+        list = mp_device_list_next(list);
+    }
+
+    mp_device_list_free(list);
+}

+ 171 - 0
tools/test_camera.c

@@ -0,0 +1,171 @@
+#include "camera.h"
+#include "device.h"
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+double get_time()
+{
+    struct timeval t;
+    struct timezone tzp;
+    gettimeofday(&t, &tzp);
+    return t.tv_sec + t.tv_usec*1e-6;
+}
+
+void on_capture(MPImage image, void *user_data)
+{
+    size_t num_bytes = mp_pixel_format_bytes_per_pixel(image.pixel_format) * image.width * image.height;
+    uint8_t *data = malloc(num_bytes);
+    memcpy(data, image.data, num_bytes);
+
+    printf("      first byte: %d.", data[0]);
+
+    free(data);
+}
+
+int main(int argc, char *argv[])
+{
+    if (argc != 2 && argc != 3) {
+        printf("Usage: ./test_camera <media_device_name> [<sub_device_name>]\n");
+        return 1;
+    }
+
+    char *video_name = argv[1];
+    char *subdev_name = NULL;
+    if (argc == 3) {
+        subdev_name = argv[2];
+    }
+
+    double find_start = get_time();
+
+    // First find the device
+    MPDevice *device = mp_device_find(video_name);
+    if (!device) {
+        printf("Device not found\n");
+        return 1;
+    }
+
+    double find_end = get_time();
+
+    printf("Finding the device took %fms\n", (find_end - find_start) * 1000);
+
+    int video_fd;
+    uint32_t video_entity_id;
+    {
+        const struct media_v2_entity *entity = mp_device_find_entity(device, video_name);
+        if (!entity) {
+            printf("Unable to find video device interface\n");
+            return 1;
+        }
+
+        video_entity_id = entity->id;
+
+        const struct media_v2_interface *iface = mp_device_find_entity_interface(device, video_entity_id);
+
+        char buf[256];
+        if (!mp_find_device_path(iface->devnode, buf, 256)) {
+            printf("Unable to find video device path\n");
+            return 1;
+        }
+
+        video_fd = open(buf, O_RDWR);
+        if (video_fd == -1) {
+            printf("Unable to open video device\n");
+            return 1;
+        }
+    }
+
+    int subdev_fd = -1;
+    if (subdev_name)
+    {
+        const struct media_v2_entity *entity = mp_device_find_entity(device, subdev_name);
+        if (!entity) {
+            printf("Unable to find sub-device\n");
+            return 1;
+        }
+
+        const struct media_v2_pad *source_pad = mp_device_get_pad_from_entity(device, entity->id);
+        const struct media_v2_pad *sink_pad = mp_device_get_pad_from_entity(device, video_entity_id);
+
+        // Disable other links
+        const struct media_v2_entity *entities = mp_device_get_entities(device);
+        for (int i = 0; i < mp_device_get_num_entities(device); ++i) {
+            if (entities[i].id != video_entity_id && entities[i].id != entity->id) {
+                const struct media_v2_pad *pad = mp_device_get_pad_from_entity(device, entities[i].id);
+                mp_device_setup_link(device, pad->id, sink_pad->id, false);
+            }
+        }
+
+        // Then enable ours
+        mp_device_setup_link(device, source_pad->id, sink_pad->id, true);
+
+        const struct media_v2_interface *iface = mp_device_find_entity_interface(device, entity->id);
+
+        char buf[256];
+        if (!mp_find_device_path(iface->devnode, buf, 256)) {
+            printf("Unable to find sub-device path\n");
+            return 1;
+        }
+
+        subdev_fd = open(buf, O_RDWR);
+        if (subdev_fd == -1) {
+            printf("Unable to open sub-device\n");
+            return 1;
+        }
+    }
+
+    double open_end = get_time();
+
+    printf("Opening the device took %fms\n", (open_end - find_end) * 1000);
+
+    MPCamera *camera = mp_camera_new(video_fd, subdev_fd);
+
+    MPCameraModeList *modes = mp_camera_list_available_modes(camera);
+
+    double list_end = get_time();
+
+    printf("Available modes: (took %fms)\n", (list_end - open_end) * 1000);
+    for (MPCameraModeList *mode = modes; mode; mode = mp_camera_mode_list_next(mode)) {
+        MPCameraMode *m = mp_camera_mode_list_get(mode);
+        printf("  %dx%d interval:%d/%d fmt:%s\n", m->width, m->height, m->frame_interval.numerator, m->frame_interval.denominator, mp_pixel_format_to_str(m->pixel_format));
+
+        if (m->frame_interval.denominator < 15 || m->frame_interval.denominator > 30) {
+            printf("    Skipping…\n");
+            continue;
+        }
+
+        double start_capture = get_time();
+
+        mp_camera_set_mode(camera, m);
+        mp_camera_start_capture(camera);
+
+        double last = get_time();
+        printf("    Testing 10 captures, starting took %fms\n", (last - start_capture) * 1000);
+
+        for (int i = 0; i < 10; ++i) {
+            mp_camera_capture_image(camera, on_capture, NULL);
+
+            double now = get_time();
+            printf(" capture took %fms\n", (now - last) * 1000);
+            last = now;
+        }
+
+        mp_camera_stop_capture(camera);
+    }
+
+    double cleanup_start = get_time();
+
+    mp_camera_free(camera);
+
+    close(video_fd);
+    if (subdev_fd != -1)
+        close(subdev_fd);
+
+    mp_device_close(device);
+
+    double cleanup_end = get_time();
+
+    printf("Cleanup took %fms\n", (cleanup_end - cleanup_start) * 1000);
+}