diff --git a/meson_options.txt b/meson_options.txt
index 1ba6778c..08876f4c 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -37,7 +37,7 @@ option('lc-compliance',
 
 option('pipelines',
         type : 'array',
-        choices : ['imx8-isi', 'ipu3', 'raspberrypi', 'rkisp1', 'simple', 'uvcvideo', 'vimc'],
+        choices : ['imx8-isi', 'ipu3', 'raspberrypi', 'rkisp1', 'simple', 'uvcvideo', 'vimc', 'virtual'],
         description : 'Select which pipeline handlers to include')
 
 option('qcam',
diff --git a/src/libcamera/pipeline/virtual/meson.build b/src/libcamera/pipeline/virtual/meson.build
new file mode 100644
index 00000000..ba7ff754
--- /dev/null
+++ b/src/libcamera/pipeline/virtual/meson.build
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: CC0-1.0
+
+libcamera_sources += files([
+    'virtual.cpp',
+])
diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp
new file mode 100644
index 00000000..09583b4e
--- /dev/null
+++ b/src/libcamera/pipeline/virtual/virtual.cpp
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2022, Google Inc.
+ *
+ * fake.cpp - Pipeline handler for fake cameras
+ */
+
+#include <libcamera/base/log.h>
+
+#include <libcamera/camera.h>
+
+#include "libcamera/internal/pipeline_handler.h"
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(VIRTUAL)
+
+class VirtualCameraConfiguration : public CameraConfiguration
+{
+public:
+	VirtualCameraConfiguration();
+
+	Status validate() override;
+};
+
+class PipelineHandlerVirtual : public PipelineHandler
+{
+public:
+	PipelineHandlerVirtual(CameraManager *manager);
+
+	std::unique_ptr<CameraConfiguration> generateConfiguration(Camera *camera,
+								   const StreamRoles &roles) override;
+	int configure(Camera *camera, CameraConfiguration *config) override;
+
+	int exportFrameBuffers(Camera *camera, Stream *stream,
+			       std::vector<std::unique_ptr<FrameBuffer>> *buffers) override;
+
+	int start(Camera *camera, const ControlList *controls) override;
+	void stopDevice(Camera *camera) override;
+
+	int queueRequestDevice(Camera *camera, Request *request) override;
+
+	bool match(DeviceEnumerator *enumerator) override;
+};
+
+VirtualCameraConfiguration::VirtualCameraConfiguration()
+	: CameraConfiguration()
+{
+}
+
+CameraConfiguration::Status VirtualCameraConfiguration::validate()
+{
+	return Invalid;
+}
+
+PipelineHandlerVirtual::PipelineHandlerVirtual(CameraManager *manager)
+	: PipelineHandler(manager)
+{
+}
+
+std::unique_ptr<CameraConfiguration> PipelineHandlerVirtual::generateConfiguration(Camera *camera,
+										   const StreamRoles &roles)
+{
+	(void)camera;
+	(void)roles;
+	return std::unique_ptr<VirtualCameraConfiguration>(nullptr);
+}
+
+int PipelineHandlerVirtual::configure(Camera *camera, CameraConfiguration *config)
+{
+	(void)camera;
+	(void)config;
+	return -1;
+}
+
+int PipelineHandlerVirtual::exportFrameBuffers(Camera *camera, Stream *stream,
+					       std::vector<std::unique_ptr<FrameBuffer>> *buffers)
+{
+	(void)camera;
+	(void)stream;
+	(void)buffers;
+	return -1;
+}
+
+int PipelineHandlerVirtual::start(Camera *camera, const ControlList *controls)
+{
+	(void)camera;
+	(void)controls;
+	return -1;
+}
+
+void PipelineHandlerVirtual::stopDevice(Camera *camera)
+{
+	(void)camera;
+}
+
+int PipelineHandlerVirtual::queueRequestDevice(Camera *camera, Request *request)
+{
+	(void)camera;
+	(void)request;
+	return -1;
+}
+
+bool PipelineHandlerVirtual::match(DeviceEnumerator *enumerator)
+{
+	(void)enumerator;
+	return false;
+}
+
+REGISTER_PIPELINE_HANDLER(PipelineHandlerVirtual)
+
+} /* namespace libcamera */
