@@ -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_,
@@ -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"
@@ -18,6 +18,7 @@ enum {
OptListProperties = 'p',
OptMonitor = 'm',
OptStream = 's',
+ OptSDL = 'S',
OptListControls = 256,
OptStrictFormats = 257,
OptMetadata = 258,
@@ -32,11 +32,21 @@ if libdrm.found()
])
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,
new file mode 100644
@@ -0,0 +1,198 @@
+/* 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 "event_loop.h"
+#include "image.h"
+
+using namespace libcamera;
+
+SDLSink::SDLSink()
+ : sdlWindow_(nullptr), sdlRenderer_(nullptr), sdlTexture_(nullptr), sdlRect_({}), sdlInit_(false)
+{
+}
+
+SDLSink::~SDLSink()
+{
+ stop();
+}
+
+int SDLSink::configure(const libcamera::CameraConfiguration &cfg)
+{
+ int ret = FrameSink::configure(cfg);
+ if (ret < 0)
+ return ret;
+
+ if (cfg.size() > 1) {
+ std::cerr << "SDL sink only supports one camera stream at present, streaming first camera stream"
+ << std::endl;
+ } else if (cfg.empty()) {
+ std::cerr << "Require at least one camera stream to process" << std::endl;
+ return -EINVAL;
+ }
+
+ const libcamera::StreamConfiguration &sCfg = cfg.at(0);
+ sdlRect_.w = sCfg.size.width;
+ sdlRect_.h = sCfg.size.height;
+ switch (sCfg.pixelFormat) {
+ case libcamera::formats::YUYV:
+ pixelFormat_ = SDL_PIXELFORMAT_YUY2;
+ pitch_ = 4 * ((sdlRect_.w + 1) / 2);
+ break;
+
+ /* From here down the fourcc values are identical between SDL, drm, libcamera */
+ case libcamera::formats::NV21:
+ pixelFormat_ = SDL_PIXELFORMAT_NV21;
+ pitch_ = sdlRect_.w;
+ break;
+ case libcamera::formats::NV12:
+ pixelFormat_ = SDL_PIXELFORMAT_NV12;
+ pitch_ = sdlRect_.w;
+ break;
+ case libcamera::formats::YVU420:
+ pixelFormat_ = SDL_PIXELFORMAT_YV12;
+ pitch_ = sdlRect_.w;
+ break;
+ case libcamera::formats::YVYU:
+ pixelFormat_ = SDL_PIXELFORMAT_YVYU;
+ pitch_ = 4 * ((sdlRect_.w + 1) / 2);
+ break;
+ case libcamera::formats::UYVY:
+ pixelFormat_ = SDL_PIXELFORMAT_UYVY;
+ pitch_ = 4 * ((sdlRect_.w + 1) / 2);
+ break;
+ default:
+ std::cerr << sCfg.pixelFormat.toString() << " not present in libcamera <-> SDL map"
+ << std::endl;
+ return -EINVAL;
+ };
+
+ return 0;
+}
+
+int SDLSink::start()
+{
+ int ret = SDL_Init(SDL_INIT_VIDEO);
+ if (ret) {
+ std::cerr << "Failed to initialize SDL: " << SDL_GetError() << std::endl;
+ return ret;
+ }
+
+ sdlInit_ = true;
+ sdlWindow_ = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, sdlRect_.w, sdlRect_.h, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
+ if (!sdlWindow_) {
+ std::cerr << "Failed to create SDL window: " << SDL_GetError() << std::endl;
+ return -EINVAL;
+ }
+
+ sdlRenderer_ = SDL_CreateRenderer(sdlWindow_, -1, 0);
+ if (!sdlRenderer_) {
+ std::cerr << "Failed to create SDL renderer: " << SDL_GetError() << std::endl;
+ return -EINVAL;
+ }
+
+ ret = SDL_RenderSetLogicalSize(sdlRenderer_, sdlRect_.w, sdlRect_.h);
+ if (ret) { /* Not critical to set, don't return in this case, set for scaling purposes */
+ std::cerr << "Failed to set SDL render logical size: " << SDL_GetError() << std::endl;
+ }
+
+ sdlTexture_ = SDL_CreateTexture(sdlRenderer_, pixelFormat_, SDL_TEXTUREACCESS_STREAMING, sdlRect_.w, sdlRect_.h);
+ if (!sdlTexture_) {
+ std::cerr << "Failed to create SDL texture: " << SDL_GetError() << std::endl;
+ return -EINVAL;
+ }
+
+ EventLoop::instance()->addTimerEvent(10000, std::bind(&SDLSink::processSDLEvents, this));
+
+ return 0;
+}
+
+int SDLSink::stop()
+{
+ if (sdlTexture_) {
+ SDL_DestroyTexture(sdlTexture_);
+ sdlTexture_ = nullptr;
+ }
+
+ if (sdlRenderer_) {
+ SDL_DestroyRenderer(sdlRenderer_);
+ sdlRenderer_ = nullptr;
+ }
+
+ if (sdlWindow_) {
+ SDL_DestroyWindow(sdlWindow_);
+ sdlWindow_ = nullptr;
+ }
+
+ if (sdlInit_) {
+ SDL_Quit();
+ sdlInit_ = false;
+ }
+
+ return FrameSink::stop();
+}
+
+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()) {
+ renderBuffer(buffer);
+ break; /* to be expanded to launch SDL window per buffer */
+ }
+
+ return true;
+}
+
+/*
+ * Process SDL events, required for things like window resize and quit button
+ */
+void SDLSink::processSDLEvents()
+{
+ for (SDL_Event e; SDL_PollEvent(&e);) {
+ if (e.type == SDL_QUIT) { /* click close icon then quit */
+ EventLoop::instance()->exit(0);
+ }
+ }
+}
+
+void SDLSink::renderBuffer(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(), pitch_);
+ SDL_RenderClear(sdlRenderer_);
+ SDL_RenderCopy(sdlRenderer_, sdlTexture_, nullptr, nullptr);
+ SDL_RenderPresent(sdlRenderer_);
+ }
+}
new file mode 100644
@@ -0,0 +1,46 @@
+/* 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;
+ int stop() override;
+ void mapBuffer(libcamera::FrameBuffer *buffer) override;
+
+ bool processRequest(libcamera::Request *request) override;
+
+private:
+ void renderBuffer(libcamera::FrameBuffer *buffer);
+ void processSDLEvents();
+
+ std::map<libcamera::FrameBuffer *, std::unique_ptr<Image>> mappedBuffers_;
+
+ SDL_Window *sdlWindow_;
+ SDL_Renderer *sdlRenderer_;
+ SDL_Texture *sdlTexture_;
+ SDL_Rect sdlRect_;
+ SDL_PixelFormatEnum pixelFormat_;
+ bool sdlInit_;
+ int pitch_;
+};