diff --git a/meson_options.txt b/meson_options.txt
index badace151bb6..dc4684df49b2 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -16,7 +16,7 @@ option('gstreamer',
 
 option('pipelines',
         type : 'array',
-        choices : ['ipu3', 'raspberrypi', 'rkisp1', 'simple', 'uvcvideo', 'vimc'],
+        choices : ['ipu3', 'raspberrypi', 'rkisp1', 'simple', 'uvcvideo', 'vimc', 'vivid'],
         description : 'Select which pipeline handlers to include')
 
 option('test',
diff --git a/src/libcamera/pipeline/vivid/meson.build b/src/libcamera/pipeline/vivid/meson.build
new file mode 100644
index 000000000000..086bb825387c
--- /dev/null
+++ b/src/libcamera/pipeline/vivid/meson.build
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: CC0-1.0
+
+libcamera_sources += files([
+    'vivid.cpp',
+])
diff --git a/src/libcamera/pipeline/vivid/vivid.cpp b/src/libcamera/pipeline/vivid/vivid.cpp
new file mode 100644
index 000000000000..b811e33a0299
--- /dev/null
+++ b/src/libcamera/pipeline/vivid/vivid.cpp
@@ -0,0 +1,441 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018, Google Inc.
+ *
+ * vivid.cpp - Pipeline handler for the vivid capture device
+ */
+
+#include <algorithm>
+#include <iomanip>
+#include <map>
+#include <math.h>
+#include <tuple>
+
+#include <linux/media-bus-format.h>
+#include <linux/version.h>
+
+#include <libcamera/camera.h>
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
+#include <libcamera/ipa/ipa_interface.h>
+#include <libcamera/ipa/ipa_module_info.h>
+#include <libcamera/request.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/camera_sensor.h"
+#include "libcamera/internal/device_enumerator.h"
+#include "libcamera/internal/ipa_manager.h"
+#include "libcamera/internal/log.h"
+#include "libcamera/internal/media_device.h"
+#include "libcamera/internal/pipeline_handler.h"
+#include "libcamera/internal/utils.h"
+#include "libcamera/internal/v4l2_controls.h"
+#include "libcamera/internal/v4l2_subdevice.h"
+#include "libcamera/internal/v4l2_videodevice.h"
+
+#define VIVID_CID_CUSTOM_BASE           (V4L2_CID_USER_BASE | 0xf000)
+#define VIVID_CID_BUTTON                (VIVID_CID_CUSTOM_BASE + 0)
+#define VIVID_CID_BOOLEAN               (VIVID_CID_CUSTOM_BASE + 1)
+#define VIVID_CID_INTEGER               (VIVID_CID_CUSTOM_BASE + 2)
+#define VIVID_CID_INTEGER64             (VIVID_CID_CUSTOM_BASE + 3)
+#define VIVID_CID_MENU                  (VIVID_CID_CUSTOM_BASE + 4)
+#define VIVID_CID_STRING                (VIVID_CID_CUSTOM_BASE + 5)
+#define VIVID_CID_BITMASK               (VIVID_CID_CUSTOM_BASE + 6)
+#define VIVID_CID_INTMENU               (VIVID_CID_CUSTOM_BASE + 7)
+#define VIVID_CID_U32_ARRAY             (VIVID_CID_CUSTOM_BASE + 8)
+#define VIVID_CID_U16_MATRIX            (VIVID_CID_CUSTOM_BASE + 9)
+#define VIVID_CID_U8_4D_ARRAY           (VIVID_CID_CUSTOM_BASE + 10)
+
+#define VIVID_CID_VIVID_BASE            (0x00f00000 | 0xf000)
+#define VIVID_CID_VIVID_CLASS           (0x00f00000 | 1)
+#define VIVID_CID_TEST_PATTERN          (VIVID_CID_VIVID_BASE + 0)
+#define VIVID_CID_OSD_TEXT_MODE         (VIVID_CID_VIVID_BASE + 1)
+#define VIVID_CID_HOR_MOVEMENT          (VIVID_CID_VIVID_BASE + 2)
+#define VIVID_CID_VERT_MOVEMENT         (VIVID_CID_VIVID_BASE + 3)
+#define VIVID_CID_SHOW_BORDER           (VIVID_CID_VIVID_BASE + 4)
+#define VIVID_CID_SHOW_SQUARE           (VIVID_CID_VIVID_BASE + 5)
+#define VIVID_CID_INSERT_SAV            (VIVID_CID_VIVID_BASE + 6)
+#define VIVID_CID_INSERT_EAV            (VIVID_CID_VIVID_BASE + 7)
+#define VIVID_CID_VBI_CAP_INTERLACED    (VIVID_CID_VIVID_BASE + 8)
+
+#define VIVID_CID_HFLIP                 (VIVID_CID_VIVID_BASE + 20)
+#define VIVID_CID_VFLIP                 (VIVID_CID_VIVID_BASE + 21)
+#define VIVID_CID_STD_ASPECT_RATIO      (VIVID_CID_VIVID_BASE + 22)
+#define VIVID_CID_DV_TIMINGS_ASPECT_RATIO       (VIVID_CID_VIVID_BASE + 23)
+#define VIVID_CID_TSTAMP_SRC            (VIVID_CID_VIVID_BASE + 24)
+#define VIVID_CID_COLORSPACE            (VIVID_CID_VIVID_BASE + 25)
+#define VIVID_CID_XFER_FUNC             (VIVID_CID_VIVID_BASE + 26)
+#define VIVID_CID_YCBCR_ENC             (VIVID_CID_VIVID_BASE + 27)
+#define VIVID_CID_QUANTIZATION          (VIVID_CID_VIVID_BASE + 28)
+#define VIVID_CID_LIMITED_RGB_RANGE     (VIVID_CID_VIVID_BASE + 29)
+#define VIVID_CID_ALPHA_MODE            (VIVID_CID_VIVID_BASE + 30)
+#define VIVID_CID_HAS_CROP_CAP          (VIVID_CID_VIVID_BASE + 31)
+#define VIVID_CID_HAS_COMPOSE_CAP       (VIVID_CID_VIVID_BASE + 32)
+#define VIVID_CID_HAS_SCALER_CAP        (VIVID_CID_VIVID_BASE + 33)
+#define VIVID_CID_HAS_CROP_OUT          (VIVID_CID_VIVID_BASE + 34)
+#define VIVID_CID_HAS_COMPOSE_OUT       (VIVID_CID_VIVID_BASE + 35)
+#define VIVID_CID_HAS_SCALER_OUT        (VIVID_CID_VIVID_BASE + 36)
+#define VIVID_CID_LOOP_VIDEO            (VIVID_CID_VIVID_BASE + 37)
+#define VIVID_CID_SEQ_WRAP              (VIVID_CID_VIVID_BASE + 38)
+#define VIVID_CID_TIME_WRAP             (VIVID_CID_VIVID_BASE + 39)
+#define VIVID_CID_MAX_EDID_BLOCKS       (VIVID_CID_VIVID_BASE + 40)
+#define VIVID_CID_PERCENTAGE_FILL       (VIVID_CID_VIVID_BASE + 41)
+#define VIVID_CID_REDUCED_FPS           (VIVID_CID_VIVID_BASE + 42)
+#define VIVID_CID_HSV_ENC               (VIVID_CID_VIVID_BASE + 43)
+#define VIVID_CID_DISPLAY_PRESENT       (VIVID_CID_VIVID_BASE + 44)
+
+#define VIVID_CID_STD_SIGNAL_MODE       (VIVID_CID_VIVID_BASE + 60)
+#define VIVID_CID_STANDARD              (VIVID_CID_VIVID_BASE + 61)
+#define VIVID_CID_DV_TIMINGS_SIGNAL_MODE        (VIVID_CID_VIVID_BASE + 62)
+#define VIVID_CID_DV_TIMINGS            (VIVID_CID_VIVID_BASE + 63)
+#define VIVID_CID_PERC_DROPPED          (VIVID_CID_VIVID_BASE + 64)
+#define VIVID_CID_DISCONNECT            (VIVID_CID_VIVID_BASE + 65)
+#define VIVID_CID_DQBUF_ERROR           (VIVID_CID_VIVID_BASE + 66)
+#define VIVID_CID_QUEUE_SETUP_ERROR     (VIVID_CID_VIVID_BASE + 67)
+#define VIVID_CID_BUF_PREPARE_ERROR     (VIVID_CID_VIVID_BASE + 68)
+#define VIVID_CID_START_STR_ERROR       (VIVID_CID_VIVID_BASE + 69)
+#define VIVID_CID_QUEUE_ERROR           (VIVID_CID_VIVID_BASE + 70)
+#define VIVID_CID_CLEAR_FB              (VIVID_CID_VIVID_BASE + 71)
+#define VIVID_CID_REQ_VALIDATE_ERROR    (VIVID_CID_VIVID_BASE + 72)
+
+#define VIVID_CID_RADIO_SEEK_MODE       (VIVID_CID_VIVID_BASE + 90)
+#define VIVID_CID_RADIO_SEEK_PROG_LIM   (VIVID_CID_VIVID_BASE + 91)
+#define VIVID_CID_RADIO_RX_RDS_RBDS     (VIVID_CID_VIVID_BASE + 92)
+#define VIVID_CID_RADIO_RX_RDS_BLOCKIO  (VIVID_CID_VIVID_BASE + 93)
+
+#define VIVID_CID_RADIO_TX_RDS_BLOCKIO  (VIVID_CID_VIVID_BASE + 94)
+
+#define VIVID_CID_SDR_CAP_FM_DEVIATION  (VIVID_CID_VIVID_BASE + 110)
+
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(VIVID)
+
+class VividCameraData : public CameraData
+{
+public:
+	VividCameraData(PipelineHandler *pipe, MediaDevice *media)
+		: CameraData(pipe), media_(media), video_(nullptr)
+	{
+	}
+
+	~VividCameraData()
+	{
+		delete video_;
+	}
+
+	int init();
+	void bufferReady(FrameBuffer *buffer);
+
+	MediaDevice *media_;
+	V4L2VideoDevice *video_;
+	Stream stream_;
+};
+
+class VividCameraConfiguration : public CameraConfiguration
+{
+public:
+	VividCameraConfiguration();
+
+	Status validate() override;
+};
+
+class PipelineHandlerVivid : public PipelineHandler
+{
+public:
+	PipelineHandlerVivid(CameraManager *manager);
+
+	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) override;
+	void stop(Camera *camera) override;
+
+	int queueRequestDevice(Camera *camera, Request *request) override;
+
+	bool match(DeviceEnumerator *enumerator) override;
+
+private:
+	int processControls(VividCameraData *data, Request *request);
+
+	VividCameraData *cameraData(const Camera *camera)
+	{
+		return static_cast<VividCameraData *>(
+			PipelineHandler::cameraData(camera));
+	}
+};
+
+VividCameraConfiguration::VividCameraConfiguration()
+	: CameraConfiguration()
+{
+}
+
+CameraConfiguration::Status VividCameraConfiguration::validate()
+{
+	Status status = Valid;
+
+	if (config_.empty())
+		return Invalid;
+
+	/* Cap the number of entries to the available streams. */
+	if (config_.size() > 1) {
+		config_.resize(1);
+		status = Adjusted;
+	}
+
+	StreamConfiguration &cfg = config_[0];
+
+	/* Adjust the pixel format. */
+	const std::vector<libcamera::PixelFormat> formats = cfg.formats().pixelformats();
+	if (std::find(formats.begin(), formats.end(), cfg.pixelFormat) == formats.end()) {
+		cfg.pixelFormat = cfg.formats().pixelformats()[0];
+		LOG(VIVID, Debug) << "Adjusting format to " << cfg.pixelFormat.toString();
+		status = Adjusted;
+	}
+
+	cfg.bufferCount = 4;
+
+	return status;
+}
+
+PipelineHandlerVivid::PipelineHandlerVivid(CameraManager *manager)
+	: PipelineHandler(manager)
+{
+}
+
+CameraConfiguration *PipelineHandlerVivid::generateConfiguration(Camera *camera,
+								 const StreamRoles &roles)
+{
+	CameraConfiguration *config = new VividCameraConfiguration();
+	VividCameraData *data = cameraData(camera);
+
+	if (roles.empty())
+		return config;
+
+	std::map<V4L2PixelFormat, std::vector<SizeRange>> v4l2Formats =
+		data->video_->formats();
+	std::map<PixelFormat, std::vector<SizeRange>> deviceFormats;
+	std::transform(v4l2Formats.begin(), v4l2Formats.end(),
+		       std::inserter(deviceFormats, deviceFormats.begin()),
+		       [&](const decltype(v4l2Formats)::value_type &format) {
+			       return decltype(deviceFormats)::value_type{
+				       format.first.toPixelFormat(),
+				       format.second
+			       };
+		       });
+
+	StreamFormats formats(deviceFormats);
+	StreamConfiguration cfg(formats);
+
+	cfg.pixelFormat = PixelFormat(DRM_FORMAT_BGR888);
+	cfg.size = { 1280, 720 };
+	cfg.bufferCount = 4;
+
+	config->addConfiguration(cfg);
+
+	config->validate();
+
+	return config;
+}
+
+int PipelineHandlerVivid::configure(Camera *camera, CameraConfiguration *config)
+{
+	VividCameraData *data = cameraData(camera);
+	StreamConfiguration &cfg = config->at(0);
+	int ret;
+
+	V4L2DeviceFormat format = {};
+	format.fourcc = data->video_->toV4L2PixelFormat(cfg.pixelFormat);
+	format.size = cfg.size;
+
+	ret = data->video_->setFormat(&format);
+	if (ret)
+		return ret;
+
+	if (format.size != cfg.size ||
+	    format.fourcc != data->video_->toV4L2PixelFormat(cfg.pixelFormat))
+		return -EINVAL;
+
+	cfg.setStream(&data->stream_);
+	cfg.stride = format.planes[0].bpl;
+
+	return 0;
+}
+
+int PipelineHandlerVivid::exportFrameBuffers(Camera *camera, Stream *stream,
+					     std::vector<std::unique_ptr<FrameBuffer>> *buffers)
+{
+	VividCameraData *data = cameraData(camera);
+	unsigned int count = stream->configuration().bufferCount;
+
+	return data->video_->exportBuffers(count, buffers);
+}
+
+int PipelineHandlerVivid::start(Camera *camera)
+{
+	VividCameraData *data = cameraData(camera);
+	unsigned int count = data->stream_.configuration().bufferCount;
+
+	int ret = data->video_->importBuffers(count);
+	if (ret < 0)
+		return ret;
+
+	ret = data->video_->streamOn();
+	if (ret < 0) {
+		data->ipa_->stop();
+		data->video_->releaseBuffers();
+		return ret;
+	}
+
+	return 0;
+}
+
+void PipelineHandlerVivid::stop(Camera *camera)
+{
+	VividCameraData *data = cameraData(camera);
+	data->video_->streamOff();
+	data->video_->releaseBuffers();
+}
+
+int PipelineHandlerVivid::processControls(VividCameraData *data, Request *request)
+{
+	ControlList controls(data->video_->controls());
+
+	for (auto it : request->controls()) {
+		unsigned int id = it.first;
+		unsigned int offset;
+		uint32_t cid;
+
+		if (id == controls::Brightness) {
+			cid = V4L2_CID_BRIGHTNESS;
+			offset = 128;
+		} else if (id == controls::Contrast) {
+			cid = V4L2_CID_CONTRAST;
+			offset = 0;
+		} else if (id == controls::Saturation) {
+			cid = V4L2_CID_SATURATION;
+			offset = 0;
+		} else {
+			continue;
+		}
+
+		int32_t value = lroundf(it.second.get<float>() * 128 + offset);
+		controls.set(cid, utils::clamp(value, 0, 255));
+	}
+
+	for (const auto &ctrl : controls)
+		LOG(VIVID, Debug)
+			<< "Setting control " << utils::hex(ctrl.first)
+			<< " to " << ctrl.second.toString();
+
+	int ret = data->video_->setControls(&controls);
+	if (ret) {
+		LOG(VIVID, Error) << "Failed to set controls: " << ret;
+		return ret < 0 ? ret : -EINVAL;
+	}
+
+	return ret;
+}
+
+int PipelineHandlerVivid::queueRequestDevice(Camera *camera, Request *request)
+{
+	VividCameraData *data = cameraData(camera);
+	FrameBuffer *buffer = request->findBuffer(&data->stream_);
+	if (!buffer) {
+		LOG(VIVID, Error)
+			<< "Attempt to queue request with invalid stream";
+
+		return -ENOENT;
+	}
+
+	int ret = processControls(data, request);
+	if (ret < 0)
+		return ret;
+
+	ret = data->video_->queueBuffer(buffer);
+	if (ret < 0)
+		return ret;
+
+	return 0;
+}
+
+bool PipelineHandlerVivid::match(DeviceEnumerator *enumerator)
+{
+	DeviceMatch dm("vivid");
+	dm.add("vivid-000-vid-cap");
+
+	MediaDevice *media = acquireMediaDevice(enumerator, dm);
+	if (!media)
+		return false;
+
+	std::unique_ptr<VividCameraData> data = std::make_unique<VividCameraData>(this, media);
+
+	/* Locate and open the capture video node. */
+	if (data->init())
+		return false;
+
+	/* Create and register the camera. */
+	std::set<Stream *> streams{ &data->stream_ };
+	std::shared_ptr<Camera> camera = Camera::create(this, data->video_->deviceName(), streams);
+	registerCamera(std::move(camera), std::move(data));
+
+	return true;
+}
+
+int VividCameraData::init()
+{
+	video_ = new V4L2VideoDevice(media_->getEntityByName("vivid-000-vid-cap"));
+	if (video_->open())
+		return -ENODEV;
+
+	video_->bufferReady.connect(this, &VividCameraData::bufferReady);
+
+	/* Initialise the supported controls. */
+	const ControlInfoMap &controls = video_->controls();
+	ControlInfoMap::Map ctrls;
+
+	for (const auto &ctrl : controls) {
+		const ControlId *id;
+		ControlInfo info;
+
+		switch (ctrl.first->id()) {
+		case V4L2_CID_BRIGHTNESS:
+			id = &controls::Brightness;
+			info = ControlInfo{ { -1.0f }, { 1.0f }, { 0.0f } };
+			break;
+		case V4L2_CID_CONTRAST:
+			id = &controls::Contrast;
+			info = ControlInfo{ { 0.0f }, { 2.0f }, { 1.0f } };
+			break;
+		case V4L2_CID_SATURATION:
+			id = &controls::Saturation;
+			info = ControlInfo{ { 0.0f }, { 2.0f }, { 1.0f } };
+			break;
+		default:
+			continue;
+		}
+
+		ctrls.emplace(id, info);
+	}
+
+	controlInfo_ = std::move(ctrls);
+
+	return 0;
+}
+
+void VividCameraData::bufferReady(FrameBuffer *buffer)
+{
+	Request *request = buffer->request();
+
+	pipe_->completeBuffer(camera_, request, buffer);
+	pipe_->completeRequest(camera_, request);
+}
+
+REGISTER_PIPELINE_HANDLER(PipelineHandlerVivid);
+
+} /* namespace libcamera */
