diff --git a/include/libcamera/buffer.h b/include/libcamera/buffer.h
index 8f9b42e39339..7da9a4263fd3 100644
--- a/include/libcamera/buffer.h
+++ b/include/libcamera/buffer.h
@@ -59,7 +59,7 @@ private:
 	friend class BufferPool;
 	friend class PipelineHandler;
 	friend class Request;
-	friend class V4L2Device;
+	friend class V4L2Videodev;

 	void cancel();

diff --git a/src/libcamera/camera_sensor.cpp b/src/libcamera/camera_sensor.cpp
index cb6649efac3f..83f121ec3514 100644
--- a/src/libcamera/camera_sensor.cpp
+++ b/src/libcamera/camera_sensor.cpp
@@ -14,7 +14,7 @@
 #include <math.h>

 #include "formats.h"
-#include "v4l2_subdevice.h"
+#include "v4l2_subdev.h"

 /**
  * \file camera_sensor.h
@@ -48,7 +48,7 @@ LOG_DEFINE_CATEGORY(CameraSensor);
 CameraSensor::CameraSensor(const MediaEntity *entity)
 	: entity_(entity)
 {
-	subdev_ = new V4L2Subdevice(entity);
+	subdev_ = new V4L2Subdev(entity);
 }

 /**
diff --git a/src/libcamera/include/camera_sensor.h b/src/libcamera/include/camera_sensor.h
index b823480241a7..2dd311652aa5 100644
--- a/src/libcamera/include/camera_sensor.h
+++ b/src/libcamera/include/camera_sensor.h
@@ -17,7 +17,7 @@
 namespace libcamera {

 class MediaEntity;
-class V4L2Subdevice;
+class V4L2Subdev;

 struct V4L2SubdeviceFormat;

@@ -46,7 +46,7 @@ protected:

 private:
 	const MediaEntity *entity_;
-	V4L2Subdevice *subdev_;
+	V4L2Subdev *subdev_;

 	std::vector<unsigned int> mbusCodes_;
 	std::vector<Size> sizes_;
diff --git a/src/libcamera/include/v4l2_device.h b/src/libcamera/include/v4l2_device.h
index bdecc087fe5a..e3f3ecf235d4 100644
--- a/src/libcamera/include/v4l2_device.h
+++ b/src/libcamera/include/v4l2_device.h
@@ -2,183 +2,37 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_device.h - V4L2 Device
+ * v4l2_device.h - Common base for V4L2 video devices and subdevices
  */
 #ifndef __LIBCAMERA_V4L2_DEVICE_H__
 #define __LIBCAMERA_V4L2_DEVICE_H__

-#include <atomic>
 #include <string>
-#include <vector>
-
-#include <linux/videodev2.h>
-
-#include <libcamera/geometry.h>
-#include <libcamera/signal.h>
-
-#include "log.h"

 namespace libcamera {

-class Buffer;
-class BufferPool;
-class EventNotifier;
-class MediaDevice;
-class MediaEntity;
-
-struct V4L2Capability final : v4l2_capability {
-	const char *driver() const
-	{
-		return reinterpret_cast<const char *>(v4l2_capability::driver);
-	}
-	const char *card() const
-	{
-		return reinterpret_cast<const char *>(v4l2_capability::card);
-	}
-	const char *bus_info() const
-	{
-		return reinterpret_cast<const char *>(v4l2_capability::bus_info);
-	}
-	unsigned int device_caps() const
-	{
-		return capabilities & V4L2_CAP_DEVICE_CAPS
-				    ? v4l2_capability::device_caps
-				    : v4l2_capability::capabilities;
-	}
-	bool isMultiplanar() const
-	{
-		return device_caps() & (V4L2_CAP_VIDEO_CAPTURE_MPLANE |
-					V4L2_CAP_VIDEO_OUTPUT_MPLANE);
-	}
-	bool isCapture() const
-	{
-		return device_caps() & (V4L2_CAP_VIDEO_CAPTURE |
-					V4L2_CAP_VIDEO_CAPTURE_MPLANE |
-					V4L2_CAP_META_CAPTURE);
-	}
-	bool isOutput() const
-	{
-		return device_caps() & (V4L2_CAP_VIDEO_OUTPUT |
-					V4L2_CAP_VIDEO_OUTPUT_MPLANE |
-					V4L2_CAP_META_OUTPUT);
-	}
-	bool isVideo() const
-	{
-		return device_caps() & (V4L2_CAP_VIDEO_CAPTURE |
-					V4L2_CAP_VIDEO_CAPTURE_MPLANE |
-					V4L2_CAP_VIDEO_OUTPUT |
-					V4L2_CAP_VIDEO_OUTPUT_MPLANE);
-	}
-	bool isMeta() const
-	{
-		return device_caps() & (V4L2_CAP_META_CAPTURE |
-					V4L2_CAP_META_OUTPUT);
-	}
-	bool isVideoCapture() const
-	{
-		return isVideo() && isCapture();
-	}
-	bool isVideoOutput() const
-	{
-		return isVideo() && isOutput();
-	}
-	bool isMetaCapture() const
-	{
-		return isMeta() && isCapture();
-	}
-	bool isMetaOutput() const
-	{
-		return isMeta() && isOutput();
-	}
-	bool hasStreaming() const
-	{
-		return device_caps() & V4L2_CAP_STREAMING;
-	}
-};
-
-class V4L2DeviceFormat
+class V4L2Device
 {
 public:
-	uint32_t fourcc;
-	Size size;
-
-	struct {
-		uint32_t size;
-		uint32_t bpl;
-	} planes[3];
-	unsigned int planesCount;
-
-	const std::string toString() const;
-};
+	virtual ~V4L2Device()
+	{
+	}

-class V4L2Device : protected Loggable
-{
-public:
-	explicit V4L2Device(const std::string &deviceNode);
-	explicit V4L2Device(const MediaEntity *entity);
-	V4L2Device(const V4L2Device &) = delete;
-	~V4L2Device();
+protected:
+	V4L2Device();

-	V4L2Device &operator=(const V4L2Device &) = delete;
+	int fd() { return fd_; }

-	int open();
+	int open(const std::string &pathname, unsigned int flags);
+	int close();
 	bool isOpen() const;
-	void close();
-
-	const char *driverName() const { return caps_.driver(); }
-	const char *deviceName() const { return caps_.card(); }
-	const char *busName() const { return caps_.bus_info(); }
-	const std::string &deviceNode() const { return deviceNode_; }

-	int getFormat(V4L2DeviceFormat *format);
-	int setFormat(V4L2DeviceFormat *format);
-
-	int exportBuffers(BufferPool *pool);
-	int importBuffers(BufferPool *pool);
-	int releaseBuffers();
-
-	int queueBuffer(Buffer *buffer);
-	Signal<Buffer *> bufferReady;
-
-	int streamOn();
-	int streamOff();
-
-	static V4L2Device *fromEntityName(const MediaDevice *media,
-					  const std::string &entity);
-
-protected:
-	std::string logPrefix() const;
+	int ioctl(unsigned long request, void *argp);

 private:
-	int getFormatMeta(V4L2DeviceFormat *format);
-	int setFormatMeta(V4L2DeviceFormat *format);
-
-	int getFormatMultiplane(V4L2DeviceFormat *format);
-	int setFormatMultiplane(V4L2DeviceFormat *format);
-
-	int getFormatSingleplane(V4L2DeviceFormat *format);
-	int setFormatSingleplane(V4L2DeviceFormat *format);
-
-	int requestBuffers(unsigned int count);
-	int createPlane(Buffer *buffer, unsigned int plane,
-			unsigned int length);
-
-	Buffer *dequeueBuffer();
-	void bufferAvailable(EventNotifier *notifier);
-
-	std::string deviceNode_;
 	int fd_;
-	V4L2Capability caps_;
-
-	enum v4l2_buf_type bufferType_;
-	enum v4l2_memory memoryType_;
-
-	BufferPool *bufferPool_;
-	std::atomic<unsigned int> queuedBuffersCount_;
-
-	EventNotifier *fdEvent_;
 };

-} /* namespace libcamera */
+}; /* namespace libcamera */

 #endif /* __LIBCAMERA_V4L2_DEVICE_H__ */
diff --git a/src/libcamera/include/v4l2_subdevice.h b/src/libcamera/include/v4l2_subdev.h
similarity index 67%
rename from src/libcamera/include/v4l2_subdevice.h
rename to src/libcamera/include/v4l2_subdev.h
index 3e4e5107aebe..9770f05deae4 100644
--- a/src/libcamera/include/v4l2_subdevice.h
+++ b/src/libcamera/include/v4l2_subdev.h
@@ -2,10 +2,10 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_subdevice.h - V4L2 Subdevice
+ * v4l2_subdev.h - V4L2 Subdevice
  */
-#ifndef __LIBCAMERA_V4L2_SUBDEVICE_H__
-#define __LIBCAMERA_V4L2_SUBDEVICE_H__
+#ifndef __LIBCAMERA_V4L2_SUBDEV_H__
+#define __LIBCAMERA_V4L2_SUBDEV_H__

 #include <map>
 #include <string>
@@ -16,6 +16,7 @@
 #include "formats.h"
 #include "log.h"
 #include "media_object.h"
+#include "v4l2_device.h"

 namespace libcamera {

@@ -28,16 +29,15 @@ struct V4L2SubdeviceFormat {
 	const std::string toString() const;
 };

-class V4L2Subdevice : protected Loggable
+class V4L2Subdev : public V4L2Device, protected Loggable
 {
 public:
-	explicit V4L2Subdevice(const MediaEntity *entity);
-	V4L2Subdevice(const V4L2Subdevice &) = delete;
-	V4L2Subdevice &operator=(const V4L2Subdevice &) = delete;
-	~V4L2Subdevice();
+	explicit V4L2Subdev(const MediaEntity *entity);
+	V4L2Subdev(const V4L2Subdev &) = delete;
+	V4L2Subdev &operator=(const V4L2Subdev &) = delete;
+	~V4L2Subdev();

 	int open();
-	bool isOpen() const;
 	void close();

 	const MediaEntity *entity() const { return entity_; }
@@ -50,8 +50,8 @@ public:
 	int getFormat(unsigned int pad, V4L2SubdeviceFormat *format);
 	int setFormat(unsigned int pad, V4L2SubdeviceFormat *format);

-	static V4L2Subdevice *fromEntityName(const MediaDevice *media,
-					     const std::string &entity);
+	static V4L2Subdev *fromEntityName(const MediaDevice *media,
+					  const std::string &entity);

 protected:
 	std::string logPrefix() const;
@@ -64,9 +64,8 @@ private:
 			 Rectangle *rect);

 	const MediaEntity *entity_;
-	int fd_;
 };

 } /* namespace libcamera */

-#endif /* __LIBCAMERA_V4L2_SUBDEVICE_H__ */
+#endif /* __LIBCAMERA_V4L2_SUBDEV_H__ */
diff --git a/src/libcamera/include/v4l2_videodev.h b/src/libcamera/include/v4l2_videodev.h
new file mode 100644
index 000000000000..c072f1fa5449
--- /dev/null
+++ b/src/libcamera/include/v4l2_videodev.h
@@ -0,0 +1,183 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * v4l2_videodev.h - V4L2 Video Device
+ */
+#ifndef __LIBCAMERA_V4L2_VIDEODEV_H__
+#define __LIBCAMERA_V4L2_VIDEODEV_H__
+
+#include <atomic>
+#include <string>
+#include <vector>
+
+#include <linux/videodev2.h>
+
+#include <libcamera/geometry.h>
+#include <libcamera/signal.h>
+
+#include "log.h"
+#include "v4l2_device.h"
+
+namespace libcamera {
+
+class Buffer;
+class BufferPool;
+class EventNotifier;
+class MediaDevice;
+class MediaEntity;
+
+struct V4L2Capability final : v4l2_capability {
+	const char *driver() const
+	{
+		return reinterpret_cast<const char *>(v4l2_capability::driver);
+	}
+	const char *card() const
+	{
+		return reinterpret_cast<const char *>(v4l2_capability::card);
+	}
+	const char *bus_info() const
+	{
+		return reinterpret_cast<const char *>(v4l2_capability::bus_info);
+	}
+	unsigned int device_caps() const
+	{
+		return capabilities & V4L2_CAP_DEVICE_CAPS
+				    ? v4l2_capability::device_caps
+				    : v4l2_capability::capabilities;
+	}
+	bool isMultiplanar() const
+	{
+		return device_caps() & (V4L2_CAP_VIDEO_CAPTURE_MPLANE |
+					V4L2_CAP_VIDEO_OUTPUT_MPLANE);
+	}
+	bool isCapture() const
+	{
+		return device_caps() & (V4L2_CAP_VIDEO_CAPTURE |
+					V4L2_CAP_VIDEO_CAPTURE_MPLANE |
+					V4L2_CAP_META_CAPTURE);
+	}
+	bool isOutput() const
+	{
+		return device_caps() & (V4L2_CAP_VIDEO_OUTPUT |
+					V4L2_CAP_VIDEO_OUTPUT_MPLANE |
+					V4L2_CAP_META_OUTPUT);
+	}
+	bool isVideo() const
+	{
+		return device_caps() & (V4L2_CAP_VIDEO_CAPTURE |
+					V4L2_CAP_VIDEO_CAPTURE_MPLANE |
+					V4L2_CAP_VIDEO_OUTPUT |
+					V4L2_CAP_VIDEO_OUTPUT_MPLANE);
+	}
+	bool isMeta() const
+	{
+		return device_caps() & (V4L2_CAP_META_CAPTURE |
+					V4L2_CAP_META_OUTPUT);
+	}
+	bool isVideoCapture() const
+	{
+		return isVideo() && isCapture();
+	}
+	bool isVideoOutput() const
+	{
+		return isVideo() && isOutput();
+	}
+	bool isMetaCapture() const
+	{
+		return isMeta() && isCapture();
+	}
+	bool isMetaOutput() const
+	{
+		return isMeta() && isOutput();
+	}
+	bool hasStreaming() const
+	{
+		return device_caps() & V4L2_CAP_STREAMING;
+	}
+};
+
+class V4L2DeviceFormat
+{
+public:
+	uint32_t fourcc;
+	Size size;
+
+	struct {
+		uint32_t size;
+		uint32_t bpl;
+	} planes[3];
+	unsigned int planesCount;
+
+	const std::string toString() const;
+};
+
+class V4L2Videodev : public V4L2Device, protected Loggable
+{
+public:
+	explicit V4L2Videodev(const std::string &deviceNode);
+	explicit V4L2Videodev(const MediaEntity *entity);
+	V4L2Videodev(const V4L2Videodev &) = delete;
+	~V4L2Videodev();
+
+	V4L2Videodev &operator=(const V4L2Videodev &) = delete;
+
+	int open();
+	void close();
+
+	const char *driverName() const { return caps_.driver(); }
+	const char *deviceName() const { return caps_.card(); }
+	const char *busName() const { return caps_.bus_info(); }
+	const std::string &deviceNode() const { return deviceNode_; }
+
+	int getFormat(V4L2DeviceFormat *format);
+	int setFormat(V4L2DeviceFormat *format);
+
+	int exportBuffers(BufferPool *pool);
+	int importBuffers(BufferPool *pool);
+	int releaseBuffers();
+
+	int queueBuffer(Buffer *buffer);
+	Signal<Buffer *> bufferReady;
+
+	int streamOn();
+	int streamOff();
+
+	static V4L2Videodev *fromEntityName(const MediaDevice *media,
+					    const std::string &entity);
+
+protected:
+	std::string logPrefix() const;
+
+private:
+	int getFormatMeta(V4L2DeviceFormat *format);
+	int setFormatMeta(V4L2DeviceFormat *format);
+
+	int getFormatMultiplane(V4L2DeviceFormat *format);
+	int setFormatMultiplane(V4L2DeviceFormat *format);
+
+	int getFormatSingleplane(V4L2DeviceFormat *format);
+	int setFormatSingleplane(V4L2DeviceFormat *format);
+
+	int requestBuffers(unsigned int count);
+	int createPlane(Buffer *buffer, unsigned int plane,
+			unsigned int length);
+
+	Buffer *dequeueBuffer();
+	void bufferAvailable(EventNotifier *notifier);
+
+	std::string deviceNode_;
+	V4L2Capability caps_;
+
+	enum v4l2_buf_type bufferType_;
+	enum v4l2_memory memoryType_;
+
+	BufferPool *bufferPool_;
+	std::atomic<unsigned int> queuedBuffersCount_;
+
+	EventNotifier *fdEvent_;
+};
+
+} /* namespace libcamera */
+
+#endif /* __LIBCAMERA_V4L2_VIDEODEV_H__ */
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index 1ca1083cf5c7..f7a48fcb43df 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -24,7 +24,8 @@ libcamera_sources = files([
     'timer.cpp',
     'utils.cpp',
     'v4l2_device.cpp',
-    'v4l2_subdevice.cpp',
+    'v4l2_subdev.cpp',
+    'v4l2_videodev.cpp',
 ])

 libcamera_headers = files([
@@ -42,7 +43,8 @@ libcamera_headers = files([
     'include/pipeline_handler.h',
     'include/utils.h',
     'include/v4l2_device.h',
-    'include/v4l2_subdevice.h',
+    'include/v4l2_subdev.h',
+    'include/v4l2_videodev.h',
 ])

 libcamera_internal_includes =  include_directories('include')
diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
index f2bdecbac20f..c4229453afe8 100644
--- a/src/libcamera/pipeline/ipu3/ipu3.cpp
+++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
@@ -22,8 +22,8 @@
 #include "media_device.h"
 #include "pipeline_handler.h"
 #include "utils.h"
-#include "v4l2_device.h"
-#include "v4l2_subdevice.h"
+#include "v4l2_subdev.h"
+#include "v4l2_videodev.h"

 namespace libcamera {

@@ -39,7 +39,7 @@ public:

 	/* ImgU output descriptor: group data specific to an ImgU output. */
 	struct ImgUOutput {
-		V4L2Device *dev;
+		V4L2Videodev *dev;
 		unsigned int pad;
 		std::string name;
 		BufferPool *pool;
@@ -84,8 +84,8 @@ public:
 	std::string name_;
 	MediaDevice *media_;

-	V4L2Subdevice *imgu_;
-	V4L2Device *input_;
+	V4L2Subdev *imgu_;
+	V4L2Videodev *input_;
 	ImgUOutput output_;
 	ImgUOutput viewfinder_;
 	ImgUOutput stat_;
@@ -125,8 +125,8 @@ public:

 	static int mediaBusToFormat(unsigned int code);

-	V4L2Device *output_;
-	V4L2Subdevice *csi2_;
+	V4L2Videodev *output_;
+	V4L2Subdev *csi2_;
 	CameraSensor *sensor_;

 	BufferPool pool_;
@@ -943,7 +943,7 @@ void IPU3CameraData::cio2BufferReady(Buffer *buffer)
  * Create and open the V4L2 devices and subdevices of the ImgU instance
  * with \a index.
  *
- * In case of errors the created V4L2Device and V4L2Subdevice instances
+ * In case of errors the created V4L2Videodev and V4L2Subdev instances
  * are destroyed at pipeline handler delete time.
  *
  * \return 0 on success or a negative error code otherwise
@@ -961,17 +961,17 @@ int ImgUDevice::init(MediaDevice *media, unsigned int index)
 	 * by the match() function: no need to check for newly created
 	 * video devices and subdevice validity here.
 	 */
-	imgu_ = V4L2Subdevice::fromEntityName(media, name_);
+	imgu_ = V4L2Subdev::fromEntityName(media, name_);
 	ret = imgu_->open();
 	if (ret)
 		return ret;

-	input_ = V4L2Device::fromEntityName(media, name_ + " input");
+	input_ = V4L2Videodev::fromEntityName(media, name_ + " input");
 	ret = input_->open();
 	if (ret)
 		return ret;

-	output_.dev = V4L2Device::fromEntityName(media, name_ + " output");
+	output_.dev = V4L2Videodev::fromEntityName(media, name_ + " output");
 	ret = output_.dev->open();
 	if (ret)
 		return ret;
@@ -980,7 +980,7 @@ int ImgUDevice::init(MediaDevice *media, unsigned int index)
 	output_.name = "output";
 	output_.pool = &outPool_;

-	viewfinder_.dev = V4L2Device::fromEntityName(media,
+	viewfinder_.dev = V4L2Videodev::fromEntityName(media,
 						     name_ + " viewfinder");
 	ret = viewfinder_.dev->open();
 	if (ret)
@@ -990,7 +990,7 @@ int ImgUDevice::init(MediaDevice *media, unsigned int index)
 	viewfinder_.name = "viewfinder";
 	viewfinder_.pool = &vfPool_;

-	stat_.dev = V4L2Device::fromEntityName(media, name_ + " 3a stat");
+	stat_.dev = V4L2Videodev::fromEntityName(media, name_ + " 3a stat");
 	ret = stat_.dev->open();
 	if (ret)
 		return ret;
@@ -1067,7 +1067,7 @@ int ImgUDevice::configureInput(const Size &size,
 int ImgUDevice::configureOutput(ImgUOutput *output,
 				const StreamConfiguration &cfg)
 {
-	V4L2Device *dev = output->dev;
+	V4L2Videodev *dev = output->dev;
 	unsigned int pad = output->pad;

 	V4L2SubdeviceFormat imguFormat = {};
@@ -1331,13 +1331,13 @@ int CIO2Device::init(const MediaDevice *media, unsigned int index)
 	 * might impact on power consumption.
 	 */

-	csi2_ = new V4L2Subdevice(csi2Entity);
+	csi2_ = new V4L2Subdev(csi2Entity);
 	ret = csi2_->open();
 	if (ret)
 		return ret;

 	std::string cio2Name = "ipu3-cio2 " + std::to_string(index);
-	output_ = V4L2Device::fromEntityName(media, cio2Name);
+	output_ = V4L2Videodev::fromEntityName(media, cio2Name);
 	ret = output_->open();
 	if (ret)
 		return ret;
diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
index 9b3eea2f6dd3..08b640a1bd49 100644
--- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp
+++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
@@ -23,8 +23,8 @@
 #include "media_device.h"
 #include "pipeline_handler.h"
 #include "utils.h"
-#include "v4l2_device.h"
-#include "v4l2_subdevice.h"
+#include "v4l2_subdev.h"
+#include "v4l2_videodev.h"

 namespace libcamera {

@@ -104,9 +104,9 @@ private:
 	void bufferReady(Buffer *buffer);

 	MediaDevice *media_;
-	V4L2Subdevice *dphy_;
-	V4L2Subdevice *isp_;
-	V4L2Device *video_;
+	V4L2Subdev *dphy_;
+	V4L2Subdev *isp_;
+	V4L2Videodev *video_;

 	Camera *activeCamera_;
 };
@@ -449,16 +449,16 @@ bool PipelineHandlerRkISP1::match(DeviceEnumerator *enumerator)
 		return false;

 	/* Create the V4L2 subdevices we will need. */
-	dphy_ = V4L2Subdevice::fromEntityName(media_, "rockchip-sy-mipi-dphy");
+	dphy_ = V4L2Subdev::fromEntityName(media_, "rockchip-sy-mipi-dphy");
 	if (dphy_->open() < 0)
 		return false;

-	isp_ = V4L2Subdevice::fromEntityName(media_, "rkisp1-isp-subdev");
+	isp_ = V4L2Subdev::fromEntityName(media_, "rkisp1-isp-subdev");
 	if (isp_->open() < 0)
 		return false;

 	/* Locate and open the capture video node. */
-	video_ = V4L2Device::fromEntityName(media_, "rkisp1_mainpath");
+	video_ = V4L2Videodev::fromEntityName(media_, "rkisp1_mainpath");
 	if (video_->open() < 0)
 		return false;

diff --git a/src/libcamera/pipeline/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo.cpp
index 45260f34c8f5..fa3c5a2f8374 100644
--- a/src/libcamera/pipeline/uvcvideo.cpp
+++ b/src/libcamera/pipeline/uvcvideo.cpp
@@ -14,7 +14,7 @@
 #include "media_device.h"
 #include "pipeline_handler.h"
 #include "utils.h"
-#include "v4l2_device.h"
+#include "v4l2_videodev.h"

 namespace libcamera {

@@ -35,7 +35,7 @@ public:

 	void bufferReady(Buffer *buffer);

-	V4L2Device *video_;
+	V4L2Videodev *video_;
 	Stream stream_;
 };

@@ -229,7 +229,7 @@ bool PipelineHandlerUVC::match(DeviceEnumerator *enumerator)
 	/* Locate and open the default video node. */
 	for (MediaEntity *entity : media->entities()) {
 		if (entity->flags() & MEDIA_ENT_FL_DEFAULT) {
-			data->video_ = new V4L2Device(entity);
+			data->video_ = new V4L2Videodev(entity);
 			break;
 		}
 	}
diff --git a/src/libcamera/pipeline/vimc.cpp b/src/libcamera/pipeline/vimc.cpp
index ad4577acd9d6..e1fa3eabeae0 100644
--- a/src/libcamera/pipeline/vimc.cpp
+++ b/src/libcamera/pipeline/vimc.cpp
@@ -20,7 +20,7 @@
 #include "media_device.h"
 #include "pipeline_handler.h"
 #include "utils.h"
-#include "v4l2_device.h"
+#include "v4l2_videodev.h"

 namespace libcamera {

@@ -41,7 +41,7 @@ public:

 	void bufferReady(Buffer *buffer);

-	V4L2Device *video_;
+	V4L2Videodev *video_;
 	Stream stream_;
 };

@@ -262,7 +262,7 @@ bool PipelineHandlerVimc::match(DeviceEnumerator *enumerator)
 	std::unique_ptr<VimcCameraData> data = utils::make_unique<VimcCameraData>(this);

 	/* Locate and open the capture video node. */
-	data->video_ = new V4L2Device(media->getEntityByName("Raw Capture 1"));
+	data->video_ = new V4L2Videodev(media->getEntityByName("Raw Capture 1"));
 	if (data->video_->open())
 		return false;

diff --git a/src/libcamera/v4l2_device.cpp b/src/libcamera/v4l2_device.cpp
index 0821bd75fb42..c20dc7928fcd 100644
--- a/src/libcamera/v4l2_device.cpp
+++ b/src/libcamera/v4l2_device.cpp
@@ -2,1011 +2,150 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_device.cpp - V4L2 Device
+ * v4l2_device.cpp - Common base for V4L2 devices and subdevices
  */

 #include "v4l2_device.h"

 #include <fcntl.h>
-#include <iomanip>
-#include <sstream>
 #include <string.h>
 #include <sys/ioctl.h>
-#include <sys/mman.h>
-#include <sys/time.h>
 #include <unistd.h>
-#include <vector>
-
-#include <libcamera/buffer.h>
-#include <libcamera/event_notifier.h>

 #include "log.h"
-#include "media_device.h"
-#include "media_object.h"

 /**
  * \file v4l2_device.h
- * \brief V4L2 Device API
+ * \brief Common base for V4L2 devices and subdevices
  */
+
 namespace libcamera {

 LOG_DEFINE_CATEGORY(V4L2)

 /**
- * \struct V4L2Capability
- * \brief struct v4l2_capability object wrapper and helpers
- *
- * The V4L2Capability structure manages the information returned by the
- * VIDIOC_QUERYCAP ioctl.
- */
-
-/**
- * \fn V4L2Capability::driver()
- * \brief Retrieve the driver module name
- * \return The string containing the name of the driver module
- */
-
-/**
- * \fn V4L2Capability::card()
- * \brief Retrieve the device card name
- * \return The string containing the device name
- */
-
-/**
- * \fn V4L2Capability::bus_info()
- * \brief Retrieve the location of the device in the system
- * \return The string containing the device location
- */
-
-/**
- * \fn V4L2Capability::device_caps()
- * \brief Retrieve the capabilities of the device
- * \return The device specific capabilities if V4L2_CAP_DEVICE_CAPS is set or
- * 	   driver capabilities otherwise
- */
-
-/**
- * \fn V4L2Capability::isMultiplanar()
- * \brief Identify if the device implements the V4L2 multiplanar APIs
- * \return True if the device supports multiplanar APIs
- */
-
-/**
- * \fn V4L2Capability::isCapture()
- * \brief Identify if the device captures data
- * \return True if the device can capture data
- */
-
-/**
- * \fn V4L2Capability::isOutput()
- * \brief Identify if the device outputs data
- * \return True if the device can output data
- */
-
-/**
- * \fn V4L2Capability::isVideo()
- * \brief Identify if the device captures or outputs images
- * \return True if the device can capture or output images
- */
-
-/**
- * \fn V4L2Capability::isMeta()
- * \brief Identify if the device captures or outputs image meta-data
- *
- * \todo Add support for META_CAPTURE introduced in Linux v5.0
- *
- * \return True if the device can capture or output image meta-data
- */
-
-/**
- * \fn V4L2Capability::isVideoCapture()
- * \brief Identify if the device captures images
- * \return True if the device can capture images
- */
-
-/**
- * \fn V4L2Capability::isVideoOutput()
- * \brief Identify if the device outputs images
- * \return True if the device can output images
- */
-
-/**
- * \fn V4L2Capability::isMetaCapture()
- * \brief Identify if the device captures image meta-data
- * \return True if the device can capture image meta-data
- */
-
-/**
- * \fn V4L2Capability::isMetaOutput()
- * \brief Identify if the device outputs image meta-data
- * \return True if the device can output image meta-data
- */
-
-/**
- * \fn V4L2Capability::hasStreaming()
- * \brief Determine if the device can perform Streaming I/O
- * \return True if the device provides Streaming I/O IOCTLs
- */
-
-/**
- * \class V4L2DeviceFormat
- * \brief The V4L2 device image format and sizes
- *
- * This class describes the image format and resolution to be programmed on a
- * V4L2 video device. The image format is defined by a fourcc code (as specified
- * by the V4L2 API with the V4L2_PIX_FMT_* macros), a resolution (width and
- * height) and one to three planes with configurable line stride and a total
- * per-plane size in bytes.
- *
- * Image formats, as defined by the V4L2 APIs, are categorised as packed,
- * semi-planar and planar, and describe the layout of the image pixel components
- * stored in memory.
- *
- * Packed image formats store pixel components one after the other, in a
- * contiguous memory area. Examples of packed image formats are YUYV
- * permutations, RGB with different pixel sub-sampling ratios such as RGB565 or
- * RGB666 or Raw-Bayer formats such as SRGGB8 or SGRBG12.
- *
- * Semi-planar and planar image formats store the pixel components in separate
- * and possibly non-contiguous memory areas, named planes, whose sizes depend on
- * the pixel components sub-sampling ratios, which are defined by the format.
- * Semi-planar formats use two planes to store pixel components and notable
- * examples of such formats are the NV12 and NV16 formats, while planar formats
- * use three planes to store pixel components and notable examples are YUV422
- * and YUV420.
- *
- * Image formats supported by the V4L2 API are defined and described in Section
- * number 2 of the "Part I - Video for Linux API" chapter of the "Linux Media
- * Infrastructure userspace API", part of the Linux kernel documentation.
- *
- * In the context of this document, packed image formats are referred to as
- * "packed formats" and semi-planar and planar image formats are referred to as
- * "planar formats".
- *
- * V4L2 also defines two different sets of APIs to work with devices that store
- * planes in contiguous or separate memory areas. They are named "Single-plane
- * APIs" and "Multi-plane APIs" respectively and are documented in Section 2.1
- * and Section 2.2 of the above mentioned "Part I - Video for Linux API"
- * documentation.
- *
- * The single-plane API allows, among other parameters, the configuration of the
- * image resolution, the pixel format and the stride length. In that case the
- * stride applies to all planes (possibly sub-sampled). The multi-plane API
- * allows configuring the resolution, the pixel format and a per-plane stride
- * length and total size.
+ * \class V4L2Device
+ * \brief Base class for V4L2Videodev and V4L2Subdev
  *
- * Packed image formats, which occupy a single memory area, are easily described
- * through the single-plane API. When used on a device that implements the
- * multi-plane API, only the size and stride information contained in the first
- * plane are taken into account.
+ * The V4L2Device class groups together the methods and fields common to
+ * both V4L2Videodev and V4L2Subdev, and provide a base class which
+ * provides methods to open and close the device node associated with the
+ * device and to perform IOCTL system calls on it.
  *
- * Planar image formats, which occupy distinct memory areas, are easily
- * described through the multi-plane APIs. When used on a device that implements
- * the single-plane API, all planes are stored one after the other in a
- * contiguous memory area, and it is not possible to configure per-plane stride
- * length and size, but only a global stride length which is applied to all
- * planes.
+ * The V4L2Device class cannot be instantiated directly, as its constructor
+ * is protected. Users should use instead one the derived classes to model
+ * either a V4L2 video device or a V4L2 subdevice.
  *
- * The V4L2DeviceFormat class describes both packed and planar image formats,
- * regardless of the API type (single or multi plane) implemented by the device
- * the format has to be applied to. The total size and bytes per line of images
- * represented with packed formats are configured using the first entry of the
- * V4L2DeviceFormat::planes array, while the per-plane size and per-plane stride
- * length of images represented with planar image formats are configured using
- * the opportune number of entries of the V4L2DeviceFormat::planes array, as
- * prescribed by the image format definition (semi-planar formats use 2 entries,
- * while planar formats use the whole 3 entries). The number of valid entries of
- * the V4L2DeviceFormat::planes array is defined by the
- * V4L2DeviceFormat::planesCount value.
- */
-
-/**
- * \var V4L2DeviceFormat::size
- * \brief The image size in pixels
+ * Methods common to V4L2Videodev and V4L2Subdev, such as V4L2 controls
+ * support are implemented in the base class to maximize code re-use.
  */

 /**
- * \var V4L2DeviceFormat::fourcc
- * \brief The fourcc code describing the pixel encoding scheme
+ * \brief Construct a V4L2Device
  *
- * The fourcc code, as defined by the V4L2 API with the V4L2_PIX_FMT_* macros,
- * that identifies the image format pixel encoding scheme.
- */
-
-/**
- * \var V4L2DeviceFormat::planes
- * \brief The per-plane memory size information
+ * The V4L2Device constructor is protected and can only be accessed by the
+ * V4L2Videodev and V4L2Subdev derived classes.
  *
- * Images are stored in memory in one or more data planes. Each data plane has a
- * specific line stride and memory size, which could differ from the image
- * visible sizes to accommodate padding at the end of lines and end of planes.
- * Only the first \ref planesCount entries are considered valid.
- */
-
-/**
- * \var V4L2DeviceFormat::planesCount
- * \brief The number of valid data planes
+ * Initialize the file descriptor to -1.
  */
-
-/**
- * \brief Assemble and return a string describing the format
- * \return A string describing the V4L2DeviceFormat
- */
-const std::string V4L2DeviceFormat::toString() const
+V4L2Device::V4L2Device() :
+	fd_(-1)
 {
-	std::stringstream ss;
-
-	ss.fill(0);
-	ss << size.toString() << "-0x" << std::hex << std::setw(8) << fourcc;
-
-	return ss.str();
 }

 /**
- * \class V4L2Device
- * \brief V4L2Device object and API
- *
- * The V4L2 Device API class models an instance of a V4L2 device node.
- * It is constructed with the path to a V4L2 video device node. The device node
- * is only opened upon a call to open() which must be checked for success.
- *
- * The device capabilities are validated when the device is opened and the
- * device is rejected if it is not a suitable V4L2 capture or output device, or
- * if the device does not support streaming I/O.
- *
- * No API call other than open(), isOpen() and close() shall be called on an
- * unopened device instance.
- *
- * The V4L2Device class tracks queued buffers and handles buffer events. It
- * automatically dequeues completed buffers and emits the \ref bufferReady
- * signal.
- *
- * Upon destruction any device left open will be closed, and any resources
- * released.
- */
-
-/**
- * \brief Construct a V4L2Device
- * \param[in] deviceNode The file-system path to the video device node
+ * \fn V4L2Device::fd()
+ * \brief Retrieve the V4L2 device file descriptor
+ * \return The V4L2 device file descriptor, -1 if the device node is not open
  */
-V4L2Device::V4L2Device(const std::string &deviceNode)
-	: deviceNode_(deviceNode), fd_(-1), bufferPool_(nullptr),
-	  queuedBuffersCount_(0), fdEvent_(nullptr)
-{
-	/*
-	 * We default to an MMAP based CAPTURE device, however this will be
-	 * updated based upon the device capabilities.
-	 */
-	bufferType_ = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
-	memoryType_ = V4L2_MEMORY_MMAP;
-}

 /**
- * \brief Construct a V4L2Device from a MediaEntity
- * \param[in] entity The MediaEntity to build the device from
+ * \brief Open a V4L2 device node
+ * \param pathname The filesystem path of the device node to open
+ * \param flags Access mode flags
+ *
+ * Initialize the file descriptor, which was initially set to -1.
  *
- * Construct a V4L2Device from a MediaEntity's device node path.
- */
-V4L2Device::V4L2Device(const MediaEntity *entity)
-	: V4L2Device(entity->deviceNode())
-{
-}
-
-V4L2Device::~V4L2Device()
-{
-	close();
-}
-
-/**
- * \brief Open a V4L2 device and query its capabilities
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Device::open()
+int V4L2Device::open(const std::string &pathname, unsigned int flags)
 {
-	int ret;
-
 	if (isOpen()) {
 		LOG(V4L2, Error) << "Device already open";
 		return -EBUSY;
 	}

-	ret = ::open(deviceNode_.c_str(), O_RDWR | O_NONBLOCK);
+	int ret = ::open(pathname.c_str(), flags);
 	if (ret < 0) {
 		ret = -errno;
-		LOG(V4L2, Error)
-			<< "Failed to open V4L2 device: " << strerror(-ret);
+		LOG(V4L2, Error) << "Failed to open V4L2 device: "
+				 << strerror(-ret);
 		return ret;
 	}
-	fd_ = ret;
-
-	ret = ioctl(fd_, VIDIOC_QUERYCAP, &caps_);
-	if (ret < 0) {
-		ret = -errno;
-		LOG(V4L2, Error)
-			<< "Failed to query device capabilities: "
-			<< strerror(-ret);
-		return ret;
-	}
-
-	LOG(V4L2, Debug)
-		<< "Opened device " << caps_.bus_info() << ": "
-		<< caps_.driver() << ": " << caps_.card();
-
-	if (!caps_.hasStreaming()) {
-		LOG(V4L2, Error) << "Device does not support streaming I/O";
-		return -EINVAL;
-	}
-
-	/*
-	 * Set buffer type and wait for read notifications on CAPTURE devices
-	 * (POLLIN), and write notifications for OUTPUT devices (POLLOUT).
-	 */
-	if (caps_.isVideoCapture()) {
-		fdEvent_ = new EventNotifier(fd_, EventNotifier::Read);
-		bufferType_ = caps_.isMultiplanar()
-			    ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
-			    : V4L2_BUF_TYPE_VIDEO_CAPTURE;
-	} else if (caps_.isVideoOutput()) {
-		fdEvent_ = new EventNotifier(fd_, EventNotifier::Write);
-		bufferType_ = caps_.isMultiplanar()
-			    ? V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE
-			    : V4L2_BUF_TYPE_VIDEO_OUTPUT;
-	} else if (caps_.isMetaCapture()) {
-		fdEvent_ = new EventNotifier(fd_, EventNotifier::Read);
-		bufferType_ = V4L2_BUF_TYPE_META_CAPTURE;
-	} else if (caps_.isMetaOutput()) {
-		fdEvent_ = new EventNotifier(fd_, EventNotifier::Write);
-		bufferType_ = V4L2_BUF_TYPE_META_OUTPUT;
-	} else {
-		LOG(V4L2, Error) << "Device is not a supported type";
-		return -EINVAL;
-	}
-
-	fdEvent_->activated.connect(this, &V4L2Device::bufferAvailable);
-	fdEvent_->setEnabled(false);
-
-	return 0;
-}
-
-/**
- * \brief Check if device is successfully opened
- * \return True if the device is open, false otherwise
- */
-bool V4L2Device::isOpen() const
-{
-	return fd_ != -1;
-}
-
-/**
- * \brief Close the device, releasing any resources acquired by open()
- */
-void V4L2Device::close()
-{
-	if (fd_ < 0)
-		return;
-
-	releaseBuffers();
-	delete fdEvent_;
-
-	::close(fd_);
-	fd_ = -1;
-}
-
-/**
- * \fn V4L2Device::driverName()
- * \brief Retrieve the name of the V4L2 device driver
- * \return The string containing the driver name
- */
-
-/**
- * \fn V4L2Device::deviceName()
- * \brief Retrieve the name of the V4L2 device
- * \return The string containing the device name
- */
-
-/**
- * \fn V4L2Device::busName()
- * \brief Retrieve the location of the device in the system
- * \return The string containing the device location
- */
-
-/**
- * \fn V4L2Device::deviceNode()
- * \brief Retrieve the video device node path
- * \return The video device device node path
- */
-
-std::string V4L2Device::logPrefix() const
-{
-	return deviceNode_ + (V4L2_TYPE_IS_OUTPUT(bufferType_) ? "[out]" : "[cap]");
-}
-
-/**
- * \brief Retrieve the image format set on the V4L2 device
- * \param[out] format The image format applied on the device
- * \return 0 on success or a negative error code otherwise
- */
-int V4L2Device::getFormat(V4L2DeviceFormat *format)
-{
-	if (caps_.isMeta())
-		return getFormatMeta(format);
-	else if (caps_.isMultiplanar())
-		return getFormatMultiplane(format);
-	else
-		return getFormatSingleplane(format);
-}
-
-/**
- * \brief Configure an image format on the V4L2 device
- * \param[inout] format The image format to apply to the device
- *
- * Apply the supplied \a format to the device, and return the actually
- * applied format parameters, as \ref V4L2Device::getFormat would do.
- *
- * \return 0 on success or a negative error code otherwise
- */
-int V4L2Device::setFormat(V4L2DeviceFormat *format)
-{
-	if (caps_.isMeta())
-		return setFormatMeta(format);
-	else if (caps_.isMultiplanar())
-		return setFormatMultiplane(format);
-	else
-		return setFormatSingleplane(format);
-}
-
-int V4L2Device::getFormatMeta(V4L2DeviceFormat *format)
-{
-	struct v4l2_format v4l2Format = {};
-	struct v4l2_meta_format *pix = &v4l2Format.fmt.meta;
-	int ret;
-
-	v4l2Format.type = bufferType_;
-	ret = ioctl(fd_, VIDIOC_G_FMT, &v4l2Format);
-	if (ret) {
-		ret = -errno;
-		LOG(V4L2, Error) << "Unable to get format: " << strerror(-ret);
-		return ret;
-	}
-
-	format->size.width = 0;
-	format->size.height = 0;
-	format->fourcc = pix->dataformat;
-	format->planesCount = 1;
-	format->planes[0].bpl = pix->buffersize;
-	format->planes[0].size = pix->buffersize;
-
-	return 0;
-}
-
-int V4L2Device::setFormatMeta(V4L2DeviceFormat *format)
-{
-	struct v4l2_format v4l2Format = {};
-	struct v4l2_meta_format *pix = &v4l2Format.fmt.meta;
-	int ret;
-
-	v4l2Format.type = bufferType_;
-	pix->dataformat = format->fourcc;
-	pix->buffersize = format->planes[0].size;
-	ret = ioctl(fd_, VIDIOC_S_FMT, &v4l2Format);
-	if (ret) {
-		ret = -errno;
-		LOG(V4L2, Error) << "Unable to set format: " << strerror(-ret);
-		return ret;
-	}
-
-	/*
-	 * Return to caller the format actually applied on the device,
-	 * which might differ from the requested one.
-	 */
-	format->size.width = 0;
-	format->size.height = 0;
-	format->fourcc = format->fourcc;
-	format->planesCount = 1;
-	format->planes[0].bpl = pix->buffersize;
-	format->planes[0].size = pix->buffersize;
-
-	return 0;
-}

-int V4L2Device::getFormatMultiplane(V4L2DeviceFormat *format)
-{
-	struct v4l2_format v4l2Format = {};
-	struct v4l2_pix_format_mplane *pix = &v4l2Format.fmt.pix_mp;
-	int ret;
-
-	v4l2Format.type = bufferType_;
-	ret = ioctl(fd_, VIDIOC_G_FMT, &v4l2Format);
-	if (ret) {
-		ret = -errno;
-		LOG(V4L2, Error) << "Unable to get format: " << strerror(-ret);
-		return ret;
-	}
-
-	format->size.width = pix->width;
-	format->size.height = pix->height;
-	format->fourcc = pix->pixelformat;
-	format->planesCount = pix->num_planes;
-
-	for (unsigned int i = 0; i < format->planesCount; ++i) {
-		format->planes[i].bpl = pix->plane_fmt[i].bytesperline;
-		format->planes[i].size = pix->plane_fmt[i].sizeimage;
-	}
-
-	return 0;
-}
-
-int V4L2Device::setFormatMultiplane(V4L2DeviceFormat *format)
-{
-	struct v4l2_format v4l2Format = {};
-	struct v4l2_pix_format_mplane *pix = &v4l2Format.fmt.pix_mp;
-	int ret;
-
-	v4l2Format.type = bufferType_;
-	pix->width = format->size.width;
-	pix->height = format->size.height;
-	pix->pixelformat = format->fourcc;
-	pix->num_planes = format->planesCount;
-	pix->field = V4L2_FIELD_NONE;
-
-	for (unsigned int i = 0; i < pix->num_planes; ++i) {
-		pix->plane_fmt[i].bytesperline = format->planes[i].bpl;
-		pix->plane_fmt[i].sizeimage = format->planes[i].size;
-	}
-
-	ret = ioctl(fd_, VIDIOC_S_FMT, &v4l2Format);
-	if (ret) {
-		ret = -errno;
-		LOG(V4L2, Error) << "Unable to set format: " << strerror(-ret);
-		return ret;
-	}
-
-	/*
-	 * Return to caller the format actually applied on the device,
-	 * which might differ from the requested one.
-	 */
-	format->size.width = pix->width;
-	format->size.height = pix->height;
-	format->fourcc = pix->pixelformat;
-	format->planesCount = pix->num_planes;
-	for (unsigned int i = 0; i < format->planesCount; ++i) {
-		format->planes[i].bpl = pix->plane_fmt[i].bytesperline;
-		format->planes[i].size = pix->plane_fmt[i].sizeimage;
-	}
-
-	return 0;
-}
-
-int V4L2Device::getFormatSingleplane(V4L2DeviceFormat *format)
-{
-	struct v4l2_format v4l2Format = {};
-	struct v4l2_pix_format *pix = &v4l2Format.fmt.pix;
-	int ret;
-
-	v4l2Format.type = bufferType_;
-	ret = ioctl(fd_, VIDIOC_G_FMT, &v4l2Format);
-	if (ret) {
-		ret = -errno;
-		LOG(V4L2, Error) << "Unable to get format: " << strerror(-ret);
-		return ret;
-	}
-
-	format->size.width = pix->width;
-	format->size.height = pix->height;
-	format->fourcc = pix->pixelformat;
-	format->planesCount = 1;
-	format->planes[0].bpl = pix->bytesperline;
-	format->planes[0].size = pix->sizeimage;
-
-	return 0;
-}
-
-int V4L2Device::setFormatSingleplane(V4L2DeviceFormat *format)
-{
-	struct v4l2_format v4l2Format = {};
-	struct v4l2_pix_format *pix = &v4l2Format.fmt.pix;
-	int ret;
-
-	v4l2Format.type = bufferType_;
-	pix->width = format->size.width;
-	pix->height = format->size.height;
-	pix->pixelformat = format->fourcc;
-	pix->bytesperline = format->planes[0].bpl;
-	pix->field = V4L2_FIELD_NONE;
-	ret = ioctl(fd_, VIDIOC_S_FMT, &v4l2Format);
-	if (ret) {
-		ret = -errno;
-		LOG(V4L2, Error) << "Unable to set format: " << strerror(-ret);
-		return ret;
-	}
-
-	/*
-	 * Return to caller the format actually applied on the device,
-	 * which might differ from the requested one.
-	 */
-	format->size.width = pix->width;
-	format->size.height = pix->height;
-	format->fourcc = pix->pixelformat;
-	format->planesCount = 1;
-	format->planes[0].bpl = pix->bytesperline;
-	format->planes[0].size = pix->sizeimage;
-
-	return 0;
-}
-
-int V4L2Device::requestBuffers(unsigned int count)
-{
-	struct v4l2_requestbuffers rb = {};
-	int ret;
-
-	rb.count = count;
-	rb.type = bufferType_;
-	rb.memory = memoryType_;
-
-	ret = ioctl(fd_, VIDIOC_REQBUFS, &rb);
-	if (ret < 0) {
-		ret = -errno;
-		LOG(V4L2, Error)
-			<< "Unable to request " << count << " buffers: "
-			<< strerror(-ret);
-		return ret;
-	}
-
-	LOG(V4L2, Debug) << rb.count << " buffers requested.";
-
-	return rb.count;
-}
-
-/**
- * \brief Request buffers to be allocated from the device and stored in the
- *  buffer pool provided.
- * \param[out] pool BufferPool to populate with buffers
- * \return 0 on success or a negative error code otherwise
- */
-int V4L2Device::exportBuffers(BufferPool *pool)
-{
-	unsigned int allocatedBuffers;
-	unsigned int i;
-	int ret;
-
-	memoryType_ = V4L2_MEMORY_MMAP;
-
-	ret = requestBuffers(pool->count());
-	if (ret < 0)
-		return ret;
-
-	allocatedBuffers = ret;
-	if (allocatedBuffers < pool->count()) {
-		LOG(V4L2, Error) << "Not enough buffers provided by V4L2Device";
-		requestBuffers(0);
-		return -ENOMEM;
-	}
-
-	/* Map the buffers. */
-	for (i = 0; i < pool->count(); ++i) {
-		struct v4l2_plane planes[VIDEO_MAX_PLANES] = {};
-		struct v4l2_buffer buf = {};
-		Buffer &buffer = pool->buffers()[i];
-
-		buf.index = i;
-		buf.type = bufferType_;
-		buf.memory = memoryType_;
-		buf.length = VIDEO_MAX_PLANES;
-		buf.m.planes = planes;
-
-		ret = ioctl(fd_, VIDIOC_QUERYBUF, &buf);
-		if (ret < 0) {
-			ret = -errno;
-			LOG(V4L2, Error)
-				<< "Unable to query buffer " << i << ": "
-				<< strerror(-ret);
-			break;
-		}
-
-		if (V4L2_TYPE_IS_MULTIPLANAR(buf.type)) {
-			for (unsigned int p = 0; p < buf.length; ++p) {
-				ret = createPlane(&buffer, p,
-						  buf.m.planes[p].length);
-				if (ret)
-					break;
-			}
-		} else {
-			ret = createPlane(&buffer, 0, buf.length);
-		}
-
-		if (ret) {
-			LOG(V4L2, Error) << "Failed to create plane";
-			break;
-		}
-	}
-
-	if (ret) {
-		requestBuffers(0);
-		pool->destroyBuffers();
-		return ret;
-	}
-
-	bufferPool_ = pool;
-
-	return 0;
-}
-
-int V4L2Device::createPlane(Buffer *buffer, unsigned int planeIndex,
-			    unsigned int length)
-{
-	struct v4l2_exportbuffer expbuf = {};
-	int ret;
-
-	LOG(V4L2, Debug)
-		<< "Buffer " << buffer->index()
-		<< " plane " << planeIndex
-		<< ": length=" << length;
-
-	expbuf.type = bufferType_;
-	expbuf.index = buffer->index();
-	expbuf.plane = planeIndex;
-	expbuf.flags = O_RDWR;
-
-	ret = ioctl(fd_, VIDIOC_EXPBUF, &expbuf);
-	if (ret < 0) {
-		ret = -errno;
-		LOG(V4L2, Error)
-			<< "Failed to export buffer: " << strerror(-ret);
-		return ret;
-	}
-
-	buffer->planes().emplace_back();
-	Plane &plane = buffer->planes().back();
-	plane.setDmabuf(expbuf.fd, length);
-	::close(expbuf.fd);
-
-	return 0;
-}
-
-/**
- * \brief Import the externally allocated \a pool of buffers
- * \param[in] pool BufferPool of buffers to import
- * \return 0 on success or a negative error code otherwise
- */
-int V4L2Device::importBuffers(BufferPool *pool)
-{
-	unsigned int allocatedBuffers;
-	int ret;
-
-	memoryType_ = V4L2_MEMORY_DMABUF;
-
-	ret = requestBuffers(pool->count());
-	if (ret < 0)
-		return ret;
-
-	allocatedBuffers = ret;
-	if (allocatedBuffers < pool->count()) {
-		LOG(V4L2, Error)
-			<< "Not enough buffers provided by V4L2Device";
-		requestBuffers(0);
-		return -ENOMEM;
-	}
-
-	LOG(V4L2, Debug) << "provided pool of " << pool->count() << " buffers";
-	bufferPool_ = pool;
+	fd_ = ret;

 	return 0;
 }

 /**
- * \brief Release all internally allocated buffers
- */
-int V4L2Device::releaseBuffers()
-{
-	LOG(V4L2, Debug) << "Releasing bufferPool";
-
-	bufferPool_ = nullptr;
-
-	return requestBuffers(0);
-}
-
-/**
- * \brief Queue a buffer into the device
- * \param[in] buffer The buffer to be queued
+ * \brief Close the device or subdevice
  *
- * For capture devices the \a buffer will be filled with data by the device.
- * For output devices the \a buffer shall contain valid data and will be
- * processed by the device. Once the device has finished processing the buffer,
- * it will be available for dequeue.
+ * Reset the file descriptor to -1
  *
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Device::queueBuffer(Buffer *buffer)
+int V4L2Device::close()
 {
-	struct v4l2_buffer buf = {};
-	struct v4l2_plane planes[VIDEO_MAX_PLANES] = {};
-	int ret;
-
-	buf.index = buffer->index();
-	buf.type = bufferType_;
-	buf.memory = memoryType_;
-	buf.field = V4L2_FIELD_NONE;
-
-	bool multiPlanar = V4L2_TYPE_IS_MULTIPLANAR(buf.type);
-
-	if (buf.memory == V4L2_MEMORY_DMABUF) {
-		if (multiPlanar) {
-			for (unsigned int p = 0;
-			     p < buffer->planes().size();
-			     p++)
-				planes[p].m.fd = buffer->planes()[p].dmabuf();
-		} else {
-			buf.m.fd = buffer->planes()[0].dmabuf();
-		}
-	}
-
-	if (multiPlanar) {
-		buf.length = buffer->planes().size();
-		buf.m.planes = planes;
-	}
-
-	if (V4L2_TYPE_IS_OUTPUT(bufferType_)) {
-		buf.bytesused = buffer->bytesused_;
-		buf.sequence = buffer->sequence_;
-		buf.timestamp.tv_sec = buffer->timestamp_ / 1000000000;
-		buf.timestamp.tv_usec = (buffer->timestamp_ / 1000) % 1000000;
-	}
-
-	LOG(V4L2, Debug) << "Queueing buffer " << buf.index;
+	if (fd_ < 0)
+		return 0;

-	ret = ioctl(fd_, VIDIOC_QBUF, &buf);
+	int ret = ::close(fd_);
 	if (ret < 0) {
 		ret = -errno;
-		LOG(V4L2, Error)
-			<< "Failed to queue buffer " << buf.index << ": "
-			<< strerror(-ret);
+		LOG(V4L2, Error) << "Failed to close V4L2 device: "
+				 << strerror(-ret);
 		return ret;
 	}

-	if (queuedBuffersCount_++ == 0)
-		fdEvent_->setEnabled(true);
+	fd_ = -1;

 	return 0;
 }

 /**
- * \brief Dequeue the next available buffer from the device
- *
- * This method dequeues the next available buffer from the device. If no buffer
- * is available to be dequeued it will return nullptr immediately.
- *
- * \return A pointer to the dequeued buffer on success, or nullptr otherwise
- */
-Buffer *V4L2Device::dequeueBuffer()
-{
-	struct v4l2_buffer buf = {};
-	struct v4l2_plane planes[VIDEO_MAX_PLANES] = {};
-	int ret;
-
-	buf.type = bufferType_;
-	buf.memory = memoryType_;
-
-	if (V4L2_TYPE_IS_MULTIPLANAR(buf.type)) {
-		buf.length = VIDEO_MAX_PLANES;
-		buf.m.planes = planes;
-	}
-
-	ret = ioctl(fd_, VIDIOC_DQBUF, &buf);
-	if (ret < 0) {
-		ret = -errno;
-		LOG(V4L2, Error)
-			<< "Failed to dequeue buffer: " << strerror(-ret);
-		return nullptr;
-	}
-
-	ASSERT(buf.index < bufferPool_->count());
-
-	if (--queuedBuffersCount_ == 0)
-		fdEvent_->setEnabled(false);
-
-	Buffer *buffer = &bufferPool_->buffers()[buf.index];
-
-	buffer->bytesused_ = buf.bytesused;
-	buffer->timestamp_ = buf.timestamp.tv_sec * 1000000000ULL
-			   + buf.timestamp.tv_usec * 1000ULL;
-	buffer->sequence_ = buf.sequence;
-	buffer->status_ = buf.flags & V4L2_BUF_FLAG_ERROR
-			? Buffer::BufferError : Buffer::BufferSuccess;
-
-	return buffer;
-}
-
-/**
- * \brief Slot to handle completed buffer events from the V4L2 device
- * \param[in] notifier The event notifier
- *
- * When this slot is called, a Buffer has become available from the device, and
- * will be emitted through the bufferReady Signal.
- *
- * For Capture devices the Buffer will contain valid data.
- * For Output devices the Buffer can be considered empty.
+ * \brief Check if the V4L2 device or subdevice is open
+ * \return True if the V4L2 device or subdevice is open, false otherwise
  */
-void V4L2Device::bufferAvailable(EventNotifier *notifier)
-{
-	Buffer *buffer = dequeueBuffer();
-	if (!buffer)
-		return;
-
-	LOG(V4L2, Debug) << "Buffer " << buffer->index() << " is available";
-
-	/* Notify anyone listening to the device. */
-	bufferReady.emit(buffer);
-}
-
-/**
- * \var V4L2Device::bufferReady
- * \brief A Signal emitted when a buffer completes
- */
-
-/**
- * \brief Start the video stream
- * \return 0 on success or a negative error code otherwise
- */
-int V4L2Device::streamOn()
+bool V4L2Device::isOpen() const
 {
-	int ret;
-
-	ret = ioctl(fd_, VIDIOC_STREAMON, &bufferType_);
-	if (ret < 0) {
-		ret = -errno;
-		LOG(V4L2, Error)
-			<< "Failed to start streaming: " << strerror(-ret);
-		return ret;
-	}
-
-	return 0;
+	return fd_ != -1;
 }

 /**
- * \brief Stop the video stream
- *
- * Buffers that are still queued when the video stream is stopped are
- * implicitly dequeued, but no bufferReady signal is emitted for them.
- *
+ * \brief Perform an IOCTL system call on the device node
+ * \param request The IOCTL request code
+ * \param argp A poiunter to the IOCTL argument
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Device::streamOff()
+int V4L2Device::ioctl(unsigned long request, void *argp)
 {
-	int ret;
-
-	ret = ioctl(fd_, VIDIOC_STREAMOFF, &bufferType_);
-	if (ret < 0) {
-		ret = -errno;
-		LOG(V4L2, Error)
-			<< "Failed to stop streaming: " << strerror(-ret);
-		return ret;
-	}
-
-	queuedBuffersCount_ = 0;
-	fdEvent_->setEnabled(false);
+	/*
+	 * Printing out an error message is usually better performed
+	 * in the caller, which can provide more context.
+	 */
+	if (::ioctl(fd_, request, argp) < 0)
+		return -errno;

 	return 0;
 }

 /**
- * \brief Create a new video device instance from \a entity in media device
- * \a media
- * \param[in] media The media device where the entity is registered
- * \param[in] entity The media entity name
- *
- * Releasing memory of the newly created instance is responsibility of the
- * caller of this function.
+ * \var V4L2Device::fd_
+ * \brief The V4L2 device or subdevice device node file descriptor
  *
- * \return A newly created V4L2Device on success, nullptr otherwise
+ * The file descriptor is initialized to -1 and reset to this value once
+ * the device or subdevice gets closed.
  */
-V4L2Device *V4L2Device::fromEntityName(const MediaDevice *media,
-				       const std::string &entity)
-{
-	MediaEntity *mediaEntity = media->getEntityByName(entity);
-	if (!mediaEntity)
-		return nullptr;
-
-	return new V4L2Device(mediaEntity);
-}

-} /* namespace libcamera */
+}; /* namespace libcamera */
diff --git a/src/libcamera/v4l2_subdevice.cpp b/src/libcamera/v4l2_subdev.cpp
similarity index 79%
rename from src/libcamera/v4l2_subdevice.cpp
rename to src/libcamera/v4l2_subdev.cpp
index fceee33156e9..60bbd07284ff 100644
--- a/src/libcamera/v4l2_subdevice.cpp
+++ b/src/libcamera/v4l2_subdev.cpp
@@ -2,10 +2,10 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_subdevice.cpp - V4L2 Subdevice
+ * v4l2_subdev.cpp - V4L2 Subdevice
  */

-#include "v4l2_subdevice.h"
+#include "v4l2_subdev.h"

 #include <fcntl.h>
 #include <iomanip>
@@ -23,7 +23,7 @@
 #include "media_object.h"

 /**
- * \file v4l2_subdevice.h
+ * \file v4l2_subdev.h
  * \brief V4L2 Subdevice API
  */

@@ -44,7 +44,7 @@ LOG_DEFINE_CATEGORY(V4L2Subdev)
  * as the "media bus format", and it is identified by a resolution and a pixel
  * format identification code, known as the "media bus code", not to be confused
  * with the fourcc code that identify the format of images when stored in memory
- * (see V4L2Device::V4L2DeviceFormat).
+ * (see V4L2Videodev::V4L2DeviceFormat).
  *
  * Media Bus formats supported by the V4L2 APIs are described in Section
  * 4.15.3.4.1 of the "Part I - Video for Linux API" chapter of the "Linux Media
@@ -84,14 +84,14 @@ const std::string V4L2SubdeviceFormat::toString() const
 }

 /**
- * \class V4L2Subdevice
+ * \class V4L2Subdev
  * \brief A V4L2 subdevice as exposed by the Linux kernel
  *
- * The V4L2Subdevice class provides an API to the "Sub-device interface" as
+ * The V4L2Subdev class provides an API to the "Sub-device interface" as
  * described in section 4.15 of the "Linux Media Infrastructure userspace API"
  * chapter of the Linux Kernel documentation.
  *
- * A V4L2Subdevice is constructed from a MediaEntity instance, using the system
+ * A V4L2Subdev is constructed from a MediaEntity instance, using the system
  * path of the entity's device node. No API call other than open(), isOpen()
  * and close() shall be called on an unopened device instance. Upon destruction
  * any device left open will be closed, and any resources released.
@@ -101,65 +101,39 @@ const std::string V4L2SubdeviceFormat::toString() const
  * \brief Create a V4L2 subdevice from a MediaEntity using its device node
  * path
  */
-V4L2Subdevice::V4L2Subdevice(const MediaEntity *entity)
-	: entity_(entity), fd_(-1)
+V4L2Subdev::V4L2Subdev(const MediaEntity *entity)
+	: V4L2Device(), entity_(entity)
 {
 }

-V4L2Subdevice::~V4L2Subdevice()
+V4L2Subdev::~V4L2Subdev()
 {
-	close();
+	V4L2Device::close();
 }

 /**
  * \brief Open a V4L2 subdevice
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Subdevice::open()
+int V4L2Subdev::open()
 {
-	int ret;
-
-	if (isOpen()) {
-		LOG(V4L2Subdev, Error) << "Device already open";
-		return -EBUSY;
-	}
-
-	ret = ::open(entity_->deviceNode().c_str(), O_RDWR);
-	if (ret < 0) {
-		ret = -errno;
-		LOG(V4L2Subdev, Error)
-			<< "Failed to open V4L2 subdevice '"
-			<< entity_->deviceNode() << "': " << strerror(-ret);
+	int ret = V4L2Device::open(entity_->deviceNode(), O_RDWR);
+	if (ret)
 		return ret;
-	}
-	fd_ = ret;

 	return 0;
 }

-/**
- * \brief Check if the subdevice is open
- * \return True if the subdevice is open, false otherwise
- */
-bool V4L2Subdevice::isOpen() const
-{
-	return fd_ != -1;
-}
-
 /**
  * \brief Close the subdevice, releasing any resources acquired by open()
  */
-void V4L2Subdevice::close()
+void V4L2Subdev::close()
 {
-	if (!isOpen())
-		return;
-
-	::close(fd_);
-	fd_ = -1;
+	V4L2Device::close();
 }

 /**
- * \fn V4L2Subdevice::entity()
+ * \fn V4L2Subdev::entity()
  * \brief Retrieve the media entity associated with the subdevice
  * \return The subdevice's associated media entity.
  */
@@ -170,7 +144,7 @@ void V4L2Subdevice::close()
  * \param[inout] rect The rectangle describing crop target area
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Subdevice::setCrop(unsigned int pad, Rectangle *rect)
+int V4L2Subdev::setCrop(unsigned int pad, Rectangle *rect)
 {
 	return setSelection(pad, V4L2_SEL_TGT_CROP, rect);
 }
@@ -181,7 +155,7 @@ int V4L2Subdevice::setCrop(unsigned int pad, Rectangle *rect)
  * \param[inout] rect The rectangle describing the compose target area
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Subdevice::setCompose(unsigned int pad, Rectangle *rect)
+int V4L2Subdev::setCompose(unsigned int pad, Rectangle *rect)
 {
 	return setSelection(pad, V4L2_SEL_TGT_COMPOSE, rect);
 }
@@ -200,7 +174,7 @@ int V4L2Subdevice::setCompose(unsigned int pad, Rectangle *rect)
  * \return A map of image formats associated with a list of image sizes, or
  * an empty map on error or if the pad does not exist
  */
-FormatEnum V4L2Subdevice::formats(unsigned int pad)
+FormatEnum V4L2Subdev::formats(unsigned int pad)
 {
 	FormatEnum formatMap = {};
 	struct v4l2_subdev_mbus_code_enum mbusEnum = {};
@@ -215,7 +189,7 @@ FormatEnum V4L2Subdevice::formats(unsigned int pad)
 	mbusEnum.index = 0;
 	mbusEnum.which = V4L2_SUBDEV_FORMAT_ACTIVE;
 	while (true) {
-		ret = ioctl(fd_, VIDIOC_SUBDEV_ENUM_MBUS_CODE, &mbusEnum);
+		ret = V4L2Device::ioctl(VIDIOC_SUBDEV_ENUM_MBUS_CODE, &mbusEnum);
 		if (ret)
 			break;

@@ -244,13 +218,13 @@ FormatEnum V4L2Subdevice::formats(unsigned int pad)
  * \param[out] format The image bus format
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Subdevice::getFormat(unsigned int pad, V4L2SubdeviceFormat *format)
+int V4L2Subdev::getFormat(unsigned int pad, V4L2SubdeviceFormat *format)
 {
 	struct v4l2_subdev_format subdevFmt = {};
 	subdevFmt.which = V4L2_SUBDEV_FORMAT_ACTIVE;
 	subdevFmt.pad = pad;

-	int ret = ioctl(fd_, VIDIOC_SUBDEV_G_FMT, &subdevFmt);
+	int ret = V4L2Device::ioctl(VIDIOC_SUBDEV_G_FMT, &subdevFmt);
 	if (ret) {
 		ret = -errno;
 		LOG(V4L2Subdev, Error)
@@ -272,12 +246,12 @@ int V4L2Subdevice::getFormat(unsigned int pad, V4L2SubdeviceFormat *format)
  * \param[inout] format The image bus format to apply to the subdevice's pad
  *
  * Apply the requested image format to the desired media pad and return the
- * actually applied format parameters, as \ref V4L2Subdevice::getFormat would
+ * actually applied format parameters, as \ref V4L2Subdev::getFormat would
  * do.
  *
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Subdevice::setFormat(unsigned int pad, V4L2SubdeviceFormat *format)
+int V4L2Subdev::setFormat(unsigned int pad, V4L2SubdeviceFormat *format)
 {
 	struct v4l2_subdev_format subdevFmt = {};
 	subdevFmt.which = V4L2_SUBDEV_FORMAT_ACTIVE;
@@ -286,7 +260,7 @@ int V4L2Subdevice::setFormat(unsigned int pad, V4L2SubdeviceFormat *format)
 	subdevFmt.format.height = format->size.height;
 	subdevFmt.format.code = format->mbus_code;

-	int ret = ioctl(fd_, VIDIOC_SUBDEV_S_FMT, &subdevFmt);
+	int ret = V4L2Device::ioctl(VIDIOC_SUBDEV_S_FMT, &subdevFmt);
 	if (ret) {
 		ret = -errno;
 		LOG(V4L2Subdev, Error)
@@ -311,25 +285,25 @@ int V4L2Subdevice::setFormat(unsigned int pad, V4L2SubdeviceFormat *format)
  * Releasing memory of the newly created instance is responsibility of the
  * caller of this function.
  *
- * \return A newly created V4L2Subdevice on success, nullptr otherwise
+ * \return A newly created V4L2Subdev on success, nullptr otherwise
  */
-V4L2Subdevice *V4L2Subdevice::fromEntityName(const MediaDevice *media,
-					     const std::string &entity)
+V4L2Subdev *V4L2Subdev::fromEntityName(const MediaDevice *media,
+				       const std::string &entity)
 {
 	MediaEntity *mediaEntity = media->getEntityByName(entity);
 	if (!mediaEntity)
 		return nullptr;

-	return new V4L2Subdevice(mediaEntity);
+	return new V4L2Subdev(mediaEntity);
 }

-std::string V4L2Subdevice::logPrefix() const
+std::string V4L2Subdev::logPrefix() const
 {
 	return "'" + entity_->name() + "'";
 }

-int V4L2Subdevice::enumPadSizes(unsigned int pad,unsigned int code,
-				std::vector<SizeRange> *sizes)
+int V4L2Subdev::enumPadSizes(unsigned int pad,unsigned int code,
+			     std::vector<SizeRange> *sizes)
 {
 	struct v4l2_subdev_frame_size_enum sizeEnum = {};
 	int ret;
@@ -339,7 +313,7 @@ int V4L2Subdevice::enumPadSizes(unsigned int pad,unsigned int code,
 	sizeEnum.code = code;
 	sizeEnum.which = V4L2_SUBDEV_FORMAT_ACTIVE;
 	while (true) {
-		ret = ioctl(fd_, VIDIOC_SUBDEV_ENUM_FRAME_SIZE, &sizeEnum);
+		ret = V4L2Device::ioctl(VIDIOC_SUBDEV_ENUM_FRAME_SIZE, &sizeEnum);
 		if (ret)
 			break;

@@ -362,8 +336,8 @@ int V4L2Subdevice::enumPadSizes(unsigned int pad,unsigned int code,
 	return 0;
 }

-int V4L2Subdevice::setSelection(unsigned int pad, unsigned int target,
-				Rectangle *rect)
+int V4L2Subdev::setSelection(unsigned int pad, unsigned int target,
+			     Rectangle *rect)
 {
 	struct v4l2_subdev_selection sel = {};

@@ -377,7 +351,7 @@ int V4L2Subdevice::setSelection(unsigned int pad, unsigned int target,
 	sel.r.width = rect->w;
 	sel.r.height = rect->h;

-	int ret = ioctl(fd_, VIDIOC_SUBDEV_S_SELECTION, &sel);
+	int ret = V4L2Device::ioctl(VIDIOC_SUBDEV_S_SELECTION, &sel);
 	if (ret < 0) {
 		ret = -errno;
 		LOG(V4L2Subdev, Error)
diff --git a/src/libcamera/v4l2_videodev.cpp b/src/libcamera/v4l2_videodev.cpp
new file mode 100644
index 000000000000..6ad3af53a3bd
--- /dev/null
+++ b/src/libcamera/v4l2_videodev.cpp
@@ -0,0 +1,992 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * v4l2_videodev.cpp - V4L2 Video Device
+ */
+
+#include "v4l2_videodev.h"
+
+#include <fcntl.h>
+#include <iomanip>
+#include <sstream>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <vector>
+
+#include <libcamera/buffer.h>
+#include <libcamera/event_notifier.h>
+
+#include "log.h"
+#include "media_device.h"
+#include "media_object.h"
+
+/**
+ * \file v4l2_videodev.h
+ * \brief V4L2 Video Device API
+ */
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(V4L2)
+
+/**
+ * \struct V4L2Capability
+ * \brief struct v4l2_capability object wrapper and helpers
+ *
+ * The V4L2Capability structure manages the information returned by the
+ * VIDIOC_QUERYCAP ioctl.
+ */
+
+/**
+ * \fn V4L2Capability::driver()
+ * \brief Retrieve the driver module name
+ * \return The string containing the name of the driver module
+ */
+
+/**
+ * \fn V4L2Capability::card()
+ * \brief Retrieve the device card name
+ * \return The string containing the device name
+ */
+
+/**
+ * \fn V4L2Capability::bus_info()
+ * \brief Retrieve the location of the device in the system
+ * \return The string containing the device location
+ */
+
+/**
+ * \fn V4L2Capability::device_caps()
+ * \brief Retrieve the capabilities of the device
+ * \return The device specific capabilities if V4L2_CAP_DEVICE_CAPS is set or
+ * 	   driver capabilities otherwise
+ */
+
+/**
+ * \fn V4L2Capability::isMultiplanar()
+ * \brief Identify if the device implements the V4L2 multiplanar APIs
+ * \return True if the device supports multiplanar APIs
+ */
+
+/**
+ * \fn V4L2Capability::isCapture()
+ * \brief Identify if the device captures data
+ * \return True if the device can capture data
+ */
+
+/**
+ * \fn V4L2Capability::isOutput()
+ * \brief Identify if the device outputs data
+ * \return True if the device can output data
+ */
+
+/**
+ * \fn V4L2Capability::isVideo()
+ * \brief Identify if the device captures or outputs images
+ * \return True if the device can capture or output images
+ */
+
+/**
+ * \fn V4L2Capability::isMeta()
+ * \brief Identify if the device captures or outputs image meta-data
+ *
+ * \todo Add support for META_CAPTURE introduced in Linux v5.0
+ *
+ * \return True if the device can capture or output image meta-data
+ */
+
+/**
+ * \fn V4L2Capability::isVideoCapture()
+ * \brief Identify if the device captures images
+ * \return True if the device can capture images
+ */
+
+/**
+ * \fn V4L2Capability::isVideoOutput()
+ * \brief Identify if the device outputs images
+ * \return True if the device can output images
+ */
+
+/**
+ * \fn V4L2Capability::isMetaCapture()
+ * \brief Identify if the device captures image meta-data
+ * \return True if the device can capture image meta-data
+ */
+
+/**
+ * \fn V4L2Capability::isMetaOutput()
+ * \brief Identify if the device outputs image meta-data
+ * \return True if the device can output image meta-data
+ */
+
+/**
+ * \fn V4L2Capability::hasStreaming()
+ * \brief Determine if the device can perform Streaming I/O
+ * \return True if the device provides Streaming I/O IOCTLs
+ */
+
+/**
+ * \class V4L2DeviceFormat
+ * \brief The V4L2 device image format and sizes
+ *
+ * This class describes the image format and resolution to be programmed on a
+ * V4L2 video device. The image format is defined by a fourcc code (as specified
+ * by the V4L2 API with the V4L2_PIX_FMT_* macros), a resolution (width and
+ * height) and one to three planes with configurable line stride and a total
+ * per-plane size in bytes.
+ *
+ * Image formats, as defined by the V4L2 APIs, are categorised as packed,
+ * semi-planar and planar, and describe the layout of the image pixel components
+ * stored in memory.
+ *
+ * Packed image formats store pixel components one after the other, in a
+ * contiguous memory area. Examples of packed image formats are YUYV
+ * permutations, RGB with different pixel sub-sampling ratios such as RGB565 or
+ * RGB666 or Raw-Bayer formats such as SRGGB8 or SGRBG12.
+ *
+ * Semi-planar and planar image formats store the pixel components in separate
+ * and possibly non-contiguous memory areas, named planes, whose sizes depend on
+ * the pixel components sub-sampling ratios, which are defined by the format.
+ * Semi-planar formats use two planes to store pixel components and notable
+ * examples of such formats are the NV12 and NV16 formats, while planar formats
+ * use three planes to store pixel components and notable examples are YUV422
+ * and YUV420.
+ *
+ * Image formats supported by the V4L2 API are defined and described in Section
+ * number 2 of the "Part I - Video for Linux API" chapter of the "Linux Media
+ * Infrastructure userspace API", part of the Linux kernel documentation.
+ *
+ * In the context of this document, packed image formats are referred to as
+ * "packed formats" and semi-planar and planar image formats are referred to as
+ * "planar formats".
+ *
+ * V4L2 also defines two different sets of APIs to work with devices that store
+ * planes in contiguous or separate memory areas. They are named "Single-plane
+ * APIs" and "Multi-plane APIs" respectively and are documented in Section 2.1
+ * and Section 2.2 of the above mentioned "Part I - Video for Linux API"
+ * documentation.
+ *
+ * The single-plane API allows, among other parameters, the configuration of the
+ * image resolution, the pixel format and the stride length. In that case the
+ * stride applies to all planes (possibly sub-sampled). The multi-plane API
+ * allows configuring the resolution, the pixel format and a per-plane stride
+ * length and total size.
+ *
+ * Packed image formats, which occupy a single memory area, are easily described
+ * through the single-plane API. When used on a device that implements the
+ * multi-plane API, only the size and stride information contained in the first
+ * plane are taken into account.
+ *
+ * Planar image formats, which occupy distinct memory areas, are easily
+ * described through the multi-plane APIs. When used on a device that implements
+ * the single-plane API, all planes are stored one after the other in a
+ * contiguous memory area, and it is not possible to configure per-plane stride
+ * length and size, but only a global stride length which is applied to all
+ * planes.
+ *
+ * The V4L2DeviceFormat class describes both packed and planar image formats,
+ * regardless of the API type (single or multi plane) implemented by the device
+ * the format has to be applied to. The total size and bytes per line of images
+ * represented with packed formats are configured using the first entry of the
+ * V4L2DeviceFormat::planes array, while the per-plane size and per-plane stride
+ * length of images represented with planar image formats are configured using
+ * the opportune number of entries of the V4L2DeviceFormat::planes array, as
+ * prescribed by the image format definition (semi-planar formats use 2 entries,
+ * while planar formats use the whole 3 entries). The number of valid entries of
+ * the V4L2DeviceFormat::planes array is defined by the
+ * V4L2DeviceFormat::planesCount value.
+ */
+
+/**
+ * \var V4L2DeviceFormat::size
+ * \brief The image size in pixels
+ */
+
+/**
+ * \var V4L2DeviceFormat::fourcc
+ * \brief The fourcc code describing the pixel encoding scheme
+ *
+ * The fourcc code, as defined by the V4L2 API with the V4L2_PIX_FMT_* macros,
+ * that identifies the image format pixel encoding scheme.
+ */
+
+/**
+ * \var V4L2DeviceFormat::planes
+ * \brief The per-plane memory size information
+ *
+ * Images are stored in memory in one or more data planes. Each data plane has a
+ * specific line stride and memory size, which could differ from the image
+ * visible sizes to accommodate padding at the end of lines and end of planes.
+ * Only the first \ref planesCount entries are considered valid.
+ */
+
+/**
+ * \var V4L2DeviceFormat::planesCount
+ * \brief The number of valid data planes
+ */
+
+/**
+ * \brief Assemble and return a string describing the format
+ * \return A string describing the V4L2DeviceFormat
+ */
+const std::string V4L2DeviceFormat::toString() const
+{
+	std::stringstream ss;
+
+	ss.fill(0);
+	ss << size.toString() << "-0x" << std::hex << std::setw(8) << fourcc;
+
+	return ss.str();
+}
+
+/**
+ * \class V4L2Videodev
+ * \brief V4L2Videodev object and API
+ *
+ * The V4L2 Device API class models an instance of a V4L2 device node.
+ * It is constructed with the path to a V4L2 video device node. The device node
+ * is only opened upon a call to open() which must be checked for success.
+ *
+ * The device capabilities are validated when the device is opened and the
+ * device is rejected if it is not a suitable V4L2 capture or output device, or
+ * if the device does not support streaming I/O.
+ *
+ * No API call other than open(), isOpen() and close() shall be called on an
+ * unopened device instance.
+ *
+ * The V4L2Videodev class tracks queued buffers and handles buffer events. It
+ * automatically dequeues completed buffers and emits the \ref bufferReady
+ * signal.
+ *
+ * Upon destruction any device left open will be closed, and any resources
+ * released.
+ */
+
+/**
+ * \brief Construct a V4L2Videodev
+ * \param[in] deviceNode The file-system path to the video device node
+ */
+V4L2Videodev::V4L2Videodev(const std::string &deviceNode)
+	: V4L2Device(), deviceNode_(deviceNode), bufferPool_(nullptr),
+	  queuedBuffersCount_(0), fdEvent_(nullptr)
+{
+	/*
+	 * We default to an MMAP based CAPTURE device, however this will be
+	 * updated based upon the device capabilities.
+	 */
+	bufferType_ = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
+	memoryType_ = V4L2_MEMORY_MMAP;
+}
+
+/**
+ * \brief Construct a V4L2Videodev from a MediaEntity
+ * \param[in] entity The MediaEntity to build the device from
+ *
+ * Construct a V4L2Videodev from a MediaEntity's device node path.
+ */
+V4L2Videodev::V4L2Videodev(const MediaEntity *entity)
+	: V4L2Videodev(entity->deviceNode())
+{
+}
+
+V4L2Videodev::~V4L2Videodev()
+{
+	V4L2Device::close();
+}
+
+/**
+ * \brief Open a V4L2 device and query its capabilities
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Videodev::open()
+{
+	int ret;
+
+	ret = V4L2Device::open(deviceNode_, O_RDWR | O_NONBLOCK);
+	if (ret < 0)
+		return ret;
+
+	ret = V4L2Device::ioctl(VIDIOC_QUERYCAP, &caps_);
+	if (ret < 0) {
+		ret = -errno;
+		LOG(V4L2, Error)
+			<< "Failed to query device capabilities: "
+			<< strerror(-ret);
+		return ret;
+	}
+
+	LOG(V4L2, Debug)
+		<< "Opened device " << caps_.bus_info() << ": "
+		<< caps_.driver() << ": " << caps_.card();
+
+	if (!caps_.hasStreaming()) {
+		LOG(V4L2, Error) << "Device does not support streaming I/O";
+		return -EINVAL;
+	}
+
+	/*
+	 * Set buffer type and wait for read notifications on CAPTURE devices
+	 * (POLLIN), and write notifications for OUTPUT devices (POLLOUT).
+	 */
+	if (caps_.isVideoCapture()) {
+		fdEvent_ = new EventNotifier(fd(), EventNotifier::Read);
+		bufferType_ = caps_.isMultiplanar()
+			    ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
+			    : V4L2_BUF_TYPE_VIDEO_CAPTURE;
+	} else if (caps_.isVideoOutput()) {
+		fdEvent_ = new EventNotifier(fd(), EventNotifier::Write);
+		bufferType_ = caps_.isMultiplanar()
+			    ? V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE
+			    : V4L2_BUF_TYPE_VIDEO_OUTPUT;
+	} else if (caps_.isMetaCapture()) {
+		fdEvent_ = new EventNotifier(fd(), EventNotifier::Read);
+		bufferType_ = V4L2_BUF_TYPE_META_CAPTURE;
+	} else if (caps_.isMetaOutput()) {
+		fdEvent_ = new EventNotifier(fd(), EventNotifier::Write);
+		bufferType_ = V4L2_BUF_TYPE_META_OUTPUT;
+	} else {
+		LOG(V4L2, Error) << "Device is not a supported type";
+		return -EINVAL;
+	}
+
+	fdEvent_->activated.connect(this, &V4L2Videodev::bufferAvailable);
+	fdEvent_->setEnabled(false);
+
+	return 0;
+}
+
+/**
+ * \brief Close the device, releasing any resources acquired by open()
+ */
+void V4L2Videodev::close()
+{
+	if (fd() < 0)
+		return;
+
+	releaseBuffers();
+	delete fdEvent_;
+
+	V4L2Device::close();
+}
+
+/**
+ * \fn V4L2Videodev::driverName()
+ * \brief Retrieve the name of the V4L2 device driver
+ * \return The string containing the driver name
+ */
+
+/**
+ * \fn V4L2Videodev::deviceName()
+ * \brief Retrieve the name of the V4L2 device
+ * \return The string containing the device name
+ */
+
+/**
+ * \fn V4L2Videodev::busName()
+ * \brief Retrieve the location of the device in the system
+ * \return The string containing the device location
+ */
+
+/**
+ * \fn V4L2Videodev::deviceNode()
+ * \brief Retrieve the video device node path
+ * \return The video device device node path
+ */
+
+std::string V4L2Videodev::logPrefix() const
+{
+	return deviceNode_ + (V4L2_TYPE_IS_OUTPUT(bufferType_) ? "[out]" : "[cap]");
+}
+
+/**
+ * \brief Retrieve the image format set on the V4L2 device
+ * \param[out] format The image format applied on the device
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Videodev::getFormat(V4L2DeviceFormat *format)
+{
+	if (caps_.isMeta())
+		return getFormatMeta(format);
+	else if (caps_.isMultiplanar())
+		return getFormatMultiplane(format);
+	else
+		return getFormatSingleplane(format);
+}
+
+/**
+ * \brief Configure an image format on the V4L2 device
+ * \param[inout] format The image format to apply to the device
+ *
+ * Apply the supplied \a format to the device, and return the actually
+ * applied format parameters, as \ref V4L2Videodev::getFormat would do.
+ *
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Videodev::setFormat(V4L2DeviceFormat *format)
+{
+	if (caps_.isMeta())
+		return setFormatMeta(format);
+	else if (caps_.isMultiplanar())
+		return setFormatMultiplane(format);
+	else
+		return setFormatSingleplane(format);
+}
+
+int V4L2Videodev::getFormatMeta(V4L2DeviceFormat *format)
+{
+	struct v4l2_format v4l2Format = {};
+	struct v4l2_meta_format *pix = &v4l2Format.fmt.meta;
+	int ret;
+
+	v4l2Format.type = bufferType_;
+	ret = V4L2Device::ioctl(VIDIOC_G_FMT, &v4l2Format);
+	if (ret) {
+		ret = -errno;
+		LOG(V4L2, Error) << "Unable to get format: " << strerror(-ret);
+		return ret;
+	}
+
+	format->size.width = 0;
+	format->size.height = 0;
+	format->fourcc = pix->dataformat;
+	format->planesCount = 1;
+	format->planes[0].bpl = pix->buffersize;
+	format->planes[0].size = pix->buffersize;
+
+	return 0;
+}
+
+int V4L2Videodev::setFormatMeta(V4L2DeviceFormat *format)
+{
+	struct v4l2_format v4l2Format = {};
+	struct v4l2_meta_format *pix = &v4l2Format.fmt.meta;
+	int ret;
+
+	v4l2Format.type = bufferType_;
+	pix->dataformat = format->fourcc;
+	pix->buffersize = format->planes[0].size;
+	ret = V4L2Device::ioctl(VIDIOC_S_FMT, &v4l2Format);
+	if (ret) {
+		ret = -errno;
+		LOG(V4L2, Error) << "Unable to set format: " << strerror(-ret);
+		return ret;
+	}
+
+	/*
+	 * Return to caller the format actually applied on the device,
+	 * which might differ from the requested one.
+	 */
+	format->size.width = 0;
+	format->size.height = 0;
+	format->fourcc = format->fourcc;
+	format->planesCount = 1;
+	format->planes[0].bpl = pix->buffersize;
+	format->planes[0].size = pix->buffersize;
+
+	return 0;
+}
+
+int V4L2Videodev::getFormatMultiplane(V4L2DeviceFormat *format)
+{
+	struct v4l2_format v4l2Format = {};
+	struct v4l2_pix_format_mplane *pix = &v4l2Format.fmt.pix_mp;
+	int ret;
+
+	v4l2Format.type = bufferType_;
+	ret = V4L2Device::ioctl(VIDIOC_G_FMT, &v4l2Format);
+	if (ret) {
+		ret = -errno;
+		LOG(V4L2, Error) << "Unable to get format: " << strerror(-ret);
+		return ret;
+	}
+
+	format->size.width = pix->width;
+	format->size.height = pix->height;
+	format->fourcc = pix->pixelformat;
+	format->planesCount = pix->num_planes;
+
+	for (unsigned int i = 0; i < format->planesCount; ++i) {
+		format->planes[i].bpl = pix->plane_fmt[i].bytesperline;
+		format->planes[i].size = pix->plane_fmt[i].sizeimage;
+	}
+
+	return 0;
+}
+
+int V4L2Videodev::setFormatMultiplane(V4L2DeviceFormat *format)
+{
+	struct v4l2_format v4l2Format = {};
+	struct v4l2_pix_format_mplane *pix = &v4l2Format.fmt.pix_mp;
+	int ret;
+
+	v4l2Format.type = bufferType_;
+	pix->width = format->size.width;
+	pix->height = format->size.height;
+	pix->pixelformat = format->fourcc;
+	pix->num_planes = format->planesCount;
+	pix->field = V4L2_FIELD_NONE;
+
+	for (unsigned int i = 0; i < pix->num_planes; ++i) {
+		pix->plane_fmt[i].bytesperline = format->planes[i].bpl;
+		pix->plane_fmt[i].sizeimage = format->planes[i].size;
+	}
+
+	ret = V4L2Device::ioctl(VIDIOC_S_FMT, &v4l2Format);
+	if (ret) {
+		ret = -errno;
+		LOG(V4L2, Error) << "Unable to set format: " << strerror(-ret);
+		return ret;
+	}
+
+	/*
+	 * Return to caller the format actually applied on the device,
+	 * which might differ from the requested one.
+	 */
+	format->size.width = pix->width;
+	format->size.height = pix->height;
+	format->fourcc = pix->pixelformat;
+	format->planesCount = pix->num_planes;
+	for (unsigned int i = 0; i < format->planesCount; ++i) {
+		format->planes[i].bpl = pix->plane_fmt[i].bytesperline;
+		format->planes[i].size = pix->plane_fmt[i].sizeimage;
+	}
+
+	return 0;
+}
+
+int V4L2Videodev::getFormatSingleplane(V4L2DeviceFormat *format)
+{
+	struct v4l2_format v4l2Format = {};
+	struct v4l2_pix_format *pix = &v4l2Format.fmt.pix;
+	int ret;
+
+	v4l2Format.type = bufferType_;
+	ret = V4L2Device::ioctl(VIDIOC_G_FMT, &v4l2Format);
+	if (ret) {
+		ret = -errno;
+		LOG(V4L2, Error) << "Unable to get format: " << strerror(-ret);
+		return ret;
+	}
+
+	format->size.width = pix->width;
+	format->size.height = pix->height;
+	format->fourcc = pix->pixelformat;
+	format->planesCount = 1;
+	format->planes[0].bpl = pix->bytesperline;
+	format->planes[0].size = pix->sizeimage;
+
+	return 0;
+}
+
+int V4L2Videodev::setFormatSingleplane(V4L2DeviceFormat *format)
+{
+	struct v4l2_format v4l2Format = {};
+	struct v4l2_pix_format *pix = &v4l2Format.fmt.pix;
+	int ret;
+
+	v4l2Format.type = bufferType_;
+	pix->width = format->size.width;
+	pix->height = format->size.height;
+	pix->pixelformat = format->fourcc;
+	pix->bytesperline = format->planes[0].bpl;
+	pix->field = V4L2_FIELD_NONE;
+	ret = V4L2Device::ioctl(VIDIOC_S_FMT, &v4l2Format);
+	if (ret) {
+		ret = -errno;
+		LOG(V4L2, Error) << "Unable to set format: " << strerror(-ret);
+		return ret;
+	}
+
+	/*
+	 * Return to caller the format actually applied on the device,
+	 * which might differ from the requested one.
+	 */
+	format->size.width = pix->width;
+	format->size.height = pix->height;
+	format->fourcc = pix->pixelformat;
+	format->planesCount = 1;
+	format->planes[0].bpl = pix->bytesperline;
+	format->planes[0].size = pix->sizeimage;
+
+	return 0;
+}
+
+int V4L2Videodev::requestBuffers(unsigned int count)
+{
+	struct v4l2_requestbuffers rb = {};
+	int ret;
+
+	rb.count = count;
+	rb.type = bufferType_;
+	rb.memory = memoryType_;
+
+	ret = V4L2Device::ioctl(VIDIOC_REQBUFS, &rb);
+	if (ret < 0) {
+		ret = -errno;
+		LOG(V4L2, Error)
+			<< "Unable to request " << count << " buffers: "
+			<< strerror(-ret);
+		return ret;
+	}
+
+	LOG(V4L2, Debug) << rb.count << " buffers requested.";
+
+	return rb.count;
+}
+
+/**
+ * \brief Request buffers to be allocated from the device and stored in the
+ *  buffer pool provided.
+ * \param[out] pool BufferPool to populate with buffers
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Videodev::exportBuffers(BufferPool *pool)
+{
+	unsigned int allocatedBuffers;
+	unsigned int i;
+	int ret;
+
+	memoryType_ = V4L2_MEMORY_MMAP;
+
+	ret = requestBuffers(pool->count());
+	if (ret < 0)
+		return ret;
+
+	allocatedBuffers = ret;
+	if (allocatedBuffers < pool->count()) {
+		LOG(V4L2, Error) << "Not enough buffers provided by V4L2Videodev";
+		requestBuffers(0);
+		return -ENOMEM;
+	}
+
+	/* Map the buffers. */
+	for (i = 0; i < pool->count(); ++i) {
+		struct v4l2_plane planes[VIDEO_MAX_PLANES] = {};
+		struct v4l2_buffer buf = {};
+		Buffer &buffer = pool->buffers()[i];
+
+		buf.index = i;
+		buf.type = bufferType_;
+		buf.memory = memoryType_;
+		buf.length = VIDEO_MAX_PLANES;
+		buf.m.planes = planes;
+
+		ret = V4L2Device::ioctl(VIDIOC_QUERYBUF, &buf);
+		if (ret < 0) {
+			ret = -errno;
+			LOG(V4L2, Error)
+				<< "Unable to query buffer " << i << ": "
+				<< strerror(-ret);
+			break;
+		}
+
+		if (V4L2_TYPE_IS_MULTIPLANAR(buf.type)) {
+			for (unsigned int p = 0; p < buf.length; ++p) {
+				ret = createPlane(&buffer, p,
+						  buf.m.planes[p].length);
+				if (ret)
+					break;
+			}
+		} else {
+			ret = createPlane(&buffer, 0, buf.length);
+		}
+
+		if (ret) {
+			LOG(V4L2, Error) << "Failed to create plane";
+			break;
+		}
+	}
+
+	if (ret) {
+		requestBuffers(0);
+		pool->destroyBuffers();
+		return ret;
+	}
+
+	bufferPool_ = pool;
+
+	return 0;
+}
+
+int V4L2Videodev::createPlane(Buffer *buffer, unsigned int planeIndex,
+			    unsigned int length)
+{
+	struct v4l2_exportbuffer expbuf = {};
+	int ret;
+
+	LOG(V4L2, Debug)
+		<< "Buffer " << buffer->index()
+		<< " plane " << planeIndex
+		<< ": length=" << length;
+
+	expbuf.type = bufferType_;
+	expbuf.index = buffer->index();
+	expbuf.plane = planeIndex;
+	expbuf.flags = O_RDWR;
+
+	ret = V4L2Device::ioctl(VIDIOC_EXPBUF, &expbuf);
+	if (ret < 0) {
+		ret = -errno;
+		LOG(V4L2, Error)
+			<< "Failed to export buffer: " << strerror(-ret);
+		return ret;
+	}
+
+	buffer->planes().emplace_back();
+	Plane &plane = buffer->planes().back();
+	plane.setDmabuf(expbuf.fd, length);
+	::close(expbuf.fd);
+
+	return 0;
+}
+
+/**
+ * \brief Import the externally allocated \a pool of buffers
+ * \param[in] pool BufferPool of buffers to import
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Videodev::importBuffers(BufferPool *pool)
+{
+	unsigned int allocatedBuffers;
+	int ret;
+
+	memoryType_ = V4L2_MEMORY_DMABUF;
+
+	ret = requestBuffers(pool->count());
+	if (ret < 0)
+		return ret;
+
+	allocatedBuffers = ret;
+	if (allocatedBuffers < pool->count()) {
+		LOG(V4L2, Error)
+			<< "Not enough buffers provided by V4L2Videodev";
+		requestBuffers(0);
+		return -ENOMEM;
+	}
+
+	LOG(V4L2, Debug) << "provided pool of " << pool->count() << " buffers";
+	bufferPool_ = pool;
+
+	return 0;
+}
+
+/**
+ * \brief Release all internally allocated buffers
+ */
+int V4L2Videodev::releaseBuffers()
+{
+	LOG(V4L2, Debug) << "Releasing bufferPool";
+
+	bufferPool_ = nullptr;
+
+	return requestBuffers(0);
+}
+
+/**
+ * \brief Queue a buffer into the device
+ * \param[in] buffer The buffer to be queued
+ *
+ * For capture devices the \a buffer will be filled with data by the device.
+ * For output devices the \a buffer shall contain valid data and will be
+ * processed by the device. Once the device has finished processing the buffer,
+ * it will be available for dequeue.
+ *
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Videodev::queueBuffer(Buffer *buffer)
+{
+	struct v4l2_buffer buf = {};
+	struct v4l2_plane planes[VIDEO_MAX_PLANES] = {};
+	int ret;
+
+	buf.index = buffer->index();
+	buf.type = bufferType_;
+	buf.memory = memoryType_;
+	buf.field = V4L2_FIELD_NONE;
+
+	bool multiPlanar = V4L2_TYPE_IS_MULTIPLANAR(buf.type);
+
+	if (buf.memory == V4L2_MEMORY_DMABUF) {
+		if (multiPlanar) {
+			for (unsigned int p = 0;
+			     p < buffer->planes().size();
+			     p++)
+				planes[p].m.fd = buffer->planes()[p].dmabuf();
+		} else {
+			buf.m.fd = buffer->planes()[0].dmabuf();
+		}
+	}
+
+	if (multiPlanar) {
+		buf.length = buffer->planes().size();
+		buf.m.planes = planes;
+	}
+
+	if (V4L2_TYPE_IS_OUTPUT(bufferType_)) {
+		buf.bytesused = buffer->bytesused_;
+		buf.sequence = buffer->sequence_;
+		buf.timestamp.tv_sec = buffer->timestamp_ / 1000000000;
+		buf.timestamp.tv_usec = (buffer->timestamp_ / 1000) % 1000000;
+	}
+
+	LOG(V4L2, Debug) << "Queueing buffer " << buf.index;
+
+	ret = V4L2Device::ioctl(VIDIOC_QBUF, &buf);
+	if (ret < 0) {
+		ret = -errno;
+		LOG(V4L2, Error)
+			<< "Failed to queue buffer " << buf.index << ": "
+			<< strerror(-ret);
+		return ret;
+	}
+
+	if (queuedBuffersCount_++ == 0)
+		fdEvent_->setEnabled(true);
+
+	return 0;
+}
+
+/**
+ * \brief Dequeue the next available buffer from the device
+ *
+ * This method dequeues the next available buffer from the device. If no buffer
+ * is available to be dequeued it will return nullptr immediately.
+ *
+ * \return A pointer to the dequeued buffer on success, or nullptr otherwise
+ */
+Buffer *V4L2Videodev::dequeueBuffer()
+{
+	struct v4l2_buffer buf = {};
+	struct v4l2_plane planes[VIDEO_MAX_PLANES] = {};
+	int ret;
+
+	buf.type = bufferType_;
+	buf.memory = memoryType_;
+
+	if (V4L2_TYPE_IS_MULTIPLANAR(buf.type)) {
+		buf.length = VIDEO_MAX_PLANES;
+		buf.m.planes = planes;
+	}
+
+	ret = V4L2Device::ioctl(VIDIOC_DQBUF, &buf);
+	if (ret < 0) {
+		ret = -errno;
+		LOG(V4L2, Error)
+			<< "Failed to dequeue buffer: " << strerror(-ret);
+		return nullptr;
+	}
+
+	ASSERT(buf.index < bufferPool_->count());
+
+	if (--queuedBuffersCount_ == 0)
+		fdEvent_->setEnabled(false);
+
+	Buffer *buffer = &bufferPool_->buffers()[buf.index];
+
+	buffer->bytesused_ = buf.bytesused;
+	buffer->timestamp_ = buf.timestamp.tv_sec * 1000000000ULL
+			   + buf.timestamp.tv_usec * 1000ULL;
+	buffer->sequence_ = buf.sequence;
+	buffer->status_ = buf.flags & V4L2_BUF_FLAG_ERROR
+			? Buffer::BufferError : Buffer::BufferSuccess;
+
+	return buffer;
+}
+
+/**
+ * \brief Slot to handle completed buffer events from the V4L2 device
+ * \param[in] notifier The event notifier
+ *
+ * When this slot is called, a Buffer has become available from the device, and
+ * will be emitted through the bufferReady Signal.
+ *
+ * For Capture devices the Buffer will contain valid data.
+ * For Output devices the Buffer can be considered empty.
+ */
+void V4L2Videodev::bufferAvailable(EventNotifier *notifier)
+{
+	Buffer *buffer = dequeueBuffer();
+	if (!buffer)
+		return;
+
+	LOG(V4L2, Debug) << "Buffer " << buffer->index() << " is available";
+
+	/* Notify anyone listening to the device. */
+	bufferReady.emit(buffer);
+}
+
+/**
+ * \var V4L2Videodev::bufferReady
+ * \brief A Signal emitted when a buffer completes
+ */
+
+/**
+ * \brief Start the video stream
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Videodev::streamOn()
+{
+	int ret;
+
+	ret = V4L2Device::ioctl(VIDIOC_STREAMON, &bufferType_);
+	if (ret < 0) {
+		ret = -errno;
+		LOG(V4L2, Error)
+			<< "Failed to start streaming: " << strerror(-ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+/**
+ * \brief Stop the video stream
+ *
+ * Buffers that are still queued when the video stream is stopped are
+ * implicitly dequeued, but no bufferReady signal is emitted for them.
+ *
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Videodev::streamOff()
+{
+	int ret;
+
+	ret = V4L2Device::ioctl(VIDIOC_STREAMOFF, &bufferType_);
+	if (ret < 0) {
+		ret = -errno;
+		LOG(V4L2, Error)
+			<< "Failed to stop streaming: " << strerror(-ret);
+		return ret;
+	}
+
+	queuedBuffersCount_ = 0;
+	fdEvent_->setEnabled(false);
+
+	return 0;
+}
+
+/**
+ * \brief Create a new video device instance from \a entity in media device
+ * \a media
+ * \param[in] media The media device where the entity is registered
+ * \param[in] entity The media entity name
+ *
+ * Releasing memory of the newly created instance is responsibility of the
+ * caller of this function.
+ *
+ * \return A newly created V4L2Videodev on success, nullptr otherwise
+ */
+V4L2Videodev *V4L2Videodev::fromEntityName(const MediaDevice *media,
+					   const std::string &entity)
+{
+	MediaEntity *mediaEntity = media->getEntityByName(entity);
+	if (!mediaEntity)
+		return nullptr;
+
+	return new V4L2Videodev(mediaEntity);
+}
+
+} /* namespace libcamera */
diff --git a/test/meson.build b/test/meson.build
index 609aeab80e7d..f23b560afc73 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -4,8 +4,8 @@ subdir('camera')
 subdir('ipa')
 subdir('media_device')
 subdir('pipeline')
-subdir('v4l2_device')
-subdir('v4l2_subdevice')
+subdir('v4l2_videodev')
+subdir('v4l2_subdev')

 public_tests = [
     ['event',                           'event.cpp'],
diff --git a/test/v4l2_subdevice/list_formats.cpp b/test/v4l2_subdev/list_formats.cpp
similarity index 95%
rename from test/v4l2_subdevice/list_formats.cpp
rename to test/v4l2_subdev/list_formats.cpp
index 3f0edafcdcd7..cd9d8b58850d 100644
--- a/test/v4l2_subdevice/list_formats.cpp
+++ b/test/v4l2_subdev/list_formats.cpp
@@ -11,15 +11,15 @@

 #include <libcamera/geometry.h>

-#include "v4l2_subdevice.h"
-#include "v4l2_subdevice_test.h"
+#include "v4l2_subdev.h"
+#include "v4l2_subdev_test.h"

 using namespace std;
 using namespace libcamera;

 /* List image formats on the "Scaler" subdevice of vimc media device.  */

-class ListFormatsTest : public V4L2SubdeviceTest
+class ListFormatsTest : public V4L2SubdevTest
 {
 protected:
 	int run() override;
diff --git a/test/v4l2_subdevice/meson.build b/test/v4l2_subdev/meson.build
similarity index 57%
rename from test/v4l2_subdevice/meson.build
rename to test/v4l2_subdev/meson.build
index 0521984b2a78..57fbead2170e 100644
--- a/test/v4l2_subdevice/meson.build
+++ b/test/v4l2_subdev/meson.build
@@ -1,12 +1,12 @@
-v4l2_subdevice_tests = [
+v4l2_subdev_tests = [
   [ 'list_formats',             'list_formats.cpp'],
   [ 'test_formats',             'test_formats.cpp'],
 ]

-foreach t : v4l2_subdevice_tests
-    exe = executable(t[0], [t[1], 'v4l2_subdevice_test.cpp'],
+foreach t : v4l2_subdev_tests
+    exe = executable(t[0], [t[1], 'v4l2_subdev_test.cpp'],
         dependencies : libcamera_dep,
         link_with : test_libraries,
         include_directories : test_includes_internal)
-    test(t[0], exe, suite : 'v4l2_subdevice', is_parallel : false)
+    test(t[0], exe, suite : 'v4l2_subdev', is_parallel : false)
 endforeach
diff --git a/test/v4l2_subdevice/test_formats.cpp b/test/v4l2_subdev/test_formats.cpp
similarity index 93%
rename from test/v4l2_subdevice/test_formats.cpp
rename to test/v4l2_subdev/test_formats.cpp
index e90c2c2426da..819264c1a97e 100644
--- a/test/v4l2_subdevice/test_formats.cpp
+++ b/test/v4l2_subdev/test_formats.cpp
@@ -8,15 +8,15 @@
 #include <climits>
 #include <iostream>

-#include "v4l2_subdevice.h"
-#include "v4l2_subdevice_test.h"
+#include "v4l2_subdev.h"
+#include "v4l2_subdev_test.h"

 using namespace std;
 using namespace libcamera;

 /* Test format handling on the "Scaler" subdevice of vimc media device.  */

-class FormatHandlingTest : public V4L2SubdeviceTest
+class FormatHandlingTest : public V4L2SubdevTest
 {
 protected:
 	int run() override;
diff --git a/test/v4l2_subdevice/v4l2_subdevice_test.cpp b/test/v4l2_subdev/v4l2_subdev_test.cpp
similarity index 84%
rename from test/v4l2_subdevice/v4l2_subdevice_test.cpp
rename to test/v4l2_subdev/v4l2_subdev_test.cpp
index 562a638cb28e..c9f26c5f88f4 100644
--- a/test/v4l2_subdevice/v4l2_subdevice_test.cpp
+++ b/test/v4l2_subdev/v4l2_subdev_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_subdevice_test.cpp - VIMC-based V4L2 subdevice test
+ * v4l2_subdev_test.cpp - VIMC-based V4L2 subdevice test
  */

 #include <iostream>
@@ -11,8 +11,8 @@

 #include "device_enumerator.h"
 #include "media_device.h"
-#include "v4l2_subdevice.h"
-#include "v4l2_subdevice_test.h"
+#include "v4l2_subdev.h"
+#include "v4l2_subdev_test.h"

 using namespace std;
 using namespace libcamera;
@@ -25,7 +25,7 @@ using namespace libcamera;
  * If the vimc module is not loaded, the test gets skipped.
  */

-int V4L2SubdeviceTest::init()
+int V4L2SubdevTest::init()
 {
 	enumerator_ = DeviceEnumerator::create();
 	if (!enumerator_) {
@@ -51,7 +51,7 @@ int V4L2SubdeviceTest::init()
 		return TestFail;
 	}

-	scaler_ = new V4L2Subdevice(videoEntity);
+	scaler_ = new V4L2Subdev(videoEntity);
 	if (scaler_->open()) {
 		cerr << "Unable to open video subdevice "
 		     << scaler_->entity()->deviceNode() << endl;
@@ -61,7 +61,7 @@ int V4L2SubdeviceTest::init()
 	return 0;
 }

-void V4L2SubdeviceTest::cleanup()
+void V4L2SubdevTest::cleanup()
 {
 	delete scaler_;
 }
diff --git a/test/v4l2_subdevice/v4l2_subdevice_test.h b/test/v4l2_subdev/v4l2_subdev_test.h
similarity index 76%
rename from test/v4l2_subdevice/v4l2_subdevice_test.h
rename to test/v4l2_subdev/v4l2_subdev_test.h
index 96646a155536..9e332daf9aec 100644
--- a/test/v4l2_subdevice/v4l2_subdevice_test.h
+++ b/test/v4l2_subdev/v4l2_subdev_test.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_subdevice_test.h - VIMC-based V4L2 subdevice test
+ * v4l2_subdev_test.h - VIMC-based V4L2 subdevice test
  */

 #ifndef __LIBCAMERA_V4L2_SUBDEVICE_TEST_H__
@@ -13,14 +13,14 @@
 #include "device_enumerator.h"
 #include "media_device.h"
 #include "test.h"
-#include "v4l2_subdevice.h"
+#include "v4l2_subdev.h"

 using namespace libcamera;

-class V4L2SubdeviceTest : public Test
+class V4L2SubdevTest : public Test
 {
 public:
-	V4L2SubdeviceTest()
+	V4L2SubdevTest()
 		: scaler_(nullptr){};

 protected:
@@ -29,7 +29,7 @@ protected:

 	std::unique_ptr<DeviceEnumerator> enumerator_;
 	std::shared_ptr<MediaDevice> media_;
-	V4L2Subdevice *scaler_;
+	V4L2Subdev *scaler_;
 };

 #endif /* __LIBCAMERA_V4L2_SUBDEVICE_TEST_H__ */
diff --git a/test/v4l2_device/buffer_sharing.cpp b/test/v4l2_videodev/buffer_sharing.cpp
similarity index 90%
rename from test/v4l2_device/buffer_sharing.cpp
rename to test/v4l2_videodev/buffer_sharing.cpp
index e63ddff85689..a997cd69af5c 100644
--- a/test/v4l2_device/buffer_sharing.cpp
+++ b/test/v4l2_videodev/buffer_sharing.cpp
@@ -4,8 +4,8 @@
  *
  * libcamera V4L2 API tests
  *
- * Validate the function of exporting buffers from a V4L2Device and
- * the ability to import them to another V4L2Device instance.
+ * Validate the function of exporting buffers from a V4L2Videodev and
+ * the ability to import them to another V4L2Videodev instance.
  * Ensure that the Buffers can successfully be queued and dequeued
  * between both devices.
  */
@@ -17,19 +17,19 @@
 #include <libcamera/event_dispatcher.h>
 #include <libcamera/timer.h>

-#include "v4l2_device_test.h"
+#include "v4l2_videodev_test.h"

-class BufferSharingTest : public V4L2DeviceTest
+class BufferSharingTest : public V4L2VideodevTest
 {
 public:
 	BufferSharingTest()
-		: V4L2DeviceTest("vivid", "vivid-000-vid-cap"),
+		: V4L2VideodevTest("vivid", "vivid-000-vid-cap"),
 		  output_(nullptr), framesCaptured_(0), framesOutput_(0) {}

 protected:
 	int init()
 	{
-		int ret = V4L2DeviceTest::init();
+		int ret = V4L2VideodevTest::init();
 		if (ret)
 			return ret;

@@ -38,7 +38,7 @@ protected:
 		if (!entity)
 			return TestSkip;

-		output_ = new V4L2Device(entity);
+		output_ = new V4L2Videodev(entity);
 		if (!output_) {
 			std::cout << "Failed to create output device" << std::endl;
 			return TestFail;
@@ -171,13 +171,13 @@ protected:

 		delete output_;

-		V4L2DeviceTest::cleanup();
+		V4L2VideodevTest::cleanup();
 	}

 private:
 	const unsigned int bufferCount = 4;

-	V4L2Device *output_;
+	V4L2Videodev *output_;

 	unsigned int framesCaptured_;
 	unsigned int framesOutput_;
diff --git a/test/v4l2_device/capture_async.cpp b/test/v4l2_videodev/capture_async.cpp
similarity index 92%
rename from test/v4l2_device/capture_async.cpp
rename to test/v4l2_videodev/capture_async.cpp
index 69b1d5a13ed8..97b2a9d9f246 100644
--- a/test/v4l2_device/capture_async.cpp
+++ b/test/v4l2_videodev/capture_async.cpp
@@ -12,13 +12,13 @@

 #include <iostream>

-#include "v4l2_device_test.h"
+#include "v4l2_videodev_test.h"

-class CaptureAsyncTest : public V4L2DeviceTest
+class CaptureAsyncTest : public V4L2VideodevTest
 {
 public:
 	CaptureAsyncTest()
-		: V4L2DeviceTest("vimc", "Raw Capture 0"), frames(0) {}
+		: V4L2VideodevTest("vimc", "Raw Capture 0"), frames(0) {}

 	void receiveBuffer(Buffer *buffer)
 	{
diff --git a/test/v4l2_device/double_open.cpp b/test/v4l2_videodev/double_open.cpp
similarity index 76%
rename from test/v4l2_device/double_open.cpp
rename to test/v4l2_videodev/double_open.cpp
index 53850620889b..b679be3b3319 100644
--- a/test/v4l2_device/double_open.cpp
+++ b/test/v4l2_videodev/double_open.cpp
@@ -7,15 +7,15 @@

 #include <iostream>

-#include "v4l2_device_test.h"
+#include "v4l2_videodev_test.h"

 namespace {

-class DoubleOpen : public V4L2DeviceTest
+class DoubleOpen : public V4L2VideodevTest
 {
 public:
 	DoubleOpen()
-		: V4L2DeviceTest("vimc", "Raw Capture 0") {}
+		: V4L2VideodevTest("vimc", "Raw Capture 0") {}
 protected:
 	int run()
 	{
@@ -23,7 +23,7 @@ protected:

 		/*
 		 * Expect failure: The device has already been opened by the
-		 * V4L2DeviceTest base class
+		 * V4L2VideodevTest base class
 		 */
 		ret = capture_->open();
 		if (!ret) {
diff --git a/test/v4l2_device/formats.cpp b/test/v4l2_videodev/formats.cpp
similarity index 86%
rename from test/v4l2_device/formats.cpp
rename to test/v4l2_videodev/formats.cpp
index 6be045ff754c..80b92a560c7a 100644
--- a/test/v4l2_device/formats.cpp
+++ b/test/v4l2_videodev/formats.cpp
@@ -8,18 +8,18 @@
 #include <climits>
 #include <iostream>

-#include "v4l2_device.h"
+#include "v4l2_videodev.h"

-#include "v4l2_device_test.h"
+#include "v4l2_videodev_test.h"

 using namespace std;
 using namespace libcamera;

-class Format : public V4L2DeviceTest
+class Format : public V4L2VideodevTest
 {
 public:
 	Format()
-		: V4L2DeviceTest("vimc", "Raw Capture 0") {}
+		: V4L2VideodevTest("vimc", "Raw Capture 0") {}
 protected:
 	int run()
 	{
diff --git a/test/v4l2_device/meson.build b/test/v4l2_videodev/meson.build
similarity index 75%
rename from test/v4l2_device/meson.build
rename to test/v4l2_videodev/meson.build
index de540b1ba6f5..34eb33d1c3ec 100644
--- a/test/v4l2_device/meson.build
+++ b/test/v4l2_videodev/meson.build
@@ -1,6 +1,6 @@
 # Tests are listed in order of complexity.
 # They are not alphabetically sorted.
-v4l2_device_tests = [
+v4l2_videodev_tests = [
     [ 'double_open',        'double_open.cpp' ],
     [ 'formats',            'formats.cpp' ],
     [ 'request_buffers',    'request_buffers.cpp' ],
@@ -9,10 +9,10 @@ v4l2_device_tests = [
     [ 'buffer_sharing',     'buffer_sharing.cpp' ],
 ]

-foreach t : v4l2_device_tests
-    exe = executable(t[0], [t[1], 'v4l2_device_test.cpp'],
+foreach t : v4l2_videodev_tests
+    exe = executable(t[0], [t[1], 'v4l2_videodev_test.cpp'],
                      dependencies : libcamera_dep,
                      link_with : test_libraries,
                      include_directories : test_includes_internal)
-    test(t[0], exe, suite : 'v4l2_device', is_parallel : false)
+    test(t[0], exe, suite : 'v4l2_videodev', is_parallel : false)
 endforeach
diff --git a/test/v4l2_device/request_buffers.cpp b/test/v4l2_videodev/request_buffers.cpp
similarity index 78%
rename from test/v4l2_device/request_buffers.cpp
rename to test/v4l2_videodev/request_buffers.cpp
index 7b7b06b2b409..493605e79746 100644
--- a/test/v4l2_device/request_buffers.cpp
+++ b/test/v4l2_videodev/request_buffers.cpp
@@ -5,13 +5,13 @@
  * libcamera V4L2 API tests
  */

-#include "v4l2_device_test.h"
+#include "v4l2_videodev_test.h"

-class RequestBuffersTest : public V4L2DeviceTest
+class RequestBuffersTest : public V4L2VideodevTest
 {
 public:
 	RequestBuffersTest()
-		: V4L2DeviceTest("vimc", "Raw Capture 0") {}
+		: V4L2VideodevTest("vimc", "Raw Capture 0") {}

 protected:
 	int run()
diff --git a/test/v4l2_device/stream_on_off.cpp b/test/v4l2_videodev/stream_on_off.cpp
similarity index 79%
rename from test/v4l2_device/stream_on_off.cpp
rename to test/v4l2_videodev/stream_on_off.cpp
index b158b8e402c8..fefe48c7ad39 100644
--- a/test/v4l2_device/stream_on_off.cpp
+++ b/test/v4l2_videodev/stream_on_off.cpp
@@ -5,13 +5,13 @@
  * libcamera V4L2 API tests
  */

-#include "v4l2_device_test.h"
+#include "v4l2_videodev_test.h"

-class StreamOnStreamOffTest : public V4L2DeviceTest
+class StreamOnStreamOffTest : public V4L2VideodevTest
 {
 public:
 	StreamOnStreamOffTest()
-		: V4L2DeviceTest("vimc", "Raw Capture 0") {}
+		: V4L2VideodevTest("vimc", "Raw Capture 0") {}
 protected:
 	int run()
 	{
diff --git a/test/v4l2_device/v4l2_device_test.cpp b/test/v4l2_videodev/v4l2_videodev_test.cpp
similarity index 91%
rename from test/v4l2_device/v4l2_device_test.cpp
rename to test/v4l2_videodev/v4l2_videodev_test.cpp
index baad48f8aa9e..b3ed503567ee 100644
--- a/test/v4l2_device/v4l2_device_test.cpp
+++ b/test/v4l2_videodev/v4l2_videodev_test.cpp
@@ -8,7 +8,7 @@
 #include <iostream>
 #include <sys/stat.h>

-#include "v4l2_device_test.h"
+#include "v4l2_videodev_test.h"

 #include "device_enumerator.h"
 #include "media_device.h"
@@ -26,7 +26,7 @@ bool exists(const std::string &path)
 	return false;
 }

-int V4L2DeviceTest::init()
+int V4L2VideodevTest::init()
 {
 	enumerator_ = DeviceEnumerator::create();
 	if (!enumerator_) {
@@ -50,7 +50,7 @@ int V4L2DeviceTest::init()
 	if (!entity)
 		return TestSkip;

-	capture_ = new V4L2Device(entity);
+	capture_ = new V4L2Videodev(entity);
 	if (!capture_)
 		return TestFail;

@@ -77,7 +77,7 @@ int V4L2DeviceTest::init()
 	return TestPass;
 }

-void V4L2DeviceTest::cleanup()
+void V4L2VideodevTest::cleanup()
 {
 	capture_->streamOff();
 	capture_->releaseBuffers();
diff --git a/test/v4l2_device/v4l2_device_test.h b/test/v4l2_videodev/v4l2_videodev_test.h
similarity index 82%
rename from test/v4l2_device/v4l2_device_test.h
rename to test/v4l2_videodev/v4l2_videodev_test.h
index 651c005f4e54..6298961b3b15 100644
--- a/test/v4l2_device/v4l2_device_test.h
+++ b/test/v4l2_videodev/v4l2_videodev_test.h
@@ -15,14 +15,14 @@

 #include "device_enumerator.h"
 #include "media_device.h"
-#include "v4l2_device.h"
+#include "v4l2_videodev.h"

 using namespace libcamera;

-class V4L2DeviceTest : public Test
+class V4L2VideodevTest : public Test
 {
 public:
-	V4L2DeviceTest(const char *driver, const char *entity)
+	V4L2VideodevTest(const char *driver, const char *entity)
 		: driver_(driver), entity_(entity), capture_(nullptr)
 	{
 	}
@@ -35,7 +35,7 @@ protected:
 	std::string entity_;
 	std::unique_ptr<DeviceEnumerator> enumerator_;
 	std::shared_ptr<MediaDevice> media_;
-	V4L2Device *capture_;
+	V4L2Videodev *capture_;
 	BufferPool pool_;
 };

