diff --git a/src/cam/camera_session.cpp b/src/cam/camera_session.cpp
index 0428b538..30162dbd 100644
--- a/src/cam/camera_session.cpp
+++ b/src/cam/camera_session.cpp
@@ -19,6 +19,9 @@
 #ifdef HAVE_KMS
 #include "kms_sink.h"
 #endif
+#ifdef HAVE_SDL
+#include "sdl_sink.h"
+#endif
 #include "main.h"
 #include "stream_options.h"
 
@@ -187,6 +190,11 @@ int CameraSession::start()
 		sink_ = std::make_unique<KMSSink>(options_[OptDisplay].toString());
 #endif
 
+#ifdef HAVE_SDL
+	if (options_.isSet(OptSDL))
+		sink_ = std::make_unique<SDLSink>();
+#endif
+
 	if (options_.isSet(OptFile)) {
 		if (!options_[OptFile].toString().empty())
 			sink_ = std::make_unique<FileSink>(streamNames_,
diff --git a/src/cam/main.cpp b/src/cam/main.cpp
index c7f664b9..1d62a64a 100644
--- a/src/cam/main.cpp
+++ b/src/cam/main.cpp
@@ -137,6 +137,11 @@ int CamApp::parseOptions(int argc, char *argv[])
 			 "Display viewfinder through DRM/KMS on specified connector",
 			 "display", ArgumentOptional, "connector", false,
 			 OptCamera);
+#endif
+#ifdef HAVE_SDL
+	parser.addOption(OptSDL, OptionNone,
+			 "Display viewfinder through SDL",
+			 "sdl", ArgumentNone, "", false, OptCamera);
 #endif
 	parser.addOption(OptFile, OptionString,
 			 "Write captured frames to disk\n"
diff --git a/src/cam/main.h b/src/cam/main.h
index 62f7bbc9..a64f95a0 100644
--- a/src/cam/main.h
+++ b/src/cam/main.h
@@ -11,6 +11,7 @@ enum {
 	OptCamera = 'c',
 	OptCapture = 'C',
 	OptDisplay = 'D',
+	OptSDL = 'S',
 	OptFile = 'F',
 	OptHelp = 'h',
 	OptInfo = 'I',
diff --git a/src/cam/meson.build b/src/cam/meson.build
index e8e2ae57..bd536c5b 100644
--- a/src/cam/meson.build
+++ b/src/cam/meson.build
@@ -32,11 +32,21 @@ cam_sources += files([
 ])
 endif
 
+libsdl2 = dependency('SDL2', required : false)
+
+if libsdl2.found()
+    cam_cpp_args += [ '-DHAVE_SDL' ]
+    cam_sources += files([
+        'sdl_sink.cpp'
+    ])
+endif
+
 cam  = executable('cam', cam_sources,
                   dependencies : [
                       libatomic,
                       libcamera_public,
                       libdrm,
+                      libsdl2,
                       libevent,
                   ],
                   cpp_args : cam_cpp_args,
diff --git a/src/cam/sdl_sink.cpp b/src/cam/sdl_sink.cpp
new file mode 100644
index 00000000..97f601b1
--- /dev/null
+++ b/src/cam/sdl_sink.cpp
@@ -0,0 +1,133 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * sdl_sink.cpp - SDL Sink
+ */
+
+#include "sdl_sink.h"
+
+#include <assert.h>
+#include <fcntl.h>
+#include <iomanip>
+#include <iostream>
+#include <signal.h>
+#include <sstream>
+#include <string.h>
+#include <unistd.h>
+
+#include <libcamera/camera.h>
+#include <libcamera/formats.h>
+
+#include "image.h"
+
+using namespace libcamera;
+
+SDLSink::SDLSink()
+	: sdlRenderer_(0)
+{
+	memset(&sdlRect_, 0, sizeof(sdlRect_));
+}
+
+SDLSink::~SDLSink()
+{
+	if (sdlRenderer_)
+		SDL_DestroyRenderer(sdlRenderer_);
+	SDL_Quit();
+}
+
+int SDLSink::configure(const libcamera::CameraConfiguration &cfg)
+{
+	int ret = FrameSink::configure(cfg);
+	if (ret < 0)
+		return ret;
+
+	const libcamera::StreamConfiguration &sCfg = cfg.at(0);
+	pf = (SDL_PixelFormatEnum)sCfg.pixelFormat.fourcc();
+	if (pf == SDL_DEFINE_PIXELFOURCC('Y', 'U', 'Y', 'V')) {
+		pf = SDL_PIXELFORMAT_YUY2;
+	} else if (int ne = strcmp(SDL_GetPixelFormatName(pf), "SDL_PIXELFORMAT_UNKNOWN"); !ne) {
+		std::cerr << "SDL_GetPixelFormatName error - exiting: SDL_PIXELFORMAT_UNKNOWN, no " << sCfg.pixelFormat.toString() << " support\n";
+		return -EINVAL;
+	}
+
+	sdlRect_.w = sCfg.size.width;
+	sdlRect_.h = sCfg.size.height;
+
+	return 0;
+}
+
+int SDLSink::start()
+{
+	int ret = SDL_Init(SDL_INIT_VIDEO);
+	if (ret) {
+		std::cerr << "SDL_Init error - exiting: " << SDL_GetError() << std::endl;
+		return ret;
+	}
+
+	sdlScreen_ = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED,
+				      SDL_WINDOWPOS_UNDEFINED, sdlRect_.w,
+				      sdlRect_.h, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
+	if (!sdlScreen_) {
+		std::cerr << "SDL_CreateWindow error - exiting: " << SDL_GetError() << std::endl;
+		return -EINVAL;
+	}
+
+	sdlRenderer_ = SDL_CreateRenderer(
+		sdlScreen_, -1, 0);
+	if (!sdlRenderer_) {
+		std::cerr << "SDL_CreateRenderer error - exiting: " << SDL_GetError() << std::endl;
+		return -EINVAL;
+	}
+
+	SDL_RenderSetLogicalSize(sdlRenderer_, sdlRect_.w,
+				 sdlRect_.h);
+
+	sdlTexture_ =
+		SDL_CreateTexture(sdlRenderer_, pf,
+				  SDL_TEXTUREACCESS_STREAMING, sdlRect_.w,
+				  sdlRect_.h);
+
+	return 0;
+}
+
+void SDLSink::mapBuffer(FrameBuffer *buffer)
+{
+	std::unique_ptr<Image> image =
+		Image::fromFrameBuffer(buffer, Image::MapMode::ReadOnly);
+	assert(image != nullptr);
+
+	mappedBuffers_[buffer] = std::move(image);
+}
+
+bool SDLSink::processRequest(Request *request)
+{
+	for (auto [stream, buffer] : request->buffers())
+		writeBuffer(buffer);
+
+	for (SDL_Event e; SDL_PollEvent(&e);) {
+		if (e.type == SDL_QUIT) { // click close icon then quit
+			kill(getpid(), SIGINT);
+		}
+	}
+
+	return true;
+}
+
+void SDLSink::writeBuffer(FrameBuffer *buffer)
+{
+	Image *image = mappedBuffers_[buffer].get();
+
+	for (unsigned int i = 0; i < buffer->planes().size(); ++i) {
+		const FrameMetadata::Plane &meta = buffer->metadata().planes()[i];
+
+		Span<uint8_t> data = image->data(i);
+		if (meta.bytesused > data.size())
+			std::cerr << "payload size " << meta.bytesused
+				  << " larger than plane size " << data.size()
+				  << std::endl;
+
+		SDL_UpdateTexture(sdlTexture_, &sdlRect_, data.data(), sdlRect_.w * 2);
+		SDL_RenderClear(sdlRenderer_);
+		SDL_RenderCopy(sdlRenderer_, sdlTexture_, NULL, NULL);
+		SDL_RenderPresent(sdlRenderer_);
+	}
+}
diff --git a/src/cam/sdl_sink.h b/src/cam/sdl_sink.h
new file mode 100644
index 00000000..f5f8982a
--- /dev/null
+++ b/src/cam/sdl_sink.h
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * sdl_sink.h - SDL Sink
+ */
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include <libcamera/stream.h>
+
+#include <SDL2/SDL.h>
+
+#include "frame_sink.h"
+
+class Image;
+
+class SDLSink : public FrameSink
+{
+public:
+	SDLSink();
+	~SDLSink();
+
+	int configure(const libcamera::CameraConfiguration &cfg) override;
+	int start() override;
+	void mapBuffer(libcamera::FrameBuffer *buffer) override;
+
+	bool processRequest(libcamera::Request *request) override;
+
+private:
+	void writeBuffer(libcamera::FrameBuffer *buffer);
+
+	std::map<libcamera::FrameBuffer *, std::unique_ptr<Image>> mappedBuffers_;
+	SDL_Window *sdlScreen_;
+	SDL_Renderer *sdlRenderer_;
+	SDL_Texture *sdlTexture_;
+	SDL_Rect sdlRect_;
+	SDL_PixelFormatEnum pf;
+};
