diff --git a/src/cam/camera_session.cpp b/src/cam/camera_session.cpp
index efffafbf..7843c3fd 100644
--- a/src/cam/camera_session.cpp
+++ b/src/cam/camera_session.cpp
@@ -5,10 +5,9 @@
  * camera_session.cpp - Camera capture session
  */
 
-#include <iomanip>
-#include <iostream>
 #include <limits.h>
-#include <sstream>
+#include <fmt/format.h>
+#include <fmt/ostream.h>
 
 #include <libcamera/control_ids.h>
 #include <libcamera/property_ids.h>
@@ -22,6 +21,9 @@
 #include "main.h"
 #include "stream_options.h"
 
+#define PR(...) fmt::print(__VA_ARGS__)
+#define EPR(...) fmt::print(stderr, __VA_ARGS__)
+
 using namespace libcamera;
 
 CameraSession::CameraSession(CameraManager *cm,
@@ -40,13 +42,12 @@ CameraSession::CameraSession(CameraManager *cm,
 		camera_ = cm->get(cameraId);
 
 	if (!camera_) {
-		std::cerr << "Camera " << cameraId << " not found" << std::endl;
+		EPR("Camera {} not found\n", cameraId);
 		return;
 	}
 
 	if (camera_->acquire()) {
-		std::cerr << "Failed to acquire camera " << cameraId
-			  << std::endl;
+		EPR("Failed to acquire camera {}", cameraId);
 		return;
 	}
 
@@ -55,15 +56,14 @@ CameraSession::CameraSession(CameraManager *cm,
 	std::unique_ptr<CameraConfiguration> config =
 		camera_->generateConfiguration(roles);
 	if (!config || config->size() != roles.size()) {
-		std::cerr << "Failed to get default stream configuration"
-			  << std::endl;
+		EPR("Failed to get default stream configuration\n");
 		return;
 	}
 
 	/* Apply configuration if explicitly requested. */
 	if (StreamKeyValueParser::updateConfiguration(config.get(),
 						      options_[OptStream])) {
-		std::cerr << "Failed to update configuration" << std::endl;
+		EPR("Failed to update configuration\n");
 		return;
 	}
 
@@ -72,20 +72,17 @@ CameraSession::CameraSession(CameraManager *cm,
 #ifdef HAVE_KMS
 	if (options_.isSet(OptDisplay)) {
 		if (options_.isSet(OptFile)) {
-			std::cerr << "--display and --file options are mutually exclusive"
-				  << std::endl;
+			EPR("--display and --file options are mutually exclusive\n");
 			return;
 		}
 
 		if (roles.size() != 1) {
-			std::cerr << "Display doesn't support multiple streams"
-				  << std::endl;
+			EPR("Display doesn't support multiple streams\n");
 			return;
 		}
 
 		if (roles[0] != StreamRole::Viewfinder) {
-			std::cerr << "Display requires a viewfinder stream"
-				  << std::endl;
+			EPR("Display requires a viewfinder stream\n");
 			return;
 		}
 	}
@@ -97,15 +94,14 @@ CameraSession::CameraSession(CameraManager *cm,
 
 	case CameraConfiguration::Adjusted:
 		if (strictFormats) {
-			std::cout << "Adjusting camera configuration disallowed by --strict-formats argument"
-				  << std::endl;
+			PR("Adjusting camera configuration disallowed by --strict-formats argument\n");
 			return;
 		}
-		std::cout << "Camera configuration adjusted" << std::endl;
+		PR("Camera configuration adjusted\n");
 		break;
 
 	case CameraConfiguration::Invalid:
-		std::cout << "Camera configuration invalid" << std::endl;
+		PR("Camera configuration invalid\n");
 		return;
 	}
 
@@ -121,8 +117,7 @@ CameraSession::~CameraSession()
 void CameraSession::listControls() const
 {
 	for (const auto &[id, info] : camera_->controls()) {
-		std::cout << "Control: " << id->name() << ": "
-			  << info.toString() << std::endl;
+		PR("Control: {}: {}}n", id->name(), info.toString());
 	}
 }
 
@@ -131,8 +126,7 @@ void CameraSession::listProperties() const
 	for (const auto &[key, value] : camera_->properties()) {
 		const ControlId *id = properties::properties.at(key);
 
-		std::cout << "Property: " << id->name() << " = "
-			  << value.toString() << std::endl;
+		PR("Property: {} = {}\n", id->name(), value.toString());
 	}
 }
 
@@ -140,17 +134,15 @@ void CameraSession::infoConfiguration() const
 {
 	unsigned int index = 0;
 	for (const StreamConfiguration &cfg : *config_) {
-		std::cout << index << ": " << cfg.toString() << std::endl;
+		PR("{}: {}\n", index, cfg.toString());
 
 		const StreamFormats &formats = cfg.formats();
 		for (PixelFormat pixelformat : formats.pixelformats()) {
-			std::cout << " * Pixelformat: "
-				  << pixelformat << " "
-				  << formats.range(pixelformat).toString()
-				  << std::endl;
+			PR(" * Pixelformat: {} {}\n", pixelformat,
+			   formats.range(pixelformat).toString());
 
 			for (const Size &size : formats.sizes(pixelformat))
-				std::cout << "  - " << size << std::endl;
+				PR("  - {}\n", size);
 		}
 
 		index++;
@@ -168,7 +160,7 @@ int CameraSession::start()
 
 	ret = camera_->configure(config_.get());
 	if (ret < 0) {
-		std::cout << "Failed to configure camera" << std::endl;
+		PR("Failed to configure camera\n");
 		return ret;
 	}
 
@@ -197,8 +189,7 @@ int CameraSession::start()
 	if (sink_) {
 		ret = sink_->configure(*config_);
 		if (ret < 0) {
-			std::cout << "Failed to configure frame sink"
-				  << std::endl;
+			PR("Failed to configure frame sink\n");
 			return ret;
 		}
 
@@ -214,12 +205,12 @@ void CameraSession::stop()
 {
 	int ret = camera_->stop();
 	if (ret)
-		std::cout << "Failed to stop capture" << std::endl;
+		PR("Failed to stop capture\n");
 
 	if (sink_) {
 		ret = sink_->stop();
 		if (ret)
-			std::cout << "Failed to stop frame sink" << std::endl;
+			PR("Failed to stop frame sink\n");
 	}
 
 	sink_.reset();
@@ -238,7 +229,7 @@ int CameraSession::startCapture()
 	for (StreamConfiguration &cfg : *config_) {
 		ret = allocator_->allocate(cfg.stream());
 		if (ret < 0) {
-			std::cerr << "Can't allocate buffers" << std::endl;
+			EPR("Can't allocate buffers\n");
 			return -ENOMEM;
 		}
 
@@ -254,7 +245,7 @@ int CameraSession::startCapture()
 	for (unsigned int i = 0; i < nbuffers; i++) {
 		std::unique_ptr<Request> request = camera_->createRequest();
 		if (!request) {
-			std::cerr << "Can't create request" << std::endl;
+			EPR("Can't create request\n");
 			return -ENOMEM;
 		}
 
@@ -266,8 +257,7 @@ int CameraSession::startCapture()
 
 			ret = request->addBuffer(stream, buffer.get());
 			if (ret < 0) {
-				std::cerr << "Can't set buffer for request"
-					  << std::endl;
+				EPR("Can't set buffer for request\n");
 				return ret;
 			}
 
@@ -281,14 +271,14 @@ int CameraSession::startCapture()
 	if (sink_) {
 		ret = sink_->start();
 		if (ret) {
-			std::cout << "Failed to start frame sink" << std::endl;
+			PR("Failed to start frame sink\n");
 			return ret;
 		}
 	}
 
 	ret = camera_->start();
 	if (ret) {
-		std::cout << "Failed to start capture" << std::endl;
+		PR("Failed to start capture\n");
 		if (sink_)
 			sink_->stop();
 		return ret;
@@ -297,7 +287,7 @@ int CameraSession::startCapture()
 	for (std::unique_ptr<Request> &request : requests_) {
 		ret = queueRequest(request.get());
 		if (ret < 0) {
-			std::cerr << "Can't queue request" << std::endl;
+			EPR("Can't queue request\n");
 			camera_->stop();
 			if (sink_)
 				sink_->stop();
@@ -306,13 +296,11 @@ int CameraSession::startCapture()
 	}
 
 	if (captureLimit_)
-		std::cout << "cam" << cameraIndex_
-			  << ": Capture " << captureLimit_ << " frames"
-			  << std::endl;
+		PR("cam{}: Capture {} frames\n", cameraIndex_,
+		   captureLimit_);
 	else
-		std::cout << "cam" << cameraIndex_
-			  << ": Capture until user interrupts by SIGINT"
-			  << std::endl;
+		PR("cam{}: Capture until user interrupts by SIGINT\n",
+		   cameraIndex_);
 
 	return 0;
 }
@@ -364,23 +352,23 @@ void CameraSession::processRequest(Request *request)
 
 	bool requeue = true;
 
-	std::stringstream info;
-	info << ts / 1000000000 << "."
-	     << std::setw(6) << std::setfill('0') << ts / 1000 % 1000000
-	     << " (" << std::fixed << std::setprecision(2) << fps << " fps)";
+	auto sbuf = fmt::memory_buffer();
+	fmt::format_to(std::back_inserter(sbuf), "{}.{:06} ({:.2f} fps)",
+		       ts / 1000000000,
+		       ts / 1000 % 1000000,
+		       fps);
 
 	for (const auto &[stream, buffer] : buffers) {
 		const FrameMetadata &metadata = buffer->metadata();
 
-		info << " " << streamNames_[stream]
-		     << " seq: " << std::setw(6) << std::setfill('0') << metadata.sequence
-		     << " bytesused: ";
+		fmt::format_to(std::back_inserter(sbuf), " {} seq: {:06} bytesused: ",
+			       streamNames_[stream], metadata.sequence);
 
 		unsigned int nplane = 0;
 		for (const FrameMetadata::Plane &plane : metadata.planes()) {
-			info << plane.bytesused;
+			fmt::format_to(std::back_inserter(sbuf), "{}", plane.bytesused);
 			if (++nplane < metadata.planes().size())
-				info << "/";
+				fmt::format_to(std::back_inserter(sbuf), "/");
 		}
 	}
 
@@ -389,14 +377,13 @@ void CameraSession::processRequest(Request *request)
 			requeue = false;
 	}
 
-	std::cout << info.str() << std::endl;
+	PR("{}\n", fmt::to_string(sbuf));
 
 	if (printMetadata_) {
 		const ControlList &requestMetadata = request->metadata();
 		for (const auto &[key, value] : requestMetadata) {
 			const ControlId *id = controls::controls.at(key);
-			std::cout << "\t" << id->name() << " = "
-				  << value.toString() << std::endl;
+			PR("\t{} = {}\n", id->name(), value.toString());
 		}
 	}
 
diff --git a/src/cam/drm.cpp b/src/cam/drm.cpp
index 46e34eb5..84919ab3 100644
--- a/src/cam/drm.cpp
+++ b/src/cam/drm.cpp
@@ -10,12 +10,12 @@
 #include <algorithm>
 #include <errno.h>
 #include <fcntl.h>
-#include <iostream>
 #include <set>
 #include <string.h>
 #include <sys/ioctl.h>
 #include <sys/stat.h>
 #include <sys/types.h>
+#include <fmt/core.h>
 
 #include <libcamera/framebuffer.h>
 #include <libcamera/geometry.h>
@@ -25,6 +25,9 @@
 
 #include "event_loop.h"
 
+#define PR(...) fmt::print(__VA_ARGS__)
+#define EPR(...) fmt::print(stderr, __VA_ARGS__)
+
 namespace DRM {
 
 Object::Object(Device *dev, uint32_t id, Type type)
@@ -178,9 +181,7 @@ Connector::Connector(Device *dev, const drmModeConnector *connector)
 {
 	auto typeName = connectorTypeNames.find(connector->connector_type);
 	if (typeName == connectorTypeNames.end()) {
-		std::cerr
-			<< "Invalid connector type "
-			<< connector->connector_type << std::endl;
+		EPR("Invalid connector type {}}n", connector->connector_type);
 		typeName = connectorTypeNames.find(DRM_MODE_CONNECTOR_Unknown);
 	}
 
@@ -213,9 +214,7 @@ Connector::Connector(Device *dev, const drmModeConnector *connector)
 						    return e.id() == encoderId;
 					    });
 		if (encoder == encoders.end()) {
-			std::cerr
-				<< "Encoder " << encoderId << " not found"
-				<< std::endl;
+			EPR("Encoder {} not found\n", encoderId);
 			continue;
 		}
 
@@ -296,9 +295,7 @@ FrameBuffer::~FrameBuffer()
 
 		if (ret == -1) {
 			ret = -errno;
-			std::cerr
-				<< "Failed to close GEM object: "
-				<< strerror(-ret) << std::endl;
+			EPR("Failed to close GEM object: {}\n", strerror(-ret));
 		}
 	}
 
@@ -408,9 +405,8 @@ int Device::init()
 	fd_ = open(name, O_RDWR | O_CLOEXEC);
 	if (fd_ < 0) {
 		ret = -errno;
-		std::cerr
-			<< "Failed to open DRM/KMS device " << name << ": "
-			<< strerror(-ret) << std::endl;
+		EPR("Failed to open DRM/KMS device {}: {}\n", name,
+		    strerror(-ret));
 		return ret;
 	}
 
@@ -421,9 +417,7 @@ int Device::init()
 	ret = drmSetClientCap(fd_, DRM_CLIENT_CAP_ATOMIC, 1);
 	if (ret < 0) {
 		ret = -errno;
-		std::cerr
-			<< "Failed to enable atomic capability: "
-			<< strerror(-ret) << std::endl;
+		EPR("Failed to enable atomic capability: {}\n", strerror(-ret));
 		return ret;
 	}
 
@@ -448,9 +442,7 @@ int Device::getResources()
 	};
 	if (!resources) {
 		ret = -errno;
-		std::cerr
-			<< "Failed to get DRM/KMS resources: "
-			<< strerror(-ret) << std::endl;
+		EPR("Failed to get DRM/KMS resources: {}\n", strerror(-ret));
 		return ret;
 	}
 
@@ -458,9 +450,7 @@ int Device::getResources()
 		drmModeCrtc *crtc = drmModeGetCrtc(fd_, resources->crtcs[i]);
 		if (!crtc) {
 			ret = -errno;
-			std::cerr
-				<< "Failed to get CRTC: " << strerror(-ret)
-				<< std::endl;
+			EPR("Failed to get CRTC: {}\n", strerror(-ret));
 			return ret;
 		}
 
@@ -476,9 +466,7 @@ int Device::getResources()
 			drmModeGetEncoder(fd_, resources->encoders[i]);
 		if (!encoder) {
 			ret = -errno;
-			std::cerr
-				<< "Failed to get encoder: " << strerror(-ret)
-				<< std::endl;
+			EPR("Failed to get encoder: {}\n", strerror(-ret));
 			return ret;
 		}
 
@@ -494,9 +482,7 @@ int Device::getResources()
 			drmModeGetConnector(fd_, resources->connectors[i]);
 		if (!connector) {
 			ret = -errno;
-			std::cerr
-				<< "Failed to get connector: " << strerror(-ret)
-				<< std::endl;
+			EPR("Failed to get connector: {}\n", strerror(-ret));
 			return ret;
 		}
 
@@ -513,9 +499,7 @@ int Device::getResources()
 	};
 	if (!planes) {
 		ret = -errno;
-		std::cerr
-			<< "Failed to get DRM/KMS planes: "
-			<< strerror(-ret) << std::endl;
+		EPR("Failed to get DRM/KMS planes: {}\n", strerror(-ret));
 		return ret;
 	}
 
@@ -524,9 +508,7 @@ int Device::getResources()
 			drmModeGetPlane(fd_, planes->planes[i]);
 		if (!plane) {
 			ret = -errno;
-			std::cerr
-				<< "Failed to get plane: " << strerror(-ret)
-				<< std::endl;
+			EPR("Failed to get plane: {}\n", strerror(-ret));
 			return ret;
 		}
 
@@ -556,9 +538,7 @@ int Device::getResources()
 		drmModePropertyRes *property = drmModeGetProperty(fd_, id);
 		if (!property) {
 			ret = -errno;
-			std::cerr
-				<< "Failed to get property: " << strerror(-ret)
-				<< std::endl;
+			EPR("Failed to get property: {}\n", strerror(-ret));
 			continue;
 		}
 
@@ -573,9 +553,8 @@ int Device::getResources()
 	for (auto &object : objects_) {
 		ret = object.second->setup();
 		if (ret < 0) {
-			std::cerr
-				<< "Failed to setup object " << object.second->id()
-				<< ": " << strerror(-ret) << std::endl;
+			EPR("Failed to setup object {}: {}\n",
+			    object.second->id(), strerror(-ret));
 			return ret;
 		}
 	}
@@ -616,9 +595,8 @@ std::unique_ptr<FrameBuffer> Device::createFrameBuffer(
 			ret = drmPrimeFDToHandle(fd_, plane.fd.get(), &handle);
 			if (ret < 0) {
 				ret = -errno;
-				std::cerr
-					<< "Unable to import framebuffer dmabuf: "
-					<< strerror(-ret) << std::endl;
+				EPR("Unable to import framebuffer dmabuf: {}\n",
+				    strerror(-ret));
 				return nullptr;
 			}
 
@@ -636,9 +614,7 @@ std::unique_ptr<FrameBuffer> Device::createFrameBuffer(
 			    strides.data(), offsets, &fb->id_, 0);
 	if (ret < 0) {
 		ret = -errno;
-		std::cerr
-			<< "Failed to add framebuffer: "
-			<< strerror(-ret) << std::endl;
+		EPR("Failed to add framebuffer: {}\n", strerror(-ret));
 		return nullptr;
 	}
 
diff --git a/src/cam/event_loop.cpp b/src/cam/event_loop.cpp
index e25784c0..87aaf59a 100644
--- a/src/cam/event_loop.cpp
+++ b/src/cam/event_loop.cpp
@@ -10,7 +10,10 @@
 #include <assert.h>
 #include <event2/event.h>
 #include <event2/thread.h>
-#include <iostream>
+#include <fmt/core.h>
+
+#define PR(...) fmt::print(__VA_ARGS__)
+#define EPR(...) fmt::print(stderr, __VA_ARGS__)
 
 EventLoop *EventLoop::instance_ = nullptr;
 
@@ -71,13 +74,13 @@ void EventLoop::addEvent(int fd, EventType type,
 	event->event_ = event_new(base_, fd, events, &EventLoop::Event::dispatch,
 				  event.get());
 	if (!event->event_) {
-		std::cerr << "Failed to create event for fd " << fd << std::endl;
+		EPR("Failed to create event for fd {}\n", fd);
 		return;
 	}
 
 	int ret = event_add(event->event_, nullptr);
 	if (ret < 0) {
-		std::cerr << "Failed to add event for fd " << fd << std::endl;
+		EPR("Failed to add event for fd {}\n", fd);
 		return;
 	}
 
diff --git a/src/cam/file_sink.cpp b/src/cam/file_sink.cpp
index 45213d4a..86e2118c 100644
--- a/src/cam/file_sink.cpp
+++ b/src/cam/file_sink.cpp
@@ -7,11 +7,12 @@
 
 #include <assert.h>
 #include <fcntl.h>
-#include <iomanip>
-#include <iostream>
-#include <sstream>
 #include <string.h>
 #include <unistd.h>
+#include <fmt/core.h>
+
+#define PR(...) fmt::print(__VA_ARGS__)
+#define EPR(...) fmt::print(stderr, __VA_ARGS__)
 
 #include <libcamera/camera.h>
 
@@ -70,10 +71,10 @@ void FileSink::writeBuffer(const Stream *stream, FrameBuffer *buffer)
 
 	pos = filename.find_first_of('#');
 	if (pos != std::string::npos) {
-		std::stringstream ss;
-		ss << streamNames_[stream] << "-" << std::setw(6)
-		   << std::setfill('0') << buffer->metadata().sequence;
-		filename.replace(pos, 1, ss.str());
+		auto s = fmt::format("{}-{:06}",
+				     streamNames_[stream],
+				     buffer->metadata().sequence);
+		filename.replace(pos, 1, s);
 	}
 
 	fd = open(filename.c_str(), O_CREAT | O_WRONLY |
@@ -81,8 +82,7 @@ void FileSink::writeBuffer(const Stream *stream, FrameBuffer *buffer)
 		  S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
 	if (fd == -1) {
 		ret = -errno;
-		std::cerr << "failed to open file " << filename << ": "
-			  << strerror(-ret) << std::endl;
+		EPR("failed to open file {}: {}\n", filename, strerror(-ret));
 		return;
 	}
 
@@ -95,20 +95,17 @@ void FileSink::writeBuffer(const Stream *stream, FrameBuffer *buffer)
 		unsigned int length = std::min<unsigned int>(meta.bytesused, data.size());
 
 		if (meta.bytesused > data.size())
-			std::cerr << "payload size " << meta.bytesused
-				  << " larger than plane size " << data.size()
-				  << std::endl;
+			EPR("payload size {} larger than plane size {}\n",
+			    meta.bytesused, data.size());
 
 		ret = ::write(fd, data.data(), length);
 		if (ret < 0) {
 			ret = -errno;
-			std::cerr << "write error: " << strerror(-ret)
-				  << std::endl;
+			EPR("write error: {}\n", strerror(-ret));
 			break;
 		} else if (ret != (int)length) {
-			std::cerr << "write error: only " << ret
-				  << " bytes written instead of "
-				  << length << std::endl;
+			EPR("write error: only {} bytes written instead of {}\n",
+			    ret, length);
 			break;
 		}
 	}
diff --git a/src/cam/image.cpp b/src/cam/image.cpp
index fe2cc6da..73bcf915 100644
--- a/src/cam/image.cpp
+++ b/src/cam/image.cpp
@@ -9,11 +9,14 @@
 
 #include <assert.h>
 #include <errno.h>
-#include <iostream>
 #include <map>
 #include <string.h>
 #include <sys/mman.h>
 #include <unistd.h>
+#include <fmt/core.h>
+
+#define PR(...) fmt::print(__VA_ARGS__)
+#define EPR(...) fmt::print(stderr, __VA_ARGS__)
 
 using namespace libcamera;
 
@@ -49,10 +52,8 @@ std::unique_ptr<Image> Image::fromFrameBuffer(const FrameBuffer *buffer, MapMode
 
 		if (plane.offset > length ||
 		    plane.offset + plane.length > length) {
-			std::cerr << "plane is out of buffer: buffer length="
-				  << length << ", plane offset=" << plane.offset
-				  << ", plane length=" << plane.length
-				  << std::endl;
+			EPR("plane is out of buffer: buffer length={}, plane offset={}, plane length={}\n",
+			    length, plane.offset, plane.length);
 			return nullptr;
 		}
 		size_t &mapLength = mappedBuffers[fd].mapLength;
@@ -68,8 +69,8 @@ std::unique_ptr<Image> Image::fromFrameBuffer(const FrameBuffer *buffer, MapMode
 					     MAP_SHARED, fd, 0);
 			if (address == MAP_FAILED) {
 				int error = -errno;
-				std::cerr << "Failed to mmap plane: "
-					  << strerror(-error) << std::endl;
+				EPR("Failed to mmap plane: {}\n",
+				    strerror(-error));
 				return nullptr;
 			}
 
diff --git a/src/cam/kms_sink.cpp b/src/cam/kms_sink.cpp
index 7add81a6..823b75e4 100644
--- a/src/cam/kms_sink.cpp
+++ b/src/cam/kms_sink.cpp
@@ -10,10 +10,13 @@
 #include <array>
 #include <algorithm>
 #include <assert.h>
-#include <iostream>
 #include <memory>
 #include <stdint.h>
 #include <string.h>
+#include <fmt/core.h>
+
+#define PR(...) fmt::print(__VA_ARGS__)
+#define EPR(...) fmt::print(stderr, __VA_ARGS__)
 
 #include <libcamera/camera.h>
 #include <libcamera/formats.h>
@@ -54,11 +57,9 @@ KMSSink::KMSSink(const std::string &connectorName)
 
 	if (!connector_) {
 		if (!connectorName.empty())
-			std::cerr
-				<< "Connector " << connectorName << " not found"
-				<< std::endl;
+			EPR("Connector {} not found\n", connectorName);
 		else
-			std::cerr << "No connected connector found" << std::endl;
+			EPR("No connected connector found\n");
 		return;
 	}
 
@@ -119,7 +120,7 @@ int KMSSink::configure(const libcamera::CameraConfiguration &config)
 						      mode.vdisplay == cfg.size.height;
 				       });
 	if (iter == modes.end()) {
-		std::cerr << "No mode matching " << cfg.size << std::endl;
+		EPR("No mode matching {}\n", cfg.size);
 		return -EINVAL;
 	}
 
@@ -192,17 +193,12 @@ int KMSSink::configurePipeline(const libcamera::PixelFormat &format)
 {
 	const int ret = selectPipeline(format);
 	if (ret) {
-		std::cerr
-			<< "Unable to find display pipeline for format "
-			<< format << std::endl;
-
+		EPR("Unable to find display pipeline for format {}\n", format);
 		return ret;
 	}
 
-	std::cout
-		<< "Using KMS plane " << plane_->id() << ", CRTC " << crtc_->id()
-		<< ", connector " << connector_->name()
-		<< " (" << connector_->id() << ")" << std::endl;
+	PR("Using KMS plane {}, CRTC {}, connector {} ({})\n",
+	   plane_->id(), crtc_->id(), connector_->name(), connector_->id());
 
 	return 0;
 }
@@ -228,9 +224,8 @@ int KMSSink::start()
 
 	ret = request->commit(DRM::AtomicRequest::FlagAllowModeset);
 	if (ret < 0) {
-		std::cerr
-			<< "Failed to disable CRTCs and planes: "
-			<< strerror(-ret) << std::endl;
+		EPR("Failed to disable CRTCs and planes: {}\n",
+		    strerror(-ret));
 		return ret;
 	}
 
@@ -250,9 +245,7 @@ int KMSSink::stop()
 
 	int ret = request.commit(DRM::AtomicRequest::FlagAllowModeset);
 	if (ret < 0) {
-		std::cerr
-			<< "Failed to stop display pipeline: "
-			<< strerror(-ret) << std::endl;
+		EPR("Failed to stop display pipeline: {}\n", strerror(-ret));
 		return ret;
 	}
 
@@ -312,9 +305,8 @@ bool KMSSink::processRequest(libcamera::Request *camRequest)
 	if (!queued_) {
 		int ret = drmRequest->commit(flags);
 		if (ret < 0) {
-			std::cerr
-				<< "Failed to commit atomic request: "
-				<< strerror(-ret) << std::endl;
+			EPR("Failed to commit atomic request: {}\n",
+			    strerror(-ret));
 			/* \todo Implement error handling */
 		}
 
diff --git a/src/cam/main.cpp b/src/cam/main.cpp
index c7f664b9..03615dc9 100644
--- a/src/cam/main.cpp
+++ b/src/cam/main.cpp
@@ -6,10 +6,9 @@
  */
 
 #include <atomic>
-#include <iomanip>
-#include <iostream>
 #include <signal.h>
 #include <string.h>
+#include <fmt/core.h>
 
 #include <libcamera/libcamera.h>
 #include <libcamera/property_ids.h>
@@ -78,8 +77,7 @@ int CamApp::init(int argc, char **argv)
 
 	ret = cm_->start();
 	if (ret) {
-		std::cout << "Failed to start camera manager: "
-			  << strerror(-ret) << std::endl;
+		fmt::print("Failed to start camera manager: {}\n", -ret);
 		return ret;
 	}
 
@@ -173,12 +171,12 @@ int CamApp::parseOptions(int argc, char *argv[])
 
 void CamApp::cameraAdded(std::shared_ptr<Camera> cam)
 {
-	std::cout << "Camera Added: " << cam->id() << std::endl;
+	fmt::print("Camera Added: {}\n", cam->id());
 }
 
 void CamApp::cameraRemoved(std::shared_ptr<Camera> cam)
 {
-	std::cout << "Camera Removed: " << cam->id() << std::endl;
+	fmt::print("Camera Removed: {}\n", cam->id());
 }
 
 void CamApp::captureDone()
@@ -193,11 +191,11 @@ int CamApp::run()
 
 	/* 1. List all cameras. */
 	if (options_.isSet(OptList)) {
-		std::cout << "Available cameras:" << std::endl;
+		fmt::print("Available cameras:\n");
 
 		unsigned int index = 1;
 		for (const std::shared_ptr<Camera> &cam : cm_->cameras()) {
-			std::cout << index << ": " << cameraName(cam.get()) << std::endl;
+			fmt::print("{}: {}\n", cameraName(cam.get()), index);
 			index++;
 		}
 	}
@@ -215,12 +213,12 @@ int CamApp::run()
 								index,
 								camera.children());
 			if (!session->isValid()) {
-				std::cout << "Failed to create camera session" << std::endl;
+				fmt::print("Failed to create camera session\n");
 				return -EINVAL;
 			}
 
-			std::cout << "Using camera " << session->camera()->id()
-				  << " as cam" << index << std::endl;
+			fmt::print("Using camera{} as cam{}\n",
+				   session->camera()->id(), index);
 
 			session->captureDone.connect(this, &CamApp::captureDone);
 
@@ -250,7 +248,7 @@ int CamApp::run()
 
 		ret = session->start();
 		if (ret) {
-			std::cout << "Failed to start camera session" << std::endl;
+			fmt::print("Failed to start camera session\n");
 			return ret;
 		}
 
@@ -259,8 +257,8 @@ int CamApp::run()
 
 	/* 5. Enable hotplug monitoring. */
 	if (options_.isSet(OptMonitor)) {
-		std::cout << "Monitoring new hotplug and unplug events" << std::endl;
-		std::cout << "Press Ctrl-C to interrupt" << std::endl;
+		fmt::print("Monitoring new hotplug and unplug events\n");
+		fmt::print("Press Ctrl-C to interrupt\n");
 
 		cm_->cameraAdded.connect(this, &CamApp::cameraAdded);
 		cm_->cameraRemoved.connect(this, &CamApp::cameraRemoved);
@@ -323,7 +321,7 @@ std::string CamApp::cameraName(const Camera *camera)
 
 void signalHandler([[maybe_unused]] int signal)
 {
-	std::cout << "Exiting" << std::endl;
+	fmt::print("Exiting");
 	CamApp::instance()->quit();
 }
 
diff --git a/src/cam/meson.build b/src/cam/meson.build
index 5bab8c9e..2b47383d 100644
--- a/src/cam/meson.build
+++ b/src/cam/meson.build
@@ -7,6 +7,8 @@ if not libevent.found()
     subdir_done()
 endif
 
+libfmt_dep = dependency('fmt')
+
 cam_enabled = true
 
 cam_sources = files([
@@ -25,7 +27,7 @@ cam_cpp_args = []
 libdrm = dependency('libdrm', required : false)
 
 if libdrm.found()
-    cam_cpp_args += [ '-DHAVE_KMS' ]
+    cam_cpp_args += [ '-DHAVE_KMS', ]
     cam_sources += files([
         'drm.cpp',
         'kms_sink.cpp'
@@ -38,6 +40,7 @@ cam  = executable('cam', cam_sources,
                       libcamera_public,
                       libdrm,
                       libevent,
+                      libfmt_dep,
                   ],
                   cpp_args : cam_cpp_args,
                   install : true)
diff --git a/src/cam/options.cpp b/src/cam/options.cpp
index 4f7e8691..c9979385 100644
--- a/src/cam/options.cpp
+++ b/src/cam/options.cpp
@@ -7,9 +7,11 @@
 
 #include <assert.h>
 #include <getopt.h>
-#include <iomanip>
-#include <iostream>
 #include <string.h>
+#include <fmt/core.h>
+
+#define PR(...) fmt::print(__VA_ARGS__)
+#define EPR(...) fmt::print(stderr, __VA_ARGS__)
 
 #include "options.h"
 
@@ -390,26 +392,23 @@ KeyValueParser::Options KeyValueParser::parse(const char *arguments)
 			continue;
 
 		if (optionsMap_.find(key) == optionsMap_.end()) {
-			std::cerr << "Invalid option " << key << std::endl;
+			EPR("Invalid option {}\n", key);
 			return options;
 		}
 
 		OptionArgument arg = optionsMap_[key].argument;
 		if (value.empty() && arg == ArgumentRequired) {
-			std::cerr << "Option " << key << " requires an argument"
-				  << std::endl;
+			EPR("Option {} requires an argument\n", key);
 			return options;
 		} else if (!value.empty() && arg == ArgumentNone) {
-			std::cerr << "Option " << key << " takes no argument"
-				  << std::endl;
+			EPR("Option {} takes no argument\n", key);
 			return options;
 		}
 
 		const Option &option = optionsMap_[key];
 		if (!options.parseValue(key, option, value.c_str())) {
-			std::cerr << "Failed to parse '" << value << "' as "
-				  << option.typeName() << " for option " << key
-				  << std::endl;
+			EPR("Failed to parse '{}' as {} for option {}\n",
+			    value, option.typeName(), key);
 			return options;
 		}
 	}
@@ -453,16 +452,16 @@ void KeyValueParser::usage(int indent)
 				argument += "]";
 		}
 
-		std::cerr << std::setw(indent) << argument;
+		EPR("{:{}}", argument, indent);
 
 		for (const char *help = option.help, *end = help; end;) {
 			end = strchr(help, '\n');
 			if (end) {
-				std::cerr << std::string(help, end - help + 1);
-				std::cerr << std::setw(indent) << " ";
+				EPR(std::string(help, end - help + 1));
+				EPR("{:{}}", "", indent);
 				help = end + 1;
 			} else {
-				std::cerr << help << std::endl;
+				EPR("{}\n", help);
 			}
 		}
 	}
@@ -929,10 +928,10 @@ OptionsParser::Options OptionsParser::parse(int argc, char **argv)
 
 		if (c == '?' || c == ':') {
 			if (c == '?')
-				std::cerr << "Invalid option ";
+				EPR("Invalid option ");
 			else
-				std::cerr << "Missing argument for option ";
-			std::cerr << argv[optind - 1] << std::endl;
+				EPR("Missing argument for option ");
+			EPR("{}\n", argv[optind - 1]);
 
 			usage();
 			return options;
@@ -946,8 +945,7 @@ OptionsParser::Options OptionsParser::parse(int argc, char **argv)
 	}
 
 	if (optind < argc) {
-		std::cerr << "Invalid non-option argument '" << argv[optind]
-			  << "'" << std::endl;
+		EPR("Invalid non-option argument '{}'\n", argv[optind]);
 		usage();
 		return options;
 	}
@@ -992,14 +990,9 @@ void OptionsParser::usage()
 
 	indent = (indent + 7) / 8 * 8;
 
-	std::cerr << "Options:" << std::endl;
-
-	std::ios_base::fmtflags f(std::cerr.flags());
-	std::cerr << std::left;
+	EPR("Options:\n");
 
 	usageOptions(options_, indent);
-
-	std::cerr.flags(f);
 }
 
 void OptionsParser::usageOptions(const std::list<Option> &options,
@@ -1036,16 +1029,16 @@ void OptionsParser::usageOptions(const std::list<Option> &options,
 		if (option.isArray)
 			argument += " ...";
 
-		std::cerr << std::setw(indent) << argument;
+		EPR("{:{}}", argument, indent);
 
-		for (const char *help = option.help, *end = help; end; ) {
+		for (const char *help = option.help, *end = help; end;) {
 			end = strchr(help, '\n');
 			if (end) {
-				std::cerr << std::string(help, end - help + 1);
-				std::cerr << std::setw(indent) << " ";
+				EPR(std::string(help, end - help + 1));
+				EPR("{:{}}", "", indent);
 				help = end + 1;
 			} else {
-				std::cerr << help << std::endl;
+				EPR("{}\n", help);
 			}
 		}
 
@@ -1060,8 +1053,8 @@ void OptionsParser::usageOptions(const std::list<Option> &options,
 		return;
 
 	for (const Option *option : parentOptions) {
-		std::cerr << std::endl << "Options valid in the context of "
-			  << option->optionName() << ":" << std::endl;
+		EPR("\nOptions valid in the context of {}:\n",
+		    option->optionName());
 		usageOptions(option->children, indent);
 	}
 }
@@ -1125,15 +1118,14 @@ bool OptionsParser::parseValue(const Option &option, const char *arg,
 
 	std::tie(options, error) = childOption(option.parent, options);
 	if (error) {
-		std::cerr << "Option " << option.optionName() << " requires a "
-			  << error->optionName() << " context" << std::endl;
+		EPR("Option {} requires a {} context\n",
+		    option.optionName(), error->optionName());
 		return false;
 	}
 
 	if (!options->parseValue(option.opt, option, arg)) {
-		std::cerr << "Can't parse " << option.typeName()
-			  << " argument for option " << option.optionName()
-			  << std::endl;
+		EPR("Can't parse {} argument for option {}\n",
+		    option.typeName(), option.optionName());
 		return false;
 	}
 
diff --git a/src/cam/stream_options.cpp b/src/cam/stream_options.cpp
index 150bd27c..666862eb 100644
--- a/src/cam/stream_options.cpp
+++ b/src/cam/stream_options.cpp
@@ -6,7 +6,10 @@
  */
 #include "stream_options.h"
 
-#include <iostream>
+#include <fmt/core.h>
+
+#define PR(...) fmt::print(__VA_ARGS__)
+#define EPR(...) fmt::print(stderr, __VA_ARGS__)
 
 using namespace libcamera;
 
@@ -30,8 +33,7 @@ KeyValueParser::Options StreamKeyValueParser::parse(const char *arguments)
 
 	if (options.valid() && options.isSet("role") &&
 	    !parseRole(&role, options)) {
-		std::cerr << "Unknown stream role "
-			  << options["role"].toString() << std::endl;
+		EPR("Unknown stream role {}\n", options["role"].toString());
 		options.invalidate();
 	}
 
@@ -64,7 +66,7 @@ int StreamKeyValueParser::updateConfiguration(CameraConfiguration *config,
 					      const OptionValue &values)
 {
 	if (!config) {
-		std::cerr << "No configuration provided" << std::endl;
+		EPR("No configuration provided\n");
 		return -EINVAL;
 	}
 
@@ -75,12 +77,8 @@ int StreamKeyValueParser::updateConfiguration(CameraConfiguration *config,
 	const std::vector<OptionValue> &streamParameters = values.toArray();
 
 	if (config->size() != streamParameters.size()) {
-		std::cerr
-			<< "Number of streams in configuration "
-			<< config->size()
-			<< " does not match number of streams parsed "
-			<< streamParameters.size()
-			<< std::endl;
+		EPR("Number of streams in configuration {} does not match number of streams parsed {}\n",
+		    config->size(), streamParameters.size());
 		return -EINVAL;
 	}
 
