diff --git a/meson.build b/meson.build
index 9eee9d39..f00e3daa 100644
--- a/meson.build
+++ b/meson.build
@@ -176,6 +176,7 @@ pipelines_support = {
     'simple':       arch_arm,
     'uvcvideo':     ['any'],
     'vimc':         ['test'],
+    'virtual':      ['test'],
 }
 
 if pipelines.contains('all')
diff --git a/meson_options.txt b/meson_options.txt
index 78a78b72..a0a75e7f 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -47,7 +47,8 @@ option('pipelines',
             'rkisp1',
             'simple',
             'uvcvideo',
-            'vimc'
+            'vimc',
+            'virtual'
         ],
         description : 'Select which pipeline handlers to build. If this is set to "auto", all the pipelines applicable to the target architecture will be built. If this is set to "all", all the pipelines will be built. If both are selected then "all" will take precedence.')
 
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 */
