From 2bb0cfbf8137e02cc32aae3b36f85ef7300e8936 Mon Sep 17 00:00:00 2001
From: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Date: Sat, 9 Jun 2018 14:29:41 +0300
Subject: Split UVC gadget into a library and test application

Split the project into a UVC gadget library and a test application. To
avoid rolling out a custom build system, switch to CMake.

Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
---
 lib/CMakeLists.txt  |  17 ++
 lib/configfs.c      | 846 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 lib/events.c        | 183 ++++++++++++
 lib/stream.c        | 223 ++++++++++++++
 lib/tools.h         |  65 ++++
 lib/uvc.c           | 395 ++++++++++++++++++++++++
 lib/uvc.h           |  36 +++
 lib/v4l2-source.c   | 180 +++++++++++
 lib/v4l2.c          | 833 +++++++++++++++++++++++++++++++++++++++++++++++++++
 lib/v4l2.h          | 339 +++++++++++++++++++++
 lib/video-buffers.c |  40 +++
 lib/video-buffers.h |  48 +++
 lib/video-source.c  |  62 ++++
 13 files changed, 3267 insertions(+)
 create mode 100644 lib/CMakeLists.txt
 create mode 100644 lib/configfs.c
 create mode 100644 lib/events.c
 create mode 100644 lib/stream.c
 create mode 100644 lib/tools.h
 create mode 100644 lib/uvc.c
 create mode 100644 lib/uvc.h
 create mode 100644 lib/v4l2-source.c
 create mode 100644 lib/v4l2.c
 create mode 100644 lib/v4l2.h
 create mode 100644 lib/video-buffers.c
 create mode 100644 lib/video-buffers.h
 create mode 100644 lib/video-source.c

(limited to 'lib')

diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
new file mode 100644
index 0000000..c798d3b
--- /dev/null
+++ b/lib/CMakeLists.txt
@@ -0,0 +1,17 @@
+file(GLOB SOURCES "*.c" "*.h")
+file(GLOB HEADERS "../include/uvcgadget/*.h")
+
+add_library(uvcgadget ${SOURCES})
+
+set_target_properties(uvcgadget PROPERTIES PUBLIC_HEADER "${HEADERS}")
+target_include_directories(uvcgadget
+	PUBLIC
+	$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include/uvcgadget>
+	$<INSTALL_INTERFACE:include>
+	PRIVATE
+	${KERNEL_INCLUDE_DIR})
+
+install(TARGETS uvcgadget
+	LIBRARY DESTINATION lib
+	ARCHIVE DESTINATION lib
+	PUBLIC_HEADER DESTINATION include/uvcgadget)
diff --git a/lib/configfs.c b/lib/configfs.c
new file mode 100644
index 0000000..1dfde95
--- /dev/null
+++ b/lib/configfs.c
@@ -0,0 +1,846 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * ConfigFS Gadget device handling
+ *
+ * Copyright (C) 2018 Kieran Bingham
+ *
+ * Contact: Kieran Bingham <kieran.bingham@ideasonboard.com>
+ */
+
+/* To provide basename and asprintf from the GNU library. */
+ #define _GNU_SOURCE
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <glob.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <linux/videodev2.h>
+
+#include "configfs.h"
+#include "tools.h"
+
+/* -----------------------------------------------------------------------------
+ * Path handling and support
+ */
+
+static char *path_join(const char *dirname, const char *name)
+{
+	char *path;
+
+	asprintf(&path, "%s/%s", dirname, name);
+
+	return path;
+}
+
+static char *path_glob_first_match(const char *g)
+{
+	glob_t globbuf;
+	char *match = NULL;
+
+	glob(g, 0, NULL, &globbuf);
+
+	if (globbuf.gl_pathc)
+		match = strdup(globbuf.gl_pathv[0]);
+
+	globfree(&globbuf);
+
+	return match;
+}
+
+/*
+ * Find and return the full path of the first directory entry that satisfies
+ * the match function.
+ */
+static char *dir_first_match(const char *dir, int(*match)(const struct dirent *))
+{
+	struct dirent **entries;
+	unsigned int i;
+	int n_entries;
+	char *path;
+
+	n_entries = scandir(dir, &entries, match, alphasort);
+	if (n_entries < 0)
+		return NULL;
+
+	if (n_entries == 0) {
+		free(entries);
+		return NULL;
+	}
+
+	path = path_join(dir, entries[0]->d_name);
+
+	for (i = 0; i < (unsigned int)n_entries; ++i)
+		free(entries[i]);
+
+	free(entries);
+
+	return path;
+}
+
+/* -----------------------------------------------------------------------------
+ * Attribute handling
+ */
+
+static int attribute_read(const char *path, const char *file, void *buf,
+			  unsigned int len)
+{
+	char *f;
+	int ret;
+	int fd;
+
+	f = path_join(path, file);
+	if (!f)
+		return -ENOMEM;
+
+	fd = open(f, O_RDONLY);
+	free(f);
+	if (fd == -1) {
+		printf("Failed to open attribute %s: %s\n", file,
+		       strerror(errno));
+		return -ENOENT;
+	}
+
+	ret = read(fd, buf, len);
+	close(fd);
+
+	if (ret < 0) {
+		printf("Failed to read attribute %s: %s\n", file,
+		       strerror(errno));
+		return -ENODATA;
+	}
+
+	return len;
+}
+
+static int attribute_read_uint(const char *path, const char *file,
+			       unsigned int *val)
+{
+	/* 4,294,967,295 */
+	char buf[11];
+	char *endptr;
+	int ret;
+
+	ret = attribute_read(path, file, buf, sizeof(buf) - 1);
+	if (ret < 0)
+		return ret;
+
+	buf[ret] = '\0';
+
+	errno = 0;
+
+	/* base 0: Autodetect hex, octal, decimal. */
+	*val = strtoul(buf, &endptr, 0);
+	if (errno)
+		return -errno;
+
+	if (endptr == buf)
+		return -ENODATA;
+
+	return 0;
+}
+
+static char *attribute_read_str(const char *path, const char *file)
+{
+	char buf[1024];
+	char *p;
+	int ret;
+
+	ret = attribute_read(path, file, buf, sizeof(buf) - 1);
+	if (ret < 0)
+		return NULL;
+
+	buf[ret] = '\0';
+
+	p = strrchr(buf, '\n');
+	if (p != buf)
+		*p = '\0';
+
+	return strdup(buf);
+}
+
+/* -----------------------------------------------------------------------------
+ * UDC parsing
+ */
+
+/*
+ * udc_find_video_device - Find the video device node for a UVC function
+ * @udc: The UDC name
+ * @function: The UVC function name
+ *
+ * This function finds the video device node corresponding to a UVC function as
+ * specified by a @function name and @udc name.
+ *
+ * The @function parameter specifies the name of the USB function, usually in
+ * the form "uvc.%u". If NULL the first function found will be used.
+ *
+ * The @udc parameter specifies the name of the UDC. If NULL any UDC that
+ * contains a function matching the @function name will be used.
+ *
+ * Return a pointer to a newly allocated string containing the video device node
+ * full path if the function is found. Otherwise return NULL. The returned
+ * pointer must be freed by the caller with a call to free().
+ */
+static char *udc_find_video_device(const char *udc, const char *function)
+{
+	char *vpath;
+	char *video = NULL;
+	glob_t globbuf;
+	unsigned int i;
+
+	asprintf(&vpath, "/sys/class/udc/%s/device/gadget/video4linux/video*",
+		 udc ? udc : "*");
+	if (!vpath)
+		return NULL;
+
+	glob(vpath, 0, NULL, &globbuf);
+	free(vpath);
+
+	for (i = 0; i < globbuf.gl_pathc; ++i) {
+		char *config;
+		bool match;
+
+		/* Match on the first if no search string. */
+		if (!function)
+			break;
+
+		config = attribute_read_str(globbuf.gl_pathv[i],
+					    "function_name");
+		match = strcmp(function, config) == 0;
+
+		free(config);
+
+		if (match)
+			break;
+	}
+
+	if (i < globbuf.gl_pathc) {
+		const char *v = basename(globbuf.gl_pathv[i]);
+
+		video = path_join("/dev", v);
+	}
+
+	globfree(&globbuf);
+
+	return video;
+}
+
+/* ------------------------------------------------------------------------
+ * GUIDs and formats
+ */
+
+#define UVC_GUID_FORMAT_MJPEG \
+	{ 'M',  'J',  'P',  'G', 0x00, 0x00, 0x10, 0x00, \
+	 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}
+#define UVC_GUID_FORMAT_YUY2 \
+	{ 'Y',  'U',  'Y',  '2', 0x00, 0x00, 0x10, 0x00, \
+	 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}
+
+struct uvc_function_format_info {
+	uint8_t guid[16];
+	uint32_t fcc;
+};
+
+static struct uvc_function_format_info uvc_formats[] = {
+	{
+		.guid		= UVC_GUID_FORMAT_YUY2,
+		.fcc		= V4L2_PIX_FMT_YUYV,
+	},
+	{
+		.guid		= UVC_GUID_FORMAT_MJPEG,
+		.fcc		= V4L2_PIX_FMT_MJPEG,
+	},
+};
+
+/* -----------------------------------------------------------------------------
+ * Legacy g_webcam support
+ */
+
+static const struct uvc_function_config g_webcam_config = {
+	.control = {
+		.intf = {
+			.bInterfaceNumber = 0,
+		},
+	},
+	.streaming = {
+		.intf = {
+			.bInterfaceNumber = 1,
+		},
+		.ep = {
+			.bInterval = 1,
+			.bMaxBurst = 0,
+			.wMaxPacketSize = 1024,
+		},
+		.num_formats = 2,
+		.formats = (struct uvc_function_config_format[]) {
+			{
+				.index = 1,
+				.guid = UVC_GUID_FORMAT_YUY2,
+				.fcc = V4L2_PIX_FMT_YUYV,
+				.num_frames = 2,
+				.frames = (struct uvc_function_config_frame[]) {
+					{
+						.index = 1,
+						.width = 640,
+						.height = 360,
+						.num_intervals = 3,
+						.intervals = (unsigned int[]) {
+							666666,
+							10000000,
+							50000000,
+						},
+					}, {
+						.index = 2,
+						.width = 1280,
+						.height = 720,
+						.num_intervals = 1,
+						.intervals = (unsigned int[]) {
+							50000000,
+						},
+					},
+				},
+			}, {
+				.index = 2,
+				.guid = UVC_GUID_FORMAT_MJPEG,
+				.fcc = V4L2_PIX_FMT_MJPEG,
+				.num_frames = 2,
+				.frames = (struct uvc_function_config_frame[]) {
+					{
+						.index = 1,
+						.width = 640,
+						.height = 360,
+						.num_intervals = 3,
+						.intervals = (unsigned int[]) {
+							666666,
+							10000000,
+							50000000,
+						},
+					}, {
+						.index = 2,
+						.width = 1280,
+						.height = 720,
+						.num_intervals = 1,
+						.intervals = (unsigned int[]) {
+							50000000,
+						},
+					},
+				},
+			},
+		},
+	},
+};
+
+static int parse_legacy_g_webcam(const char *udc,
+				 struct uvc_function_config *fc)
+{
+	void *memdup(const void *src, size_t size)
+	{
+		void *dst;
+
+		dst = malloc(size);
+		if (!dst)
+			return NULL;
+		memcpy(dst, src, size);
+		return dst;
+	}
+
+	unsigned int i, j;
+	size_t size;
+
+	*fc = g_webcam_config;
+
+	/*
+	 * We need to duplicate all sub-structures as the
+	 * configfs_free_uvc_function() function expects them to be dynamically
+	 * allocated.
+	 */
+	size = sizeof *fc->streaming.formats * fc->streaming.num_formats;
+	fc->streaming.formats = memdup(fc->streaming.formats, size);
+
+	for (i = 0; i < fc->streaming.num_formats; ++i) {
+		struct uvc_function_config_format *format =
+			&fc->streaming.formats[i];
+
+		size = sizeof *format->frames * format->num_frames;
+		format->frames = memdup(format->frames, size);
+
+		for (j = 0; j < format->num_frames; ++j) {
+			struct uvc_function_config_frame *frame =
+				&format->frames[j];
+
+			size = sizeof *frame->intervals * frame->num_intervals;
+			frame->intervals = memdup(frame->intervals, size);
+		}
+	}
+
+	fc->video = udc_find_video_device(udc, NULL);
+
+	return fc->video ? 0 : -ENODEV;
+}
+
+/* -----------------------------------------------------------------------------
+ * ConfigFS support
+ */
+
+/*
+ * configfs_find_uvc_function - Find the ConfigFS full path for a UVC function
+ * @function: The UVC function name
+ *
+ * Return a pointer to a newly allocated string containing the full ConfigFS
+ * path to the function if the function is found. Otherwise return NULL. The
+ * returned pointer must be freed by the caller with a call to free().
+ */
+static char *configfs_find_uvc_function(const char *function)
+{
+	const char *target = function ? function : "*";
+	const char *root;
+	char *func_path;
+	char *path;
+
+	/*
+	 * The function description can be provided as a path from the
+	 * usb_gadget root "g1/functions/uvc.0", or if there is no ambiguity
+	 * over the gadget name, a shortcut "uvc.0" can be provided.
+	 */
+	if (!strchr(target, '/'))
+		root = "/sys/kernel/config/usb_gadget/*/functions";
+	else
+		root = "/sys/kernel/config/usb_gadget";
+
+	path = path_join(root, target);
+
+	func_path = path_glob_first_match(path);
+	free(path);
+
+	return func_path;
+}
+
+/*
+ * configfs_free_uvc_function - Free a uvc_function_config object
+ * @fc: The uvc_function_config to be freed
+ *
+ * Free the given @fc function previously allocated by a call to
+ * configfs_parse_uvc_function().
+ */
+void configfs_free_uvc_function(struct uvc_function_config *fc)
+{
+	unsigned int i, j;
+
+	free(fc->udc);
+	free(fc->video);
+
+	for (i = 0; i < fc->streaming.num_formats; ++i) {
+		struct uvc_function_config_format *format =
+			&fc->streaming.formats[i];
+
+		for (j = 0; j < format->num_frames; ++j) {
+			struct uvc_function_config_frame *frame =
+				&format->frames[j];
+
+			free(frame->intervals);
+		}
+
+		free(format->frames);
+	}
+
+	free(fc->streaming.formats);
+	free(fc);
+}
+
+#define configfs_parse_child(parent, child, cfg, parse)			\
+({									\
+	char *__path;							\
+	int __ret;							\
+									\
+	__path = path_join((parent), (child));				\
+	if (__path) {							\
+		__ret = parse(__path, (cfg));				\
+		free(__path);						\
+	} else {							\
+		__ret = -ENOMEM;					\
+	}								\
+									\
+	__ret;								\
+})
+
+static int configfs_parse_interface(const char *path,
+				    struct uvc_function_config_interface *cfg)
+{
+	int ret;
+
+	ret = attribute_read_uint(path, "bInterfaceNumber",
+				  &cfg->bInterfaceNumber);
+
+	return ret;
+}
+
+static int configfs_parse_control(const char *path,
+				  struct uvc_function_config_control *cfg)
+{
+	int ret;
+
+	ret = configfs_parse_interface(path, &cfg->intf);
+
+	return ret;
+}
+
+static int configfs_parse_streaming_frame(const char *path,
+			struct uvc_function_config_frame *frame)
+{
+	char *intervals;
+	char *p;
+	int ret = 0;
+
+	ret = ret ? : attribute_read_uint(path, "bFrameIndex", &frame->index);
+	ret = ret ? : attribute_read_uint(path, "wWidth", &frame->width);
+	ret = ret ? : attribute_read_uint(path, "wHeight", &frame->height);
+	if (ret)
+		return ret;
+
+	intervals = attribute_read_str(path, "dwFrameInterval");
+	if (!intervals)
+		return -EINVAL;
+
+	for (p = intervals; *p; ) {
+		unsigned int interval;
+		unsigned int *mem;
+		char *endp;
+		size_t size;
+
+		interval = strtoul(p, &endp, 10);
+		if (*endp != '\0' && *endp != '\n') {
+			ret = -EINVAL;
+			break;
+		}
+
+		p = *endp ? endp + 1 : endp;
+
+		size = sizeof *frame->intervals * (frame->num_intervals + 1);
+		mem = realloc(frame->intervals, size);
+		if (!mem) {
+			ret = -ENOMEM;
+			break;
+		}
+
+		frame->intervals = mem;
+		frame->intervals[frame->num_intervals++] = interval;
+	}
+
+	free(intervals);
+
+	return ret;
+}
+
+static int configfs_parse_streaming_format(const char *path,
+			struct uvc_function_config_format *format)
+{
+	int frame_filter(const struct dirent *ent)
+	{
+		/* Accept all directories but "." and "..". */
+		if (ent->d_type != DT_DIR)
+			return 0;
+		if (!strcmp(ent->d_name, "."))
+			return 0;
+		if (!strcmp(ent->d_name, ".."))
+			return 0;
+		return 1;
+	}
+
+	int frame_compare(const void *a, const void *b)
+	{
+		const struct uvc_function_config_frame *fa = a;
+		const struct uvc_function_config_frame *fb = b;
+
+		if (fa->index < fb->index)
+			return -1;
+		else if (fa->index == fb->index)
+			return 0;
+		else
+			return 1;
+	}
+
+	struct dirent **entries;
+	unsigned int i;
+	int n_entries;
+	int ret;
+
+	ret = attribute_read_uint(path, "bFormatIndex", &format->index);
+	if (ret < 0)
+		return ret;
+
+	ret = attribute_read(path, "guidFormat", format->guid,
+			     sizeof(format->guid));
+	if (ret < 0)
+		return ret;
+	if (ret != 16)
+		return -EINVAL;
+
+	for (i = 0; i < ARRAY_SIZE(uvc_formats); ++i) {
+		if (!memcmp(uvc_formats[i].guid, format->guid, 16)) {
+			format->fcc = uvc_formats[i].fcc;
+			break;
+		}
+	}
+
+	/* Find all entries corresponding to a frame and parse them. */
+	n_entries = scandir(path, &entries, frame_filter, alphasort);
+	if (n_entries < 0)
+		return -errno;
+
+	if (n_entries == 0) {
+		free(entries);
+		return -EINVAL;
+	}
+
+	format->num_frames = n_entries;
+	format->frames = calloc(sizeof *format->frames, format->num_frames);
+	if (!format->frames)
+		return -ENOMEM;
+
+	for (i = 0; i < (unsigned int)n_entries; ++i) {
+		char *frame;
+
+		frame = path_join(path, entries[i]->d_name);
+		if (!frame) {
+			ret = -ENOMEM;
+			goto done;
+		}
+
+		ret = configfs_parse_streaming_frame(frame, &format->frames[i]);
+		free(frame);
+		if (ret < 0)
+			goto done;
+	}
+
+	/* Sort the frames by index. */
+	qsort(format->frames, format->num_frames, sizeof *format->frames,
+	      frame_compare);
+
+done:
+	for (i = 0; i < (unsigned int)n_entries; ++i)
+		free(entries[i]);
+	free(entries);
+
+	return ret;
+}
+
+static int configfs_parse_streaming_header(const char *path,
+			struct uvc_function_config_streaming *cfg)
+{
+	int format_filter(const struct dirent *ent)
+	{
+		char *path;
+		bool valid;
+
+		/*
+		 * Accept all links that point to a directory containing a
+		 * "bFormatIndex" file.
+		 */
+		if (ent->d_type != DT_LNK)
+			return 0;
+
+		path = path_join(ent->d_name, "bFormatIndex");
+		if (!path)
+			return 0;
+
+		valid = access(path, R_OK);
+		free(path);
+		return valid;
+	}
+
+	int format_compare(const void *a, const void *b)
+	{
+		const struct uvc_function_config_format *fa = a;
+		const struct uvc_function_config_format *fb = b;
+
+		if (fa->index < fb->index)
+			return -1;
+		else if (fa->index == fb->index)
+			return 0;
+		else
+			return 1;
+	}
+
+	struct dirent **entries;
+	unsigned int i;
+	int n_entries;
+	int ret;
+
+	/* Find all entries corresponding to a format and parse them. */
+	n_entries = scandir(path, &entries, format_filter, alphasort);
+	if (n_entries < 0)
+		return -errno;
+
+	if (n_entries == 0) {
+		free(entries);
+		return -EINVAL;
+	}
+
+	cfg->num_formats = n_entries;
+	cfg->formats = calloc(sizeof *cfg->formats, cfg->num_formats);
+	if (!cfg->formats)
+		return -ENOMEM;
+
+	for (i = 0; i < (unsigned int)n_entries; ++i) {
+		char *format;
+
+		format = path_join(path, entries[i]->d_name);
+		if (!format) {
+			ret = -ENOMEM;
+			goto done;
+		}
+
+		ret = configfs_parse_streaming_format(format, &cfg->formats[i]);
+		free(format);
+		if (ret < 0)
+			goto done;
+	}
+
+	/* Sort the formats by index. */
+	qsort(cfg->formats, cfg->num_formats, sizeof *cfg->formats,
+	      format_compare);
+
+done:
+	for (i = 0; i < (unsigned int)n_entries; ++i)
+		free(entries[i]);
+	free(entries);
+
+	return ret;
+}
+
+static int configfs_parse_streaming(const char *path,
+				    struct uvc_function_config_streaming *cfg)
+{
+	int link_filter(const struct dirent *ent)
+	{
+		/* Accept all links. */
+		return ent->d_type == DT_LNK;
+	}
+
+	char *header;
+	char *class;
+	int ret;
+
+	ret = configfs_parse_interface(path, &cfg->intf);
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * Handle the high-speed class descriptors only for now. Find the first
+	 * link to the class descriptors.
+	 */
+	class = path_join(path, "class/hs");
+	if (!class)
+		return -ENOMEM;
+
+	header = dir_first_match(class, link_filter);
+	free(class);
+	if (!header)
+		return -EINVAL;
+
+	ret = configfs_parse_streaming_header(header, cfg);
+	free(header);
+	return ret;
+}
+
+static int configfs_parse_uvc(const char *fpath,
+			      struct uvc_function_config *fc)
+{
+	int ret = 0;
+
+	ret = ret ? : configfs_parse_child(fpath, "control", &fc->control,
+					   configfs_parse_control);
+	ret = ret ? : configfs_parse_child(fpath, "streaming", &fc->streaming,
+					   configfs_parse_streaming);
+
+	/*
+	 * These parameters should be part of the streaming interface in
+	 * ConfigFS, but for legacy reasons they are located directly in the
+	 * function directory.
+	 */
+	ret = ret ? : attribute_read_uint(fpath, "streaming_interval",
+					  &fc->streaming.ep.bInterval);
+	ret = ret ? : attribute_read_uint(fpath, "streaming_maxburst",
+					  &fc->streaming.ep.bMaxBurst);
+	ret = ret ? : attribute_read_uint(fpath, "streaming_maxpacket",
+					  &fc->streaming.ep.wMaxPacketSize);
+
+	return ret;
+}
+
+/*
+ * configfs_parse_uvc_function - Parse a UVC function configuration in ConfigFS
+ * @function: The function name
+ *
+ * This function locates and parse the configuration of a UVC function in
+ * ConfigFS as specified by the @function name argument. The function name can
+ * be fully qualified with a gadget name (e.g. "g%u/functions/uvc.%u"), or as a
+ * shortcut can be an unqualified function name (e.g. "uvc.%u"). When the
+ * function name is unqualified, the first function matching the name in any
+ * UDC will be returned.
+ *
+ * Return a pointer to a newly allocated UVC function configuration structure
+ * that contains configuration parameters for the function, if the function is
+ * found. Otherwise return NULL. The returned pointer must be freed by the
+ * caller with a call to free().
+ */
+struct uvc_function_config *configfs_parse_uvc_function(const char *function)
+{
+	struct uvc_function_config *fc;
+	char *fpath;
+	int ret = 0;
+
+	fc = malloc(sizeof *fc);
+	if (fc == NULL)
+		return NULL;
+
+	memset(fc, 0, sizeof *fc);
+
+	/* Find the function in ConfigFS. */
+	fpath = configfs_find_uvc_function(function);
+	if (!fpath) {
+		/*
+		 * If the function can't be found attempt legacy parsing to
+		 * support the g_webcam gadget. The function parameter contains
+		 * a UDC name in that case.
+		 */
+		ret = parse_legacy_g_webcam(function, fc);
+		if (ret) {
+			configfs_free_uvc_function(fc);
+			fc = NULL;
+		}
+
+		return fc;
+	}
+
+	/*
+	 * Parse the function configuration. Remove the gadget name qualifier
+	 * from the function name, if any.
+	 */
+	if (function)
+		function = basename(function);
+
+	fc->udc = attribute_read_str(fpath, "../../UDC");
+	fc->video = udc_find_video_device(fc->udc, function);
+	if (!fc->video) {
+		ret = -ENODEV;
+		goto done;
+	}
+
+	ret = configfs_parse_uvc(fpath, fc);
+
+done:
+	if (ret) {
+		configfs_free_uvc_function(fc);
+		fc = NULL;
+	}
+
+	free(fpath);
+
+	return fc;
+}
diff --git a/lib/events.c b/lib/events.c
new file mode 100644
index 0000000..6840c19
--- /dev/null
+++ b/lib/events.c
@@ -0,0 +1,183 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Generic Event Handling
+ *
+ * Copyright (C) 2018 Laurent Pinchart
+ *
+ * This file comes from the omap3-isp-live project
+ * (git://git.ideasonboard.org/omap3-isp-live.git)
+ *
+ * Copyright (C) 2010-2011 Ideas on board SPRL
+ *
+ * Contact: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+ */
+
+#define _DEFAULT_SOURCE
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sys/select.h>
+
+#include "events.h"
+#include "list.h"
+#include "tools.h"
+
+#define SELECT_TIMEOUT		2000		/* in milliseconds */
+
+struct event_fd {
+	struct list_entry list;
+
+	int fd;
+	enum event_type type;
+	void (*callback)(void *priv);
+	void *priv;
+};
+
+void events_watch_fd(struct events *events, int fd, enum event_type type,
+		     void(*callback)(void *), void *priv)
+{
+	struct event_fd *event;
+
+	event = malloc(sizeof *event);
+	if (event == NULL)
+		return;
+
+	event->fd = fd;
+	event->type = type;
+	event->callback = callback;
+	event->priv = priv;
+
+	switch (event->type) {
+	case EVENT_READ:
+		FD_SET(fd, &events->rfds);
+		break;
+	case EVENT_WRITE:
+		FD_SET(fd, &events->wfds);
+		break;
+	case EVENT_EXCEPTION:
+		FD_SET(fd, &events->efds);
+		break;
+	}
+
+	events->maxfd = max(events->maxfd, fd);
+
+	list_append(&event->list, &events->events);
+}
+
+void events_unwatch_fd(struct events *events, int fd, enum event_type type)
+{
+	struct event_fd *event = NULL;
+	struct event_fd *entry;
+	int maxfd = 0;
+
+	list_for_each_entry(entry, &events->events, list) {
+		if (entry->fd == fd && entry->type == type)
+			event = entry;
+		else
+			maxfd = max(maxfd, entry->fd);
+	}
+
+	if (event == NULL)
+		return;
+
+	switch (event->type) {
+	case EVENT_READ:
+		FD_CLR(fd, &events->rfds);
+		break;
+	case EVENT_WRITE:
+		FD_CLR(fd, &events->wfds);
+		break;
+	case EVENT_EXCEPTION:
+		FD_CLR(fd, &events->efds);
+		break;
+	}
+
+	events->maxfd = maxfd;
+
+	list_remove(&event->list);
+	free(event);
+}
+
+static void events_dispatch(struct events *events, const fd_set *rfds,
+			    const fd_set *wfds, const fd_set *efds)
+{
+	struct event_fd *event;
+	struct event_fd *next;
+
+	list_for_each_entry_safe(event, next, &events->events, list) {
+		if (event->type == EVENT_READ &&
+		    FD_ISSET(event->fd, rfds))
+			event->callback(event->priv);
+		else if (event->type == EVENT_WRITE &&
+			 FD_ISSET(event->fd, wfds))
+			event->callback(event->priv);
+		else if (event->type == EVENT_EXCEPTION &&
+			 FD_ISSET(event->fd, efds))
+			event->callback(event->priv);
+
+		/* If the callback stopped events processing, we're done. */
+		if (events->done)
+			break;
+	}
+}
+
+bool events_loop(struct events *events)
+{
+	events->done = false;
+
+	while (!events->done) {
+		fd_set rfds;
+		fd_set wfds;
+		fd_set efds;
+		int ret;
+
+		rfds = events->rfds;
+		wfds = events->wfds;
+		efds = events->efds;
+
+		ret = select(events->maxfd + 1, &rfds, &wfds, &efds, NULL);
+		if (ret < 0) {
+			/* EINTR means that a signal has been received, continue
+			 * to the next iteration in that case.
+			 */
+			if (errno == EINTR)
+				continue;
+
+			printf("error: select failed with %d\n", errno);
+			break;
+		}
+
+		events_dispatch(events, &rfds, &wfds, &efds);
+	}
+
+	return !events->done;
+}
+
+void events_stop(struct events *events)
+{
+	events->done = true;
+}
+
+void events_init(struct events *events)
+{
+	memset(events, 0, sizeof *events);
+
+	FD_ZERO(&events->rfds);
+	FD_ZERO(&events->wfds);
+	FD_ZERO(&events->efds);
+	events->maxfd = 0;
+	list_init(&events->events);
+}
+
+void events_cleanup(struct events *events)
+{
+	while (!list_empty(&events->events)) {
+		struct event_fd *event;
+
+		event = list_first_entry(&events->events, typeof(*event), list);
+		list_remove(&event->list);
+		free(event);
+	}
+}
diff --git a/lib/stream.c b/lib/stream.c
new file mode 100644
index 0000000..57745aa
--- /dev/null
+++ b/lib/stream.c
@@ -0,0 +1,223 @@
+/*
+ * UVC stream handling
+ *
+ * Copyright (C) 2010 Ideas on board SPRL <laurent.pinchart@ideasonboard.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "events.h"
+#include "stream.h"
+#include "uvc.h"
+#include "v4l2.h"
+#include "video-buffers.h"
+#include "video-source.h"
+
+/*
+ * struct uvc_stream - Representation of a UVC stream
+ * @src: video source
+ * @uvc: UVC V4L2 output device
+ * @events: struct events containing event information
+ */
+struct uvc_stream
+{
+	struct video_source *src;
+	struct uvc_device *uvc;
+
+	struct events *events;
+};
+
+/* ---------------------------------------------------------------------------
+ * Video streaming
+ */
+
+static void uvc_stream_source_process(void *d, struct video_source *src,
+				      struct video_buffer *buffer)
+{
+	struct uvc_stream *stream = d;
+	struct v4l2_device *sink = uvc_v4l2_device(stream->uvc);
+
+	v4l2_queue_buffer(sink, buffer);
+}
+
+static void uvc_stream_uvc_process(void *d)
+{
+	struct uvc_stream *stream = d;
+	struct v4l2_device *sink = uvc_v4l2_device(stream->uvc);
+	struct video_buffer buf;
+	int ret;
+
+	ret = v4l2_dequeue_buffer(sink, &buf);
+	if (ret < 0)
+		return;
+
+	video_source_queue_buffer(stream->src, &buf);
+}
+
+static int uvc_stream_start(struct uvc_stream *stream)
+{
+	struct v4l2_device *sink = uvc_v4l2_device(stream->uvc);
+	struct video_buffer_set *buffers = NULL;
+	int ret;
+
+	printf("Starting video stream.\n");
+
+	/* Allocate and export the buffers on the source. */
+	ret = video_source_alloc_buffers(stream->src, 4);
+	if (ret < 0) {
+		printf("Failed to allocate source buffers: %s (%d)\n",
+		       strerror(-ret), -ret);
+		return ret;
+	}
+
+	ret = video_source_export_buffers(stream->src, &buffers);
+	if (ret < 0) {
+		printf("Failed to export buffers on source: %s (%d)\n",
+		       strerror(-ret), -ret);
+		goto error_free_source;
+	}
+
+	/* Allocate and import the buffers on the sink. */
+	ret = v4l2_alloc_buffers(sink, V4L2_MEMORY_DMABUF, buffers->nbufs);
+	if (ret < 0) {
+		printf("Failed to allocate sink buffers: %s (%d)\n",
+		       strerror(-ret), -ret);
+		goto error_free_source;
+	}
+
+	ret = v4l2_import_buffers(sink, buffers);
+	if (ret < 0) {
+		printf("Failed to import buffers on sink: %s (%d)\n",
+		       strerror(-ret), -ret);
+		goto error_free_sink;
+	}
+
+	/* Start the source and sink. */
+	video_source_stream_on(stream->src);
+	v4l2_stream_on(sink);
+
+	events_watch_fd(stream->events, sink->fd, EVENT_WRITE,
+			uvc_stream_uvc_process, stream);
+
+	return 0;
+
+error_free_sink:
+	v4l2_free_buffers(sink);
+error_free_source:
+	video_source_free_buffers(stream->src);
+	if (buffers)
+		video_buffer_set_delete(buffers);
+	return ret;
+}
+
+static int uvc_stream_stop(struct uvc_stream *stream)
+{
+	struct v4l2_device *sink = uvc_v4l2_device(stream->uvc);
+
+	printf("Stopping video stream.\n");
+
+	events_unwatch_fd(stream->events, sink->fd, EVENT_WRITE);
+
+	v4l2_stream_off(sink);
+	video_source_stream_off(stream->src);
+
+	v4l2_free_buffers(sink);
+	video_source_free_buffers(stream->src);
+
+	return 0;
+}
+
+void uvc_stream_enable(struct uvc_stream *stream, int enable)
+{
+	if (enable)
+		uvc_stream_start(stream);
+	else
+		uvc_stream_stop(stream);
+}
+
+int uvc_stream_set_format(struct uvc_stream *stream,
+			  const struct v4l2_pix_format *format)
+{
+	struct v4l2_pix_format fmt = *format;
+	int ret;
+
+	printf("Setting format to 0x%08x %ux%u\n",
+		format->pixelformat, format->width, format->height);
+
+	ret = uvc_set_format(stream->uvc, &fmt);
+	if (ret < 0)
+		return ret;
+
+	return video_source_set_format(stream->src, &fmt);
+}
+
+/* ---------------------------------------------------------------------------
+ * Stream handling
+ */
+
+struct uvc_stream *uvc_stream_new(const char *uvc_device)
+{
+	struct uvc_stream *stream;
+
+	stream = malloc(sizeof(*stream));
+	if (stream == NULL)
+		return NULL;
+
+	memset(stream, 0, sizeof(*stream));
+
+	stream->uvc = uvc_open(uvc_device, stream);
+	if (stream->uvc == NULL)
+		goto error;
+
+	return stream;
+
+error:
+	free(stream);
+	return NULL;
+}
+
+void uvc_stream_delete(struct uvc_stream *stream)
+{
+	if (stream == NULL)
+		return;
+
+	uvc_close(stream->uvc);
+
+	free(stream);
+}
+
+void uvc_stream_init_uvc(struct uvc_stream *stream,
+			 struct uvc_function_config *fc)
+{
+	uvc_set_config(stream->uvc, fc);
+	uvc_events_init(stream->uvc, stream->events);
+}
+
+void uvc_stream_set_event_handler(struct uvc_stream *stream,
+				  struct events *events)
+{
+	stream->events = events;
+}
+
+void uvc_stream_set_video_source(struct uvc_stream *stream,
+				 struct video_source *src)
+{
+	stream->src = src;
+
+	video_source_set_buffer_handler(src, uvc_stream_source_process, stream);
+}
diff --git a/lib/tools.h b/lib/tools.h
new file mode 100644
index 0000000..ff2f908
--- /dev/null
+++ b/lib/tools.h
@@ -0,0 +1,65 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Miscellaneous Tools
+ *
+ * Copyright (C) 2018 Laurent Pinchart
+ *
+ * This file comes from the omap3-isp-live project
+ * (git://git.ideasonboard.org/omap3-isp-live.git)
+ *
+ * Copyright (C) 2010-2011 Ideas on board SPRL
+ *
+ * Contact: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+ */
+
+#ifndef __TOOLS_H__
+#define __TOOLS_H__
+
+#define ARRAY_SIZE(array)	(sizeof(array) / sizeof((array)[0]))
+
+#define min(a, b) ({				\
+	typeof(a) __a = (a);			\
+	typeof(b) __b = (b);			\
+	__a < __b ? __a : __b;			\
+})
+
+#define min_t(type, a, b) ({			\
+	type __a = (a);				\
+	type __b = (b);				\
+	__a < __b ? __a : __b;			\
+})
+
+#define max(a, b) ({				\
+	typeof(a) __a = (a);			\
+	typeof(b) __b = (b);			\
+	__a > __b ? __a : __b;			\
+})
+
+#define max_t(type, a, b) ({			\
+	type __a = (a);				\
+	type __b = (b);				\
+	__a > __b ? __a : __b;			\
+})
+
+#define clamp(val, min, max) ({			\
+	typeof(val) __val = (val);		\
+	typeof(min) __min = (min);		\
+	typeof(max) __max = (max);		\
+	__val = __val < __min ? __min : __val;	\
+	__val > __max ? __max : __val;		\
+})
+
+#define clamp_t(type, val, min, max) ({		\
+	type __val = (val);			\
+	type __min = (min);			\
+	type __max = (max);			\
+	__val = __val < __min ? __min : __val;	\
+	__val > __max ? __max : __val;		\
+})
+
+#define div_round_up(num, denom)	(((num) + (denom) - 1) / (denom))
+
+#define container_of(ptr, type, member) \
+	(type *)((char *)(ptr) - offsetof(type, member))
+
+#endif /* __TOOLS_H__ */
diff --git a/lib/uvc.c b/lib/uvc.c
new file mode 100644
index 0000000..81d1760
--- /dev/null
+++ b/lib/uvc.c
@@ -0,0 +1,395 @@
+/*
+ * UVC protocol handling
+ *
+ * Copyright (C) 2010 Ideas on board SPRL <laurent.pinchart@ideasonboard.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ */
+
+#include <errno.h>
+#include <linux/usb/ch9.h>
+#include <linux/usb/g_uvc.h>
+#include <linux/usb/video.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+
+#include "configfs.h"
+#include "events.h"
+#include "stream.h"
+#include "tools.h"
+#include "uvc.h"
+#include "v4l2.h"
+
+struct uvc_device
+{
+	struct v4l2_device *vdev;
+
+	struct uvc_stream *stream;
+	struct uvc_function_config *fc;
+
+	struct uvc_streaming_control probe;
+	struct uvc_streaming_control commit;
+
+	int control;
+
+	unsigned int fcc;
+	unsigned int width;
+	unsigned int height;
+	unsigned int maxsize;
+};
+
+struct uvc_device *uvc_open(const char *devname, struct uvc_stream *stream)
+{
+	struct uvc_device *dev;
+
+	dev = malloc(sizeof *dev);
+	if (dev == NULL)
+		return NULL;
+
+	memset(dev, 0, sizeof *dev);
+	dev->stream = stream;
+
+	dev->vdev = v4l2_open(devname);
+	if (dev->vdev == NULL) {
+		free(dev);
+		return NULL;
+	}
+
+	return dev;
+}
+
+void uvc_close(struct uvc_device *dev)
+{
+	v4l2_close(dev->vdev);
+	dev->vdev = NULL;
+
+	free(dev);
+}
+
+/* ---------------------------------------------------------------------------
+ * Request processing
+ */
+
+static void
+uvc_fill_streaming_control(struct uvc_device *dev,
+			   struct uvc_streaming_control *ctrl,
+			   int iformat, int iframe, unsigned int ival)
+{
+	const struct uvc_function_config_format *format;
+	const struct uvc_function_config_frame *frame;
+	unsigned int i;
+
+	/*
+	 * Restrict the iformat, iframe and ival to valid values. Negative
+	 * values for iformat or iframe will result in the maximum valid value
+	 * being selected.
+	 */
+        iformat = clamp((unsigned int)iformat, 1U,
+                        dev->fc->streaming.num_formats);
+	format = &dev->fc->streaming.formats[iformat-1];
+
+	iframe = clamp((unsigned int)iframe, 1U, format->num_frames);
+	frame = &format->frames[iframe-1];
+
+	for (i = 0; i < frame->num_intervals; ++i) {
+		if (ival <= frame->intervals[i]) {
+			ival = frame->intervals[i];
+			break;
+		}
+	}
+
+	if (i == frame->num_intervals)
+		ival = frame->intervals[frame->num_intervals-1];
+
+	memset(ctrl, 0, sizeof *ctrl);
+
+	ctrl->bmHint = 1;
+	ctrl->bFormatIndex = iformat;
+	ctrl->bFrameIndex = iframe ;
+	ctrl->dwFrameInterval = ival;
+
+	switch (format->fcc) {
+	case V4L2_PIX_FMT_YUYV:
+		ctrl->dwMaxVideoFrameSize = frame->width * frame->height * 2;
+		break;
+	case V4L2_PIX_FMT_MJPEG:
+		ctrl->dwMaxVideoFrameSize = dev->maxsize;
+		break;
+	}
+
+	ctrl->dwMaxPayloadTransferSize = dev->fc->streaming.ep.wMaxPacketSize;
+	ctrl->bmFramingInfo = 3;
+	ctrl->bPreferedVersion = 1;
+	ctrl->bMaxVersion = 1;
+}
+
+static void
+uvc_events_process_standard(struct uvc_device *dev,
+			    const struct usb_ctrlrequest *ctrl,
+			    struct uvc_request_data *resp)
+{
+	printf("standard request\n");
+	(void)dev;
+	(void)ctrl;
+	(void)resp;
+}
+
+static void
+uvc_events_process_control(struct uvc_device *dev, uint8_t req, uint8_t cs,
+			   struct uvc_request_data *resp)
+{
+	printf("control request (req %02x cs %02x)\n", req, cs);
+	(void)dev;
+	(void)resp;
+}
+
+static void
+uvc_events_process_streaming(struct uvc_device *dev, uint8_t req, uint8_t cs,
+			     struct uvc_request_data *resp)
+{
+	struct uvc_streaming_control *ctrl;
+
+	printf("streaming request (req %02x cs %02x)\n", req, cs);
+
+	if (cs != UVC_VS_PROBE_CONTROL && cs != UVC_VS_COMMIT_CONTROL)
+		return;
+
+	ctrl = (struct uvc_streaming_control *)&resp->data;
+	resp->length = sizeof *ctrl;
+
+	switch (req) {
+	case UVC_SET_CUR:
+		dev->control = cs;
+		resp->length = 34;
+		break;
+
+	case UVC_GET_CUR:
+		if (cs == UVC_VS_PROBE_CONTROL)
+			memcpy(ctrl, &dev->probe, sizeof *ctrl);
+		else
+			memcpy(ctrl, &dev->commit, sizeof *ctrl);
+		break;
+
+	case UVC_GET_MIN:
+	case UVC_GET_MAX:
+	case UVC_GET_DEF:
+		uvc_fill_streaming_control(dev, ctrl, req == UVC_GET_MAX ? -1 : 1,
+					   req == UVC_GET_MAX ? -1 : 1, 0);
+		break;
+
+	case UVC_GET_RES:
+		memset(ctrl, 0, sizeof *ctrl);
+		break;
+
+	case UVC_GET_LEN:
+		resp->data[0] = 0x00;
+		resp->data[1] = 0x22;
+		resp->length = 2;
+		break;
+
+	case UVC_GET_INFO:
+		resp->data[0] = 0x03;
+		resp->length = 1;
+		break;
+	}
+}
+
+static void
+uvc_events_process_class(struct uvc_device *dev,
+			 const struct usb_ctrlrequest *ctrl,
+			 struct uvc_request_data *resp)
+{
+	unsigned int interface = ctrl->wIndex & 0xff;
+
+	if ((ctrl->bRequestType & USB_RECIP_MASK) != USB_RECIP_INTERFACE)
+		return;
+
+	if (interface == dev->fc->control.intf.bInterfaceNumber)
+		uvc_events_process_control(dev, ctrl->bRequest, ctrl->wValue >> 8, resp);
+	else if (interface == dev->fc->streaming.intf.bInterfaceNumber)
+		uvc_events_process_streaming(dev, ctrl->bRequest, ctrl->wValue >> 8, resp);
+}
+
+static void
+uvc_events_process_setup(struct uvc_device *dev,
+			 const struct usb_ctrlrequest *ctrl,
+			 struct uvc_request_data *resp)
+{
+	dev->control = 0;
+
+	printf("bRequestType %02x bRequest %02x wValue %04x wIndex %04x "
+		"wLength %04x\n", ctrl->bRequestType, ctrl->bRequest,
+		ctrl->wValue, ctrl->wIndex, ctrl->wLength);
+
+	switch (ctrl->bRequestType & USB_TYPE_MASK) {
+	case USB_TYPE_STANDARD:
+		uvc_events_process_standard(dev, ctrl, resp);
+		break;
+
+	case USB_TYPE_CLASS:
+		uvc_events_process_class(dev, ctrl, resp);
+		break;
+
+	default:
+		break;
+	}
+}
+
+static void
+uvc_events_process_data(struct uvc_device *dev,
+			const struct uvc_request_data *data)
+{
+	const struct uvc_streaming_control *ctrl =
+		(const struct uvc_streaming_control *)&data->data;
+	struct uvc_streaming_control *target;
+
+	switch (dev->control) {
+	case UVC_VS_PROBE_CONTROL:
+		printf("setting probe control, length = %d\n", data->length);
+		target = &dev->probe;
+		break;
+
+	case UVC_VS_COMMIT_CONTROL:
+		printf("setting commit control, length = %d\n", data->length);
+		target = &dev->commit;
+		break;
+
+	default:
+		printf("setting unknown control, length = %d\n", data->length);
+		return;
+	}
+
+	uvc_fill_streaming_control(dev, target, ctrl->bFormatIndex,
+				   ctrl->bFrameIndex, ctrl->dwFrameInterval);
+
+	if (dev->control == UVC_VS_COMMIT_CONTROL) {
+		const struct uvc_function_config_format *format;
+		const struct uvc_function_config_frame *frame;
+		struct v4l2_pix_format pixfmt;
+
+		format = &dev->fc->streaming.formats[target->bFormatIndex-1];
+		frame = &format->frames[target->bFrameIndex-1];
+
+		dev->fcc = format->fcc;
+		dev->width = frame->width;
+		dev->height = frame->height;
+
+		memset(&pixfmt, 0, sizeof pixfmt);
+		pixfmt.width = frame->width;
+		pixfmt.height = frame->height;
+		pixfmt.pixelformat = format->fcc;
+		pixfmt.field = V4L2_FIELD_NONE;
+		if (format->fcc == V4L2_PIX_FMT_MJPEG)
+			pixfmt.sizeimage = dev->maxsize * 1.5;
+
+		uvc_stream_set_format(dev->stream, &pixfmt);
+	}
+}
+
+static void uvc_events_process(void *d)
+{
+	struct uvc_device *dev = d;
+	struct v4l2_event v4l2_event;
+	const struct uvc_event *uvc_event = (void *)&v4l2_event.u.data;
+	struct uvc_request_data resp;
+	int ret;
+
+	ret = ioctl(dev->vdev->fd, VIDIOC_DQEVENT, &v4l2_event);
+	if (ret < 0) {
+		printf("VIDIOC_DQEVENT failed: %s (%d)\n", strerror(errno),
+			errno);
+		return;
+	}
+
+	memset(&resp, 0, sizeof resp);
+	resp.length = -EL2HLT;
+
+	switch (v4l2_event.type) {
+	case UVC_EVENT_CONNECT:
+	case UVC_EVENT_DISCONNECT:
+		return;
+
+	case UVC_EVENT_SETUP:
+		uvc_events_process_setup(dev, &uvc_event->req, &resp);
+		break;
+
+	case UVC_EVENT_DATA:
+		uvc_events_process_data(dev, &uvc_event->data);
+		return;
+
+	case UVC_EVENT_STREAMON:
+		uvc_stream_enable(dev->stream, 1);
+		return;
+
+	case UVC_EVENT_STREAMOFF:
+		uvc_stream_enable(dev->stream, 0);
+		return;
+	}
+
+	ioctl(dev->vdev->fd, UVCIOC_SEND_RESPONSE, &resp);
+	if (ret < 0) {
+		printf("UVCIOC_S_EVENT failed: %s (%d)\n", strerror(errno),
+			errno);
+		return;
+	}
+}
+
+/* ---------------------------------------------------------------------------
+ * Initialization and setup
+ */
+
+void uvc_events_init(struct uvc_device *dev, struct events *events)
+{
+	struct v4l2_event_subscription sub;
+
+	/* Default to the minimum values. */
+	uvc_fill_streaming_control(dev, &dev->probe, 1, 1, 0);
+	uvc_fill_streaming_control(dev, &dev->commit, 1, 1, 0);
+
+	memset(&sub, 0, sizeof sub);
+	sub.type = UVC_EVENT_SETUP;
+	ioctl(dev->vdev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
+	sub.type = UVC_EVENT_DATA;
+	ioctl(dev->vdev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
+	sub.type = UVC_EVENT_STREAMON;
+	ioctl(dev->vdev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
+	sub.type = UVC_EVENT_STREAMOFF;
+	ioctl(dev->vdev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
+
+	events_watch_fd(events, dev->vdev->fd, EVENT_EXCEPTION,
+			uvc_events_process, dev);
+}
+
+void uvc_set_config(struct uvc_device *dev, struct uvc_function_config *fc)
+{
+	/* FIXME: The maximum size should be specified per format and frame. */
+	dev->maxsize = 0;
+	dev->fc = fc;
+}
+
+int uvc_set_format(struct uvc_device *dev, struct v4l2_pix_format *format)
+{
+	return v4l2_set_format(dev->vdev, format);
+}
+
+struct v4l2_device *uvc_v4l2_device(struct uvc_device *dev)
+{
+	/*
+	 * TODO: The V4L2 device shouldn't be exposed. We should replace this
+	 * with an abstract video sink class when one will be avaiilable.
+	 */
+	return dev->vdev;
+}
diff --git a/lib/uvc.h b/lib/uvc.h
new file mode 100644
index 0000000..f73dbff
--- /dev/null
+++ b/lib/uvc.h
@@ -0,0 +1,36 @@
+/*
+ * UVC protocol handling
+ *
+ * Copyright (C) 2010 Ideas on board SPRL <laurent.pinchart@ideasonboard.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ */
+
+#ifndef __UVC_H__
+#define __UVC_H__
+
+struct events;
+struct v4l2_device;
+struct uvc_device;
+struct uvc_function_config;
+struct uvc_stream;
+
+struct uvc_device *uvc_open(const char *devname, struct uvc_stream *stream);
+void uvc_close(struct uvc_device *dev);
+void uvc_events_init(struct uvc_device *dev, struct events *events);
+void uvc_set_config(struct uvc_device *dev, struct uvc_function_config *fc);
+int uvc_set_format(struct uvc_device *dev, struct v4l2_pix_format *format);
+struct v4l2_device *uvc_v4l2_device(struct uvc_device *dev);
+
+#endif /* __UVC_H__ */
diff --git a/lib/v4l2-source.c b/lib/v4l2-source.c
new file mode 100644
index 0000000..7eced6a
--- /dev/null
+++ b/lib/v4l2-source.c
@@ -0,0 +1,180 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * V4L2 video source
+ *
+ * Copyright (C) 2018 Laurent Pinchart
+ *
+ * Contact: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+ */
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "events.h"
+#include "tools.h"
+#include "v4l2.h"
+#include "v4l2-source.h"
+#include "video-buffers.h"
+
+struct v4l2_source {
+	struct video_source src;
+
+	struct v4l2_device *vdev;
+};
+
+#define to_v4l2_source(s) container_of(s, struct v4l2_source, src)
+
+static void v4l2_source_video_process(void *d)
+{
+	struct v4l2_source *src = d;
+	struct video_buffer buf;
+	int ret;
+
+	ret = v4l2_dequeue_buffer(src->vdev, &buf);
+	if (ret < 0)
+		return;
+
+	src->src.handler(src->src.handler_data, &src->src, &buf);
+}
+
+static void v4l2_source_destroy(struct video_source *s)
+{
+	struct v4l2_source *src = to_v4l2_source(s);
+
+	v4l2_close(src->vdev);
+	free(src);
+}
+
+static int v4l2_source_set_format(struct video_source *s,
+				  struct v4l2_pix_format *fmt)
+{
+	struct v4l2_source *src = to_v4l2_source(s);
+
+	return v4l2_set_format(src->vdev, fmt);
+}
+
+static int v4l2_source_alloc_buffers(struct video_source *s, unsigned int nbufs)
+{
+	struct v4l2_source *src = to_v4l2_source(s);
+
+	return v4l2_alloc_buffers(src->vdev, V4L2_MEMORY_MMAP, nbufs);
+}
+
+static int v4l2_source_export_buffers(struct video_source *s,
+				      struct video_buffer_set **bufs)
+{
+	struct v4l2_source *src = to_v4l2_source(s);
+	struct video_buffer_set *buffers;
+	unsigned int i;
+	int ret;
+
+	ret = v4l2_export_buffers(src->vdev);
+	if (ret < 0)
+		return ret;
+
+	buffers = video_buffer_set_new(src->vdev->buffers.nbufs);
+	if (!buffers)
+		return -ENOMEM;
+
+	for (i = 0; i < src->vdev->buffers.nbufs; ++i) {
+		struct video_buffer *buffer = &src->vdev->buffers.buffers[i];
+
+		buffers->buffers[i].size = buffer->size;
+		buffers->buffers[i].dmabuf = buffer->dmabuf;
+	}
+
+	*bufs = buffers;
+	return 0;
+}
+
+static int v4l2_source_free_buffers(struct video_source *s)
+{
+	struct v4l2_source *src = to_v4l2_source(s);
+
+	return v4l2_free_buffers(src->vdev);
+}
+
+static int v4l2_source_stream_on(struct video_source *s)
+{
+	struct v4l2_source *src = to_v4l2_source(s);
+	unsigned int i;
+	int ret;
+
+	/* Queue all buffers. */
+	for (i = 0; i < src->vdev->buffers.nbufs; ++i) {
+		struct video_buffer buf = {
+			.index = i,
+			.size = src->vdev->buffers.buffers[i].size,
+			.dmabuf = src->vdev->buffers.buffers[i].dmabuf,
+		};
+
+		ret = v4l2_queue_buffer(src->vdev, &buf);
+		if (ret < 0)
+			return ret;
+	}
+
+	ret = v4l2_stream_on(src->vdev);
+	if (ret < 0)
+		return ret;
+
+	events_watch_fd(src->src.events, src->vdev->fd, EVENT_READ,
+			v4l2_source_video_process, src);
+
+	return 0;
+}
+
+static int v4l2_source_stream_off(struct video_source *s)
+{
+	struct v4l2_source *src = to_v4l2_source(s);
+
+	events_unwatch_fd(src->src.events, src->vdev->fd, EVENT_READ);
+
+	return v4l2_stream_off(src->vdev);
+}
+
+static int v4l2_source_queue_buffer(struct video_source *s,
+				    struct video_buffer *buf)
+{
+	struct v4l2_source *src = to_v4l2_source(s);
+
+	return v4l2_queue_buffer(src->vdev, buf);
+}
+
+static const struct video_source_ops v4l2_source_ops = {
+	.destroy = v4l2_source_destroy,
+	.set_format = v4l2_source_set_format,
+	.alloc_buffers = v4l2_source_alloc_buffers,
+	.export_buffers = v4l2_source_export_buffers,
+	.free_buffers = v4l2_source_free_buffers,
+	.stream_on = v4l2_source_stream_on,
+	.stream_off = v4l2_source_stream_off,
+	.queue_buffer = v4l2_source_queue_buffer,
+};
+
+struct video_source *v4l2_video_source_create(const char *devname)
+{
+	struct v4l2_source *src;
+
+	src = malloc(sizeof *src);
+	if (!src)
+		return NULL;
+
+	memset(src, 0, sizeof *src);
+	src->src.ops = &v4l2_source_ops;
+
+	src->vdev = v4l2_open(devname);
+	if (!src->vdev) {
+		free(src);
+		return NULL;
+	}
+
+	return &src->src;
+}
+
+void v4l2_video_source_init(struct video_source *s, struct events *events)
+{
+	struct v4l2_source *src = to_v4l2_source(s);
+
+	src->src.events = events;
+}
diff --git a/lib/v4l2.c b/lib/v4l2.c
new file mode 100644
index 0000000..9030270
--- /dev/null
+++ b/lib/v4l2.c
@@ -0,0 +1,833 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * V4L2 Devices
+ *
+ * Copyright (C) 2018 Laurent Pinchart
+ *
+ * This file originally comes from the omap3-isp-live project
+ * (git://git.ideasonboard.org/omap3-isp-live.git)
+ *
+ * Copyright (C) 2010-2011 Ideas on board SPRL
+ *
+ * Contact: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <linux/videodev2.h>
+
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/select.h>
+#include <sys/time.h>
+
+#include "list.h"
+#include "tools.h"
+#include "v4l2.h"
+#include "video-buffers.h"
+
+#ifndef V4L2_BUF_FLAG_ERROR
+#define V4L2_BUF_FLAG_ERROR	0x0040
+#endif
+
+struct v4l2_ival_desc {
+	struct v4l2_fract min;
+	struct v4l2_fract max;
+	struct v4l2_fract step;
+
+	struct list_entry list;
+};
+
+struct v4l2_frame_desc {
+	unsigned int min_width;
+	unsigned int min_height;
+	unsigned int max_width;
+	unsigned int max_height;
+	unsigned int step_width;
+	unsigned int step_height;
+
+	struct list_entry list;
+	struct list_entry ivals;
+};
+
+struct v4l2_format_desc {
+	unsigned int pixelformat;
+
+	struct list_entry list;
+	struct list_entry frames;
+};
+
+/* -----------------------------------------------------------------------------
+ * Formats enumeration
+ */
+
+static int
+v4l2_enum_frame_intervals(struct v4l2_device *dev, struct v4l2_format_desc *format,
+	struct v4l2_frame_desc *frame)
+{
+	struct v4l2_ival_desc *ival;
+	unsigned int i;
+	int ret;
+
+	for (i = 0; ; ++i) {
+		struct v4l2_frmivalenum ivalenum;
+
+		memset(&ivalenum, 0, sizeof ivalenum);
+		ivalenum.index = i;
+		ivalenum.pixel_format = format->pixelformat;
+		ivalenum.width = frame->min_width;
+		ivalenum.height = frame->min_height;
+		ret = ioctl(dev->fd, VIDIOC_ENUM_FRAMEINTERVALS, &ivalenum);
+		if (ret < 0)
+			break;
+
+		if (i != ivalenum.index)
+			printf("Warning: driver returned wrong ival index "
+				"%u.\n", ivalenum.index);
+		if (format->pixelformat != ivalenum.pixel_format)
+			printf("Warning: driver returned wrong ival pixel "
+				"format %08x.\n", ivalenum.pixel_format);
+		if (frame->min_width != ivalenum.width)
+			printf("Warning: driver returned wrong ival width "
+				"%u.\n", ivalenum.width);
+		if (frame->min_height != ivalenum.height)
+			printf("Warning: driver returned wrong ival height "
+				"%u.\n", ivalenum.height);
+
+		ival = malloc(sizeof *ival);
+		if (ival == NULL)
+			return -ENOMEM;
+
+		memset(ival, 0, sizeof *ival);
+
+		switch (ivalenum.type) {
+		case V4L2_FRMIVAL_TYPE_DISCRETE:
+			ival->min = ivalenum.discrete;
+			ival->max = ivalenum.discrete;
+			ival->step.numerator = 1;
+			ival->step.denominator = 1;
+			break;
+
+		case V4L2_FRMIVAL_TYPE_STEPWISE:
+			ival->min = ivalenum.stepwise.min;
+			ival->max = ivalenum.stepwise.max;
+			ival->step = ivalenum.stepwise.step;
+			break;
+
+		default:
+			printf("Error: driver returned invalid frame ival "
+				"type %u\n", ivalenum.type);
+			return -EINVAL;
+		}
+
+		list_append(&ival->list, &frame->ivals);
+	}
+
+	return 0;
+}
+
+static int
+v4l2_enum_frame_sizes(struct v4l2_device *dev, struct v4l2_format_desc *format)
+{
+	struct v4l2_frame_desc *frame;
+	unsigned int i;
+	int ret;
+
+	for (i = 0; ; ++i) {
+		struct v4l2_frmsizeenum frmenum;
+
+		memset(&frmenum, 0, sizeof frmenum);
+		frmenum.index = i;
+		frmenum.pixel_format = format->pixelformat;
+
+		ret = ioctl(dev->fd, VIDIOC_ENUM_FRAMESIZES, &frmenum);
+		if (ret < 0)
+			break;
+
+		if (i != frmenum.index)
+			printf("Warning: driver returned wrong frame index "
+				"%u.\n", frmenum.index);
+		if (format->pixelformat != frmenum.pixel_format)
+			printf("Warning: driver returned wrong frame pixel "
+				"format %08x.\n", frmenum.pixel_format);
+
+		frame = malloc(sizeof *frame);
+		if (frame == NULL)
+			return -ENOMEM;
+
+		memset(frame, 0, sizeof *frame);
+
+		list_init(&frame->ivals);
+		frame->step_width = 1;
+		frame->step_height = 1;
+
+		switch (frmenum.type) {
+		case V4L2_FRMSIZE_TYPE_DISCRETE:
+			frame->min_width = frmenum.discrete.width;
+			frame->min_height = frmenum.discrete.height;
+			frame->max_width = frmenum.discrete.width;
+			frame->max_height = frmenum.discrete.height;
+			break;
+
+		case V4L2_FRMSIZE_TYPE_STEPWISE:
+			frame->step_width = frmenum.stepwise.step_width;
+			frame->step_height = frmenum.stepwise.step_height;
+			/* fallthrough */
+		case V4L2_FRMSIZE_TYPE_CONTINUOUS:
+			frame->min_width = frmenum.stepwise.min_width;
+			frame->min_height = frmenum.stepwise.min_height;
+			frame->max_width = frmenum.stepwise.max_width;
+			frame->max_height = frmenum.stepwise.max_height;
+			break;
+
+		default:
+			printf("Error: driver returned invalid frame size "
+				"type %u\n", frmenum.type);
+			return -EINVAL;
+		}
+
+		list_append(&frame->list, &format->frames);
+
+		ret = v4l2_enum_frame_intervals(dev, format, frame);
+		if (ret < 0)
+			return ret;
+	}
+
+	return 0;
+}
+static int v4l2_enum_formats(struct v4l2_device *dev)
+{
+	struct v4l2_format_desc *format;
+	unsigned int i;
+	int ret;
+
+	for (i = 0; ; ++i) {
+		struct v4l2_fmtdesc fmtenum;
+
+		memset(&fmtenum, 0, sizeof fmtenum);
+		fmtenum.index = i;
+		fmtenum.type = dev->type;
+
+		ret = ioctl(dev->fd, VIDIOC_ENUM_FMT, &fmtenum);
+		if (ret < 0)
+			break;
+
+		if (i != fmtenum.index)
+			printf("Warning: driver returned wrong format index "
+				"%u.\n", fmtenum.index);
+		if (dev->type != fmtenum.type)
+			printf("Warning: driver returned wrong format type "
+				"%u.\n", fmtenum.type);
+
+		format = malloc(sizeof *format);
+		if (format == NULL)
+			return -ENOMEM;
+
+		memset(format, 0, sizeof *format);
+
+		list_init(&format->frames);
+		format->pixelformat = fmtenum.pixelformat;
+
+		list_append(&format->list, &dev->formats);
+
+		ret = v4l2_enum_frame_sizes(dev, format);
+		if (ret < 0)
+			return ret;
+	}
+
+	return 0;
+}
+
+/* -----------------------------------------------------------------------------
+ * Open/close
+ */
+
+struct v4l2_device *v4l2_open(const char *devname)
+{
+	struct v4l2_device *dev;
+	struct v4l2_capability cap;
+	__u32 capabilities;
+	int ret;
+
+	dev = malloc(sizeof *dev);
+	if (dev == NULL)
+		return NULL;
+
+	memset(dev, 0, sizeof *dev);
+	dev->fd = -1;
+	dev->name = strdup(devname);
+	list_init(&dev->formats);
+
+	dev->fd = open(devname, O_RDWR | O_NONBLOCK);
+	if (dev->fd < 0) {
+		printf("Error opening device %s: %d.\n", devname, errno);
+		v4l2_close(dev);
+		return NULL;
+	}
+
+	memset(&cap, 0, sizeof cap);
+	ret = ioctl(dev->fd, VIDIOC_QUERYCAP, &cap);
+	if (ret < 0) {
+		printf("Error opening device %s: unable to query "
+			"device.\n", devname);
+		v4l2_close(dev);
+		return NULL;
+	}
+
+	/* 
+	 * If the device_caps field is set use it, otherwise use the older
+	 * capabilities field.
+	 */
+	capabilities = cap.device_caps ? : cap.capabilities;
+
+	if (capabilities & V4L2_CAP_VIDEO_CAPTURE)
+		dev->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+	else if (capabilities & V4L2_CAP_VIDEO_OUTPUT)
+		dev->type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
+	else {
+		printf("Error opening device %s: neither video capture "
+			"nor video output supported.\n", devname);
+		v4l2_close(dev);
+		return NULL;
+	}
+
+	ret = v4l2_enum_formats(dev);
+	if (ret < 0) {
+		printf("Error opening device %s: unable to enumerate "
+			"formats.\n", devname);
+		v4l2_close(dev);
+		return NULL;
+	}
+
+	printf("Device %s opened: %s (%s).\n", devname, cap.card, cap.bus_info);
+
+	return dev;
+}
+
+void v4l2_close(struct v4l2_device *dev)
+{
+	struct v4l2_format_desc *format, *next_fmt;
+	struct v4l2_frame_desc *frame, *next_frm;
+	struct v4l2_ival_desc *ival, *next_ival;
+
+	if (dev == NULL)
+		return;
+
+	list_for_each_entry_safe(format, next_fmt, &dev->formats, list) {
+		list_for_each_entry_safe(frame, next_frm, &format->frames, list) {
+			list_for_each_entry_safe(ival, next_ival, &frame->ivals, list) {
+				free(ival);
+			}
+			free(frame);
+		}
+		free(format);
+	}
+
+	free(dev->name);
+	close(dev->fd);
+	free(dev);
+}
+
+/* -----------------------------------------------------------------------------
+ * Controls
+ */
+
+int v4l2_get_control(struct v4l2_device *dev, unsigned int id, int32_t *value)
+{
+	struct v4l2_control ctrl;
+	int ret;
+
+	ctrl.id = id;
+
+	ret = ioctl(dev->fd, VIDIOC_G_CTRL, &ctrl);
+	if (ret < 0) {
+		printf("%s: unable to get control (%d).\n", dev->name, errno);
+		return -errno;
+	}
+
+	*value = ctrl.value;
+	return 0;
+}
+
+int v4l2_set_control(struct v4l2_device *dev, unsigned int id, int32_t *value)
+{
+	struct v4l2_control ctrl;
+	int ret;
+
+	ctrl.id = id;
+	ctrl.value = *value;
+
+	ret = ioctl(dev->fd, VIDIOC_S_CTRL, &ctrl);
+	if (ret < 0) {
+		printf("%s: unable to set control (%d).\n", dev->name, errno);
+		return -errno;
+	}
+
+	*value = ctrl.value;
+	return 0;
+}
+
+int v4l2_get_controls(struct v4l2_device *dev, unsigned int count,
+		      struct v4l2_ext_control *ctrls)
+{
+	struct v4l2_ext_controls controls;
+	int ret;
+
+	memset(&controls, 0, sizeof controls);
+	controls.count = count;
+	controls.controls = ctrls;
+
+	ret = ioctl(dev->fd, VIDIOC_G_EXT_CTRLS, &controls);
+	if (ret < 0)
+		printf("%s: unable to get multiple controls (%d).\n", dev->name,
+		       errno);
+
+	return ret;
+}
+
+int v4l2_set_controls(struct v4l2_device *dev, unsigned int count,
+		      struct v4l2_ext_control *ctrls)
+{
+	struct v4l2_ext_controls controls;
+	int ret;
+
+	memset(&controls, 0, sizeof controls);
+	controls.count = count;
+	controls.controls = ctrls;
+
+	ret = ioctl(dev->fd, VIDIOC_S_EXT_CTRLS, &controls);
+	if (ret < 0)
+		printf("%s: unable to set multiple controls (%d).\n", dev->name,
+		       errno);
+
+	return ret;
+}
+
+/* -----------------------------------------------------------------------------
+ * Formats and frame rates
+ */
+
+int v4l2_get_crop(struct v4l2_device *dev, struct v4l2_rect *rect)
+{
+	struct v4l2_crop crop;
+	int ret;
+
+	memset(&crop, 0, sizeof crop);
+	crop.type = dev->type;
+
+	ret = ioctl(dev->fd, VIDIOC_G_CROP, &crop);
+	if (ret < 0) {
+		printf("%s: unable to get crop rectangle (%d).\n", dev->name,
+		       errno);
+		return -errno;
+	}
+
+	dev->crop = crop.c;
+	*rect = crop.c;
+
+	return 0;
+}
+
+int v4l2_set_crop(struct v4l2_device *dev, struct v4l2_rect *rect)
+{
+	struct v4l2_crop crop;
+	int ret;
+
+	memset(&crop, 0, sizeof crop);
+	crop.type = dev->type;
+	crop.c = *rect;
+
+	ret = ioctl(dev->fd, VIDIOC_S_CROP, &crop);
+	if (ret < 0) {
+		printf("%s: unable to set crop rectangle (%d).\n", dev->name,
+		       errno);
+		return -errno;
+	}
+
+	dev->crop = crop.c;
+	*rect = crop.c;
+
+	return 0;
+}
+
+int v4l2_get_format(struct v4l2_device *dev, struct v4l2_pix_format *format)
+{
+	struct v4l2_format fmt;
+	int ret;
+
+	memset(&fmt, 0, sizeof fmt);
+	fmt.type = dev->type;
+
+	ret = ioctl(dev->fd, VIDIOC_G_FMT, &fmt);
+	if (ret < 0) {
+		printf("%s: unable to get format (%d).\n", dev->name, errno);
+		return -errno;
+	}
+
+	dev->format = fmt.fmt.pix;
+	*format = fmt.fmt.pix;
+
+	return 0;
+}
+
+int v4l2_set_format(struct v4l2_device *dev, struct v4l2_pix_format *format)
+{
+	struct v4l2_format fmt;
+	int ret;
+
+	memset(&fmt, 0, sizeof fmt);
+	fmt.type = dev->type;
+	fmt.fmt.pix.width = format->width;
+	fmt.fmt.pix.height = format->height;
+	fmt.fmt.pix.pixelformat = format->pixelformat;
+	fmt.fmt.pix.field = V4L2_FIELD_ANY;
+
+	ret = ioctl(dev->fd, VIDIOC_S_FMT, &fmt);
+	if (ret < 0) {
+		printf("%s: unable to set format (%d).\n", dev->name, errno);
+		return -errno;
+	}
+
+	dev->format = fmt.fmt.pix;
+	*format = fmt.fmt.pix;
+
+	return 0;
+}
+
+/* -----------------------------------------------------------------------------
+ * Buffers management
+ */
+
+int v4l2_alloc_buffers(struct v4l2_device *dev, enum v4l2_memory memtype,
+		       unsigned int nbufs)
+{
+	struct v4l2_requestbuffers rb;
+	unsigned int i;
+	int ret;
+
+	if (dev->buffers.nbufs != 0)
+		return -EBUSY;
+
+	if (memtype != V4L2_MEMORY_MMAP && memtype != V4L2_MEMORY_DMABUF)
+		return -EINVAL;
+
+	/* Request the buffers from the driver. */
+	memset(&rb, 0, sizeof rb);
+	rb.count = nbufs;
+	rb.type = dev->type;
+	rb.memory = memtype;
+
+	ret = ioctl(dev->fd, VIDIOC_REQBUFS, &rb);
+	if (ret < 0) {
+		printf("%s: unable to request buffers (%d).\n", dev->name,
+		       errno);
+		ret = -errno;
+		goto done;
+	}
+
+	if (rb.count > nbufs) {
+		printf("%s: driver needs more buffers (%u) than available (%u).\n",
+		       dev->name, rb.count, nbufs);
+		ret = -E2BIG;
+		goto done;
+	}
+
+	printf("%s: %u buffers requested.\n", dev->name, rb.count);
+
+	/* Allocate the buffer objects. */
+	dev->memtype = memtype;
+	dev->buffers.nbufs = rb.count;
+
+	dev->buffers.buffers = calloc(nbufs, sizeof *dev->buffers.buffers);
+	if (dev->buffers.buffers == NULL) {
+		ret = -ENOMEM;
+		goto done;
+	}
+
+	for (i = 0; i < dev->buffers.nbufs; ++i) {
+		dev->buffers.buffers[i].index = i;
+		dev->buffers.buffers[i].dmabuf = -1;
+	}
+
+	ret = 0;
+
+done:
+	if (ret < 0)
+		v4l2_free_buffers(dev);
+
+	return ret;
+}
+
+int v4l2_free_buffers(struct v4l2_device *dev)
+{
+	struct v4l2_requestbuffers rb;
+	unsigned int i;
+	int ret;
+
+	if (dev->buffers.nbufs == 0)
+		return 0;
+
+	for (i = 0; i < dev->buffers.nbufs; ++i) {
+		struct video_buffer *buffer = &dev->buffers.buffers[i];
+
+		if (buffer->mem) {
+			ret = munmap(buffer->mem, buffer->size);
+			if (ret < 0) {
+				printf("%s: unable to unmap buffer %u (%d)\n",
+				       dev->name, i, errno);
+				return -errno;
+			}
+
+			buffer->mem = NULL;
+		}
+
+		if (buffer->dmabuf != -1) {
+			close(buffer->dmabuf);
+			buffer->dmabuf = -1;
+		}
+
+		buffer->size = 0;
+	}
+
+	memset(&rb, 0, sizeof rb);
+	rb.count = 0;
+	rb.type = dev->type;
+	rb.memory = dev->memtype;
+
+	ret = ioctl(dev->fd, VIDIOC_REQBUFS, &rb);
+	if (ret < 0) {
+		printf("%s: unable to release buffers (%d)\n", dev->name,
+		       errno);
+		return -errno;
+	}
+
+	free(dev->buffers.buffers);
+	dev->buffers.buffers = NULL;
+	dev->buffers.nbufs = 0;
+
+	return 0;
+}
+
+int v4l2_export_buffers(struct v4l2_device *dev)
+{
+	unsigned int i;
+	int ret;
+
+	if (dev->buffers.nbufs == 0)
+		return -EINVAL;
+
+	if (dev->memtype != V4L2_MEMORY_MMAP)
+		return -EINVAL;
+
+	for (i = 0; i < dev->buffers.nbufs; ++i) {
+		struct v4l2_exportbuffer expbuf = {
+			.type = dev->type,
+			.index = i,
+		};
+		struct v4l2_buffer buf = {
+			.index = i,
+			.type = dev->type,
+			.memory = dev->memtype,
+		};
+
+		ret = ioctl(dev->fd, VIDIOC_QUERYBUF, &buf);
+		if (ret < 0) {
+			printf("%s: unable to query buffer %u (%d).\n",
+			       dev->name, i, errno);
+			return -errno;
+		}
+
+		ret = ioctl(dev->fd, VIDIOC_EXPBUF, &expbuf);
+		if (ret < 0) {
+			printf("Failed to export buffer %u.\n", i);
+			return -errno;
+		}
+
+		dev->buffers.buffers[i].size = buf.length;
+		dev->buffers.buffers[i].dmabuf = expbuf.fd;
+
+		printf("%s: buffer %u exported with fd %u.\n",
+		       dev->name, i, dev->buffers.buffers[i].dmabuf);
+	}
+
+	return 0;
+}
+
+int v4l2_import_buffers(struct v4l2_device *dev,
+			const struct video_buffer_set *buffers)
+{
+	unsigned int i;
+	int ret;
+
+	if (dev->buffers.nbufs == 0 || dev->buffers.nbufs > buffers->nbufs)
+		return -EINVAL;
+
+	if (dev->memtype != V4L2_MEMORY_DMABUF)
+		return -EINVAL;
+
+	for (i = 0; i < dev->buffers.nbufs; ++i) {
+		const struct video_buffer *buffer = &buffers->buffers[i];
+		struct v4l2_buffer buf = {
+			.index = i,
+			.type = dev->type,
+			.memory = dev->memtype,
+		};
+		int fd;
+
+		ret = ioctl(dev->fd, VIDIOC_QUERYBUF, &buf);
+		if (ret < 0) {
+			printf("%s: unable to query buffer %u (%d).\n",
+			       dev->name, i, errno);
+			return -errno;
+		}
+
+		if (buffer->size < buf.length) {
+			printf("%s: buffer %u too small (%u bytes required, %u bytes available).\n",
+			       dev->name, i, buf.length, buffer->size);
+			return -EINVAL;
+		}
+
+		fd = dup(buffer->dmabuf);
+		if (fd < 0) {
+			printf("%s: failed to duplicate dmabuf fd %d.\n",
+			       dev->name, buffer->dmabuf);
+			return ret;
+		}
+
+		printf("%s: buffer %u valid.\n", dev->name, i);
+
+		dev->buffers.buffers[i].dmabuf = fd;
+		dev->buffers.buffers[i].size = buffer->size;
+	}
+
+	return 0;
+}
+
+int v4l2_mmap_buffers(struct v4l2_device *dev)
+{
+	unsigned int i;
+	int ret;
+
+	if (dev->memtype != V4L2_MEMORY_MMAP)
+		return -EINVAL;
+
+	for (i = 0; i < dev->buffers.nbufs; ++i) {
+		struct video_buffer *buffer = &dev->buffers.buffers[i];
+		struct v4l2_buffer buf = {
+			.index = i,
+			.type = dev->type,
+			.memory = dev->memtype,
+		};
+		void *mem;
+
+		ret = ioctl(dev->fd, VIDIOC_QUERYBUF, &buf);
+		if (ret < 0) {
+			printf("%s: unable to query buffer %u (%d).\n",
+			       dev->name, i, errno);
+			return -errno;
+		}
+
+		mem = mmap(0, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED,
+			   dev->fd, buf.m.offset);
+		if (mem == MAP_FAILED) {
+			printf("%s: unable to map buffer %u (%d)\n",
+			       dev->name, i, errno);
+			return -errno;
+		}
+
+		buffer->mem = mem;
+		buffer->size = buf.length;
+
+		printf("%s: buffer %u mapped at address %p.\n", dev->name, i,
+		       mem);
+	}
+
+	return 0;
+}
+
+int v4l2_dequeue_buffer(struct v4l2_device *dev, struct video_buffer *buffer)
+{
+	struct v4l2_buffer buf;
+	int ret;
+
+	memset(&buf, 0, sizeof buf);
+	buf.type = dev->type;
+	buf.memory = dev->memtype;
+
+	ret = ioctl(dev->fd, VIDIOC_DQBUF, &buf);
+	if (ret < 0) {
+		printf("%s: unable to dequeue buffer index %u/%u (%d)\n",
+		       dev->name, buf.index, dev->buffers.nbufs, errno);
+		return -errno;
+	}
+
+	buffer->index = buf.index;
+	buffer->bytesused = buf.bytesused;
+	buffer->timestamp = buf.timestamp;
+	buffer->error = !!(buf.flags & V4L2_BUF_FLAG_ERROR);
+
+	return 0;
+}
+
+int v4l2_queue_buffer(struct v4l2_device *dev, struct video_buffer *buffer)
+{
+	struct v4l2_buffer buf;
+	int ret;
+
+	if (buffer->index >= dev->buffers.nbufs)
+		return -EINVAL;
+
+	memset(&buf, 0, sizeof buf);
+	buf.index = buffer->index;
+	buf.type = dev->type;
+	buf.memory = dev->memtype;
+
+	if (dev->memtype == V4L2_MEMORY_DMABUF)
+		buf.m.fd = (unsigned long)dev->buffers.buffers[buffer->index].dmabuf;
+
+	if (dev->type == V4L2_BUF_TYPE_VIDEO_OUTPUT)
+		buf.bytesused = buffer->bytesused;
+
+	ret = ioctl(dev->fd, VIDIOC_QBUF, &buf);
+	if (ret < 0) {
+		printf("%s: unable to queue buffer index %u/%u (%d)\n",
+		       dev->name, buf.index, dev->buffers.nbufs, errno);
+		return -errno;
+	}
+
+	return 0;
+}
+
+/* -----------------------------------------------------------------------------
+ * Stream management
+ */
+
+int v4l2_stream_on(struct v4l2_device *dev)
+{
+	int type = dev->type;
+	int ret;
+
+	ret = ioctl(dev->fd, VIDIOC_STREAMON, &type);
+	if (ret < 0)
+		return -errno;
+
+	return 0;
+}
+
+int v4l2_stream_off(struct v4l2_device *dev)
+{
+	int type = dev->type;
+	int ret;
+
+	ret = ioctl(dev->fd, VIDIOC_STREAMOFF, &type);
+	if (ret < 0)
+		return -errno;
+
+	return 0;
+}
diff --git a/lib/v4l2.h b/lib/v4l2.h
new file mode 100644
index 0000000..8d7b3c5
--- /dev/null
+++ b/lib/v4l2.h
@@ -0,0 +1,339 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * V4L2 Devices
+ *
+ * Copyright (C) 2018 Laurent Pinchart
+ *
+ * This file originally comes from the omap3-isp-live project
+ * (git://git.ideasonboard.org/omap3-isp-live.git)
+ *
+ * Copyright (C) 2010-2011 Ideas on board SPRL
+ *
+ * Contact: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+ */
+#ifndef __V4L2_H
+#define __V4L2_H
+
+#include <linux/videodev2.h>
+#include <stdint.h>
+
+#include "list.h"
+#include "video-buffers.h"
+
+struct v4l2_device
+{
+	int fd;
+	char *name;
+
+	enum v4l2_buf_type type;
+	enum v4l2_memory memtype;
+
+	struct list_entry formats;
+	struct v4l2_pix_format format;
+	struct v4l2_rect crop;
+
+	struct video_buffer_set buffers;
+};
+
+/*
+ * v4l2_open - Open a V4L2 device
+ * @devname: Name (including path) of the device node
+ *
+ * Open the V4L2 device referenced by @devname for video capture or display in
+ * non-blocking mode.
+ *
+ * If the device can be opened, query its capabilities and enumerates frame
+ * formats, sizes and intervals.
+ *
+ * Return a pointer to a newly allocated v4l2_device structure instance on
+ * success and NULL on failure. The returned pointer must be freed with
+ * v4l2_close when the device isn't needed anymore.
+ */
+struct v4l2_device *v4l2_open(const char *devname);
+
+/*
+ * v4l2_close - Close a V4L2 device
+ * @dev: Device instance
+ *
+ * Close the device instance given as argument and free allocated resources.
+ * Access to the device instance is forbidden after this function returns.
+ */
+void v4l2_close(struct v4l2_device *dev);
+
+/*
+ * v4l2_get_format - Retrieve the current pixel format
+ * @dev: Device instance
+ * @format: Pixel format structure to be filled
+ *
+ * Query the device to retrieve the current pixel format and frame size and fill
+ * the @format structure.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_get_format(struct v4l2_device *dev, struct v4l2_pix_format *format);
+
+/*
+ * v4l2_set_format - Set the pixel format
+ * @dev: Device instance
+ * @format: Pixel format structure to be set
+ *
+ * Set the pixel format and frame size stored in @format. The device can modify
+ * the requested format and size, in which case the @format structure will be
+ * updated to reflect the modified settings.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_set_format(struct v4l2_device *dev, struct v4l2_pix_format *format);
+
+/*
+ * v4l2_get_crop - Retrieve the current crop rectangle
+ * @dev: Device instance
+ * @rect: Crop rectangle structure to be filled
+ *
+ * Query the device to retrieve the current crop rectangle and fill the @rect
+ * structure.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_get_crop(struct v4l2_device *dev, struct v4l2_rect *rect);
+
+/*
+ * v4l2_set_crop - Set the crop rectangle
+ * @dev: Device instance
+ * @rect: Crop rectangle structure to be set
+ *
+ * Set the crop rectangle stored in @rect. The device can modify the requested
+ * rectangle, in which case the @rect structure will be updated to reflect the
+ * modified settings.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_set_crop(struct v4l2_device *dev, struct v4l2_rect *rect);
+
+/*
+ * v4l2_alloc_buffers - Allocate buffers for video frames
+ * @dev: Device instance
+ * @memtype: Type of buffers
+ * @nbufs: Number of buffers to allocate
+ *
+ * Request the driver to allocate @nbufs buffers. The driver can modify the
+ * number of buffers depending on its needs. The number of allocated buffers
+ * will be stored in the @dev->buffers.nbufs field.
+ *
+ * When @memtype is set to V4L2_MEMORY_MMAP the buffers are allocated by the
+ * driver. They can then be mapped to userspace by calling v4l2_mmap_buffers().
+ * When @memtype is set to V4L2_MEMORY_DMABUF the driver only allocates buffer
+ * objects and relies on the application to provide memory storage for video
+ * frames.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_alloc_buffers(struct v4l2_device *dev, enum v4l2_memory memtype,
+		       unsigned int nbufs);
+
+/*
+ * v4l2_free_buffers - Free buffers
+ * @dev: Device instance
+ *
+ * Free buffers previously allocated by v4l2_alloc_buffers(). If the buffers
+ * have been allocated with the V4L2_MEMORY_DMABUF memory type only the buffer
+ * objects are freed, and the caller is responsible for freeing the video frames
+ * memory if required.
+ *
+ * When successful this function sets the @dev->buffers.nbufs field to zero.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_free_buffers(struct v4l2_device *dev);
+
+/*
+ * v4l2_export_buffers - Export buffers as dmabuf objects
+ * @dev: Device instance
+ *
+ * Export all the buffers previously allocated by v4l2_alloc_buffers() as dmabuf
+ * objects. The dmabuf objects handles can be accessed through the dmabuf field
+ * of each entry in the @dev::buffers array.
+ *
+ * The dmabuf objects handles will be automatically closed when the buffers are
+ * freed with v4l2_free_buffers().
+ *
+ * This function can only be called when buffers have been allocated with the
+ * memory type set to V4L2_MEMORY_MMAP. If the memory type is different, or if
+ * no buffers have been allocated, it will return -EINVAL.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_export_buffers(struct v4l2_device *dev);
+
+/*
+ * v4l2_import_buffers - Import buffer backing store as dmabuf objects
+ * @dev: Device instance
+ * @buffers: Buffers to be imported
+ *
+ * Import the dmabuf objects from @buffers as backing store for the device
+ * buffers previously allocated by v4l2_alloc_buffers().
+ *
+ * The dmabuf file handles are duplicated and stored in the dmabuf field of the
+ * @dev::buffers array. The handles from the @buffers set can thus be closed
+ * independently without impacting usage of the imported dmabuf objects. The
+ * duplicated file handles will be automatically closed when the buffers are
+ * freed with v4l2_free_buffers().
+ *
+ * This function can only be called when buffers have been allocated with the
+ * memory type set to V4L2_MEMORY_DMABUF. If the memory type is different, if no
+ * buffers have been allocated, or if the number of allocated buffers is larger
+ * than @buffers->nbufs, it will return -EINVAL.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_import_buffers(struct v4l2_device *dev,
+			const struct video_buffer_set *buffers);
+
+/*
+ * v4l2_mmap_buffers - Map buffers to application memory space
+ * @dev: Device instance
+ *
+ * Map all the buffers previously allocated by v4l2_alloc_buffers() to the
+ * application memory space. The buffer memory can be accessed through the mem
+ * field of each entry in the @dev::buffers array.
+ *
+ * Buffers will be automatically unmapped when freed with v4l2_free_buffers().
+ *
+ * This function can only be called when buffers have been allocated with the
+ * memory type set to V4L2_MEMORY_MMAP. If the memory type is different, or if
+ * no buffers have been allocated, it will return -EINVAL.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_mmap_buffers(struct v4l2_device *dev);
+
+/*
+ * v4l2_queue_buffer - Queue a buffer for video capture/output
+ * @dev: Device instance
+ * @buffer: Buffer to be queued
+ *
+ * Queue the buffer identified by @buffer for video capture or output, depending
+ * on the device type.
+ *
+ * The caller must initialize the @buffer::index field with the index of the
+ * buffer to be queued. The index is zero-based and must be lower than the
+ * number of allocated buffers.
+ *
+ * For V4L2_MEMORY_DMABUF buffers, the caller must initialize the @buffer::dmabuf
+ * field with the address of the video frame memory, and the @buffer:length
+ * field with its size in bytes. For optimal performances the address and length
+ * should be identical between v4l2_queue_buffer() calls for a given buffer
+ * index.
+ *
+ * For video output, the caller must initialize the @buffer::bytesused field
+ * with the size of video data. The value should differ from the buffer length
+ * for variable-size video formats only.
+ *
+ * Upon successful return the buffer ownership is transferred to the driver. The
+ * caller must not touch video memory for that buffer before calling
+ * v4l2_dequeue_buffer(). Attempting to queue an already queued buffer will
+ * fail.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_queue_buffer(struct v4l2_device *dev, struct video_buffer *buffer);
+
+/*
+ * v4l2_dequeue_buffer - Dequeue the next buffer
+ * @dev: Device instance
+ * @buffer: Dequeued buffer data to be filled
+ *
+ * Dequeue the next buffer processed by the driver and fill all fields in
+ * @buffer. 
+ *
+ * This function does not block. If no buffer is ready it will return
+ * immediately with -EAGAIN.
+ *
+ * If an error occured during video capture or display, the @buffer::error field
+ * is set to true. Depending on the device the video data can be partly
+ * corrupted or complete garbage.
+ *
+ * Once dequeued the buffer ownership is transferred to the caller. Video memory
+ * for that buffer can be safely read from and written to.
+ *
+ * Return 0 on success or a negative error code on failure. An error that
+ * results in @buffer:error being set is not considered as a failure condition
+ * for the purpose of the return value.
+ */
+int v4l2_dequeue_buffer(struct v4l2_device *dev, struct video_buffer *buffer);
+
+/*
+ * v4l2_stream_on - Start video streaming
+ * @dev: Device instance
+ *
+ * Start video capture or output on the device. For video output devices at
+ * least one buffer must be queued before starting the stream.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_stream_on(struct v4l2_device *dev);
+
+/*
+ * v4l2_stream_off - Stop video streaming
+ * @dev: Device instance
+ *
+ * Stop video capture or output on the device. Upon successful return ownership
+ * of all buffers is returned to the caller.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_stream_off(struct v4l2_device *dev);
+
+/*
+ * v4l2_get_control - Read the value of a control
+ * @dev: Device instance
+ * @id: Control ID
+ * @value: Control value to be filled
+ *
+ * Retrieve the current value of control @id and store it in @value.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_get_control(struct v4l2_device *dev, unsigned int id, int32_t *value);
+
+/*
+ * v4l2_set_control - Write the value of a control
+ * @dev: Device instance
+ * @id: Control ID
+ * @value: Control value
+ *
+ * Set control @id to @value. The device is allowed to modify the requested
+ * value, in which case @value is updated to the modified value.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_set_control(struct v4l2_device *dev, unsigned int id, int32_t *value);
+
+/*
+ * v4l2_get_controls - Read the value of multiple controls
+ * @dev: Device instance
+ * @count: Number of controls
+ * @ctrls: Controls to be read
+ *
+ * Retrieve the current value of controls identified by @ctrls.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_get_controls(struct v4l2_device *dev, unsigned int count,
+		      struct v4l2_ext_control *ctrls);
+
+/*
+ * v4l2_set_controls - Write the value of multiple controls
+ * @dev: Device instance
+ * @count: Number of controls
+ * @ctrls: Controls to be written
+ *
+ * Set controls identified by @ctrls. The device is allowed to modify the
+ * requested values, in which case @ctrls is updated to the modified value.
+ *
+ * Return 0 on success or a negative error code on failure.
+ */
+int v4l2_set_controls(struct v4l2_device *dev, unsigned int count,
+		      struct v4l2_ext_control *ctrls);
+
+#endif
diff --git a/lib/video-buffers.c b/lib/video-buffers.c
new file mode 100644
index 0000000..f5dc6ec
--- /dev/null
+++ b/lib/video-buffers.c
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Video buffers
+ *
+ * Copyright (C) 2018 Laurent Pinchart
+ *
+ * Contact: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+ */
+
+#include "video-buffers.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+struct video_buffer_set *video_buffer_set_new(unsigned int nbufs)
+{
+	struct video_buffer_set *buffers;
+
+	buffers = malloc(sizeof *buffers);
+	if (!buffers)
+		return NULL;
+
+	buffers->nbufs = nbufs;
+	buffers->buffers = calloc(nbufs, sizeof *buffers->buffers);
+	if (!buffers->buffers) {
+		free(buffers);
+		return NULL;
+	}
+
+	return buffers;
+}
+
+void video_buffer_set_delete(struct video_buffer_set *buffers)
+{
+	if (!buffers)
+		return;
+
+	free(buffers->buffers);
+	free(buffers);
+}
diff --git a/lib/video-buffers.h b/lib/video-buffers.h
new file mode 100644
index 0000000..001f75e
--- /dev/null
+++ b/lib/video-buffers.h
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Video buffers
+ *
+ * Copyright (C) 2018 Laurent Pinchart
+ *
+ * Contact: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+ */
+#ifndef __VIDEO_BUFFERS_H__
+#define __VIDEO_BUFFERS_H__
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <sys/time.h>
+
+/*
+ *
+ * struct video_buffer - Video buffer information
+ * @index: Zero-based buffer index, limited to the number of buffers minus one
+ * @size: Size of the video memory, in bytes
+ * @bytesused: Number of bytes used by video data, smaller or equal to @size
+ * @timestamp: Time stamp at which the buffer has been captured
+ * @error: True if an error occured while capturing video data for the buffer
+ * @allocated: True if memory for the buffer has been allocated
+ * @mem: Video data memory
+ * @dmabuf: Video data dmabuf handle
+ */
+struct video_buffer
+{
+	unsigned int index;
+	unsigned int size;
+	unsigned int bytesused;
+	struct timeval timestamp;
+	bool error;
+	void *mem;
+	int dmabuf;
+};
+
+struct video_buffer_set
+{
+	struct video_buffer *buffers;
+	unsigned int nbufs;
+};
+
+struct video_buffer_set *video_buffer_set_new(unsigned int nbufs);
+void video_buffer_set_delete(struct video_buffer_set *buffers);
+
+#endif /* __VIDEO_BUFFERS_H__ */
diff --git a/lib/video-source.c b/lib/video-source.c
new file mode 100644
index 0000000..06092f5
--- /dev/null
+++ b/lib/video-source.c
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Abstract video source
+ *
+ * Copyright (C) 2018 Laurent Pinchart
+ *
+ * Contact: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+ */
+
+#include "video-source.h"
+
+void video_source_set_buffer_handler(struct video_source *src,
+				     video_source_buffer_handler_t handler,
+				     void *data)
+{
+	src->handler = handler;
+	src->handler_data = data;
+}
+
+void video_source_destroy(struct video_source *src)
+{
+	if (src)
+		src->ops->destroy(src);
+}
+
+int video_source_set_format(struct video_source *src,
+			    struct v4l2_pix_format *fmt)
+{
+	return src->ops->set_format(src, fmt);
+}
+
+int video_source_alloc_buffers(struct video_source *src, unsigned int nbufs)
+{
+	return src->ops->alloc_buffers(src, nbufs);
+}
+
+int video_source_export_buffers(struct video_source *src,
+				struct video_buffer_set **buffers)
+{
+	return src->ops->export_buffers(src, buffers);
+}
+
+int video_source_free_buffers(struct video_source *src)
+{
+	return src->ops->free_buffers(src);
+}
+
+int video_source_stream_on(struct video_source *src)
+{
+	return src->ops->stream_on(src);
+}
+
+int video_source_stream_off(struct video_source *src)
+{
+	return src->ops->stream_off(src);
+}
+
+int video_source_queue_buffer(struct video_source *src,
+			      struct video_buffer *buf)
+{
+	return src->ops->queue_buffer(src, buf);
+}
-- 
cgit v1.2.3