diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
index 6e093fc22259..b1e289919d4e 100644
--- a/src/libcamera/pipeline/ipu3/ipu3.cpp
+++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
@@ -61,7 +61,7 @@ public:
 	}
 
 	int init(MediaDevice *media, unsigned int index);
-	int configureInput(const StreamConfiguration &config,
+	int configureInput(const Size &config,
 			   V4L2DeviceFormat *inputFormat);
 	int configureOutput(ImgUOutput *output,
 			    const StreamConfiguration &config);
@@ -111,7 +111,7 @@ public:
 	}
 
 	int init(const MediaDevice *media, unsigned int index);
-	int configure(const StreamConfiguration &config,
+	int configure(const Size &config,
 		      V4L2DeviceFormat *outputFormat);
 
 	BufferPool *exportBuffers();
@@ -133,6 +133,20 @@ public:
 	BufferPool pool_;
 };
 
+class IPU3Stream : public Stream
+{
+public:
+	IPU3Stream()
+		: active_(false), device_(nullptr), cfg_(nullptr)
+	{
+	}
+
+	bool active_;
+	std::string name_;
+	ImgUDevice::ImgUOutput *device_;
+	const StreamConfiguration *cfg_;
+};
+
 class PipelineHandlerIPU3 : public PipelineHandler
 {
 public:
@@ -173,7 +187,8 @@ private:
 		CIO2Device cio2_;
 		ImgUDevice *imgu_;
 
-		Stream stream_;
+		IPU3Stream outStream_;
+		IPU3Stream vfStream_;
 	};
 
 	static constexpr unsigned int IPU3_BUFFER_COUNT = 4;
@@ -212,7 +227,7 @@ PipelineHandlerIPU3::streamConfiguration(Camera *camera,
 {
 	CameraConfiguration configs;
 	IPU3CameraData *data = cameraData(camera);
-	StreamConfiguration *config = &configs[&data->stream_];
+	StreamConfiguration config = {};
 
 	/*
 	 * FIXME: Soraka: the maximum resolution reported by both sensors
@@ -222,15 +237,24 @@ PipelineHandlerIPU3::streamConfiguration(Camera *camera,
 	 *
 	 * \todo Clarify ImgU alignement requirements.
 	 */
-	config->width = 2560;
-	config->height = 1920;
-	config->pixelFormat = V4L2_PIX_FMT_NV12;
-	config->bufferCount = IPU3_BUFFER_COUNT;
+	config.width = 2560;
+	config.height = 1920;
+	config.pixelFormat = V4L2_PIX_FMT_NV12;
+	config.bufferCount = IPU3_BUFFER_COUNT;
+
+	configs[&data->outStream_] = config;
+	LOG(IPU3, Debug)
+		<< "Stream '" << data->outStream_.name_ << "' set to "
+		<< config.width << "x" << config.height << "-0x"
+		<< std::hex << std::setfill('0') << std::setw(8)
+		<< config.pixelFormat;
 
+	configs[&data->vfStream_] = config;
 	LOG(IPU3, Debug)
-		<< "Stream format set to " << config->width << "x"
-		<< config->height << "-0x" << std::hex << std::setfill('0')
-		<< std::setw(8) << config->pixelFormat;
+		<< "Stream '" << data->vfStream_.name_ << "' set to "
+		<< config.width << "x" << config.height << "-0x"
+		<< std::hex << std::setfill('0') << std::setw(8)
+		<< config.pixelFormat;
 
 	return configs;
 }
@@ -239,35 +263,58 @@ int PipelineHandlerIPU3::configureStreams(Camera *camera,
 					  const CameraConfiguration &config)
 {
 	IPU3CameraData *data = cameraData(camera);
-	const StreamConfiguration &cfg = config[&data->stream_];
+	IPU3Stream *outStream = &data->outStream_;
+	IPU3Stream *vfStream = &data->vfStream_;
 	CIO2Device *cio2 = &data->cio2_;
 	ImgUDevice *imgu = data->imgu_;
+	Size sensorSize = {};
 	int ret;
 
-	LOG(IPU3, Info)
-		<< "Requested image format " << cfg.width << "x"
-		<< cfg.height << "-0x" << std::hex << std::setfill('0')
-		<< std::setw(8) << cfg.pixelFormat << " on camera '"
-		<< camera->name() << "'";
+	outStream->active_ = false;
+	vfStream->active_ = false;
+	for (Stream *s : config) {
+		IPU3Stream *stream = static_cast<IPU3Stream *>(s);
+		const StreamConfiguration &cfg = config[stream];
 
-	/*
-	 * Verify that the requested size respects the IPU3 alignement
-	 * requirements (the image width shall be a multiple of 8 pixels and
-	 * its height a multiple of 4 pixels) and the camera maximum sizes.
-	 *
-	 * \todo: consider the BDS scaling factor requirements:
-	 * "the downscaling factor must be an integer value multiple of 1/32"
-	 */
-	if (cfg.width % 8 || cfg.height % 4) {
-		LOG(IPU3, Error) << "Invalid stream size: bad alignment";
-		return -EINVAL;
-	}
+		/*
+		 * Verify that the requested size respects the IPU3 alignment
+		 * requirements (the image width shall be a multiple of 8
+		 * pixels and its height a multiple of 4 pixels) and the camera
+		 * maximum sizes.
+		 *
+		 * \todo: Consider the BDS scaling factor requirements: "the
+		 * downscaling factor must be an integer value multiple of 1/32"
+		 */
+		if (cfg.width % 8 || cfg.height % 4) {
+			LOG(IPU3, Error)
+				<< "Invalid stream size: bad alignment";
+			return -EINVAL;
+		}
 
-	if (cfg.width > cio2->maxSize_.width ||
-	    cfg.height > cio2->maxSize_.height) {
-		LOG(IPU3, Error)
-			<< "Invalid stream size: larger than sensor resolution";
-		return -EINVAL;
+		if (cfg.width > cio2->maxSize_.width ||
+		    cfg.height > cio2->maxSize_.height) {
+			LOG(IPU3, Error)
+				<< "Invalid stream size: larger than sensor resolution";
+			return -EINVAL;
+		}
+
+		LOG(IPU3, Info)
+			<< "Stream '" << stream->name_ << "' "
+			<< cfg.width << "x" << cfg.height << "-0x"
+			<< std::hex << std::setw(8) << cfg.pixelFormat
+			<< " on camera'" << camera->name() << "'";
+
+		/*
+		 * Collect the maximum width and height: IPU3 can downscale
+		 * only.
+		 */
+		if (cfg.width > sensorSize.width)
+			sensorSize.width = cfg.width;
+		if (cfg.height > sensorSize.height)
+			sensorSize.height = cfg.height;
+
+		stream->active_ = true;
+		stream->cfg_ = &cfg;
 	}
 
 	/*
@@ -283,24 +330,51 @@ int PipelineHandlerIPU3::configureStreams(Camera *camera,
 	 * adjusted format to be propagated to the ImgU output devices.
 	 */
 	V4L2DeviceFormat cio2Format = {};
-	ret = cio2->configure(cfg, &cio2Format);
+	ret = cio2->configure(sensorSize, &cio2Format);
 	if (ret)
 		return ret;
 
-	ret = imgu->configureInput(cfg, &cio2Format);
+	ret = imgu->configureInput(sensorSize, &cio2Format);
 	if (ret)
 		return ret;
 
-	/* Apply the format to the ImgU output, viewfinder and stat. */
-	ret = imgu->configureOutput(&imgu->output_, cfg);
-	if (ret)
-		return ret;
+	/* Apply the format to the configured streams output devices. */
+	for (Stream *s : config) {
+		IPU3Stream *stream = static_cast<IPU3Stream *>(s);
 
-	ret = imgu->configureOutput(&imgu->viewfinder_, cfg);
-	if (ret)
-		return ret;
+		ret = imgu->configureOutput(stream->device_, *stream->cfg_);
+		if (ret)
+			return ret;
+	}
+
+	/*
+	 * As we need to set format also on the non-active streams, use
+	 * the configuration of the active one for that purpose (there should
+	 * be at least one active stream in the configuration request).
+	 */
+	if (!outStream->active_) {
+		ret = imgu->configureOutput(outStream->device_,
+					    *vfStream->cfg_);
+		if (ret)
+			return ret;
+	}
 
-	ret = imgu->configureOutput(&imgu->stat_, cfg);
+	if (!vfStream->active_) {
+		ret = imgu->configureOutput(vfStream->device_,
+					    *outStream->cfg_);
+		if (ret)
+			return ret;
+	}
+
+	/*
+	 * Apply the largest available format to the stat node.
+	 * \todo Revise this when we'll actually use the stat node.
+	 */
+	StreamConfiguration statConfig = {};
+	statConfig.width = cio2Format.width;
+	statConfig.height = cio2Format.height;
+
+	ret = imgu->configureOutput(&imgu->stat_, statConfig);
 	if (ret)
 		return ret;
 
@@ -418,7 +492,7 @@ int PipelineHandlerIPU3::queueRequest(Camera *camera, Request *request)
 {
 	IPU3CameraData *data = cameraData(camera);
 	V4L2Device *output = data->imgu_->output_.dev;
-	Stream *stream = &data->stream_;
+	IPU3Stream *stream = &data->outStream_;
 
 	/* Queue a buffer to the ImgU output for capture. */
 	Buffer *buffer = request->findBuffer(stream);
@@ -569,7 +643,10 @@ int PipelineHandlerIPU3::registerCameras()
 	for (unsigned int id = 0; id < 4 && numCameras < 2; ++id) {
 		std::unique_ptr<IPU3CameraData> data =
 			utils::make_unique<IPU3CameraData>(this);
-		std::set<Stream *> streams{ &data->stream_ };
+		std::set<Stream *> streams = {
+			&data->outStream_,
+			&data->vfStream_,
+		};
 		CIO2Device *cio2 = &data->cio2_;
 
 		ret = cio2->init(cio2MediaDev_.get(), id);
@@ -577,11 +654,16 @@ int PipelineHandlerIPU3::registerCameras()
 			continue;
 
 		/**
-		 * \todo Dynamically assign ImgU devices; as of now, limit
-		 * support to two cameras only, and assign imgu0 to the first
-		 * one and imgu1 to the second.
+		 * \todo Dynamically assign ImgU and output devices to each
+		 * stream and camera; as of now, limit support to two cameras
+		 * only, and assign imgu0 to the first one and imgu1 to the
+		 * second.
 		 */
 		data->imgu_ = numCameras ? &imgu1_ : &imgu0_;
+		data->outStream_.device_ = &data->imgu_->output_;
+		data->outStream_.name_ = "output";
+		data->vfStream_.device_ = &data->imgu_->viewfinder_;
+		data->vfStream_.name_ = "viewfinder";
 
 		/*
 		 * Connect video devices' 'bufferReady' signals to their
@@ -598,7 +680,7 @@ int PipelineHandlerIPU3::registerCameras()
 		data->imgu_->output_.dev->bufferReady.connect(data.get(),
 					&IPU3CameraData::imguOutputBufferReady);
 
-		/* Create and register the Camera instance. */
+		/* Initialize and register the Camera and its streams. */
 		std::string cameraName = cio2->sensor_->entityName() + " "
 				       + std::to_string(id);
 		std::shared_ptr<Camera> camera = Camera::create(this,
@@ -732,12 +814,12 @@ int ImgUDevice::init(MediaDevice *media, unsigned int index)
 
 /**
  * \brief Configure the ImgU unit input
- * \param[in] config The requested stream configuration
+ * \param[in] size The ImgU input frame size
  * \param[in] inputFormat The format to be applied to ImgU input
  *
  * \return 0 on success or a negative error code otherwise
  */
-int ImgUDevice::configureInput(const StreamConfiguration &config,
+int ImgUDevice::configureInput(const Size &size,
 			       V4L2DeviceFormat *inputFormat)
 {
 	/* Configure the ImgU input video device with the requested sizes. */
@@ -775,8 +857,8 @@ int ImgUDevice::configureInput(const StreamConfiguration &config,
 			 << rect.toString();
 
 	V4L2SubdeviceFormat imguFormat = {};
-	imguFormat.width = config.width;
-	imguFormat.height = config.height;
+	imguFormat.width = size.width;
+	imguFormat.height = size.height;
 	imguFormat.mbus_code = MEDIA_BUS_FMT_FIXED;
 
 	ret = imgu_->setFormat(PAD_INPUT, &imguFormat);
@@ -1098,15 +1180,15 @@ int CIO2Device::init(const MediaDevice *media, unsigned int index)
 
 /**
  * \brief Configure the CIO2 unit
- * \param[in] config The requested configuration
+ * \param[in] size The requested CIO2 output frame size
  * \param[out] outputFormat The CIO2 unit output image format
  *
  * \return 0 on success or a negative error code otherwise
  */
-int CIO2Device::configure(const StreamConfiguration &config,
+int CIO2Device::configure(const Size &size,
 			  V4L2DeviceFormat *outputFormat)
 {
-	unsigned int imageSize = config.width * config.height;
+	unsigned int imageSize = size.width * size.height;
 	V4L2SubdeviceFormat sensorFormat = {};
 	unsigned int best = ~0;
 	int ret;
@@ -1116,7 +1198,7 @@ int CIO2Device::configure(const StreamConfiguration &config,
 		if (mediaBusToFormat(it.first) < 0)
 			continue;
 
-		for (const SizeRange &size : it.second) {
+		for (const SizeRange &range : it.second) {
 			/*
 			 * Only select formats bigger than the requested sizes
 			 * as the IPU3 cannot up-scale.
@@ -1125,19 +1207,19 @@ int CIO2Device::configure(const StreamConfiguration &config,
 			 * as possible. This will need to be revisited when
 			 * implementing the scaling policy.
 			 */
-			if (size.maxWidth < config.width ||
-			    size.maxHeight < config.height)
+			if (range.maxWidth < size.width ||
+			    range.maxHeight < size.height)
 				continue;
 
-			unsigned int diff = size.maxWidth * size.maxHeight
+			unsigned int diff = range.maxWidth * range.maxHeight
 					  - imageSize;
 			if (diff >= best)
 				continue;
 
 			best = diff;
 
-			sensorFormat.width = size.maxWidth;
-			sensorFormat.height = size.maxHeight;
+			sensorFormat.width = range.maxWidth;
+			sensorFormat.height = range.maxHeight;
 			sensorFormat.mbus_code = it.first;
 		}
 	}
