[libcamera-devel,2/2] qcam: Add Qt-based GUI application

Message ID 20190323073125.25497-3-laurent.pinchart@ideasonboard.com
State Accepted
Headers show
Series
  • Qt-based libcamera viewer
Related show

Commit Message

Laurent Pinchart March 23, 2019, 7:31 a.m. UTC
qcam is a sample camera GUI application based on Qt. It demonstrates
integration of the Qt event loop with libcamera.

The application lets the user select a camera through the GUI, and then
captures a single stream from the camera and displays it in a window.

Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
---
 src/meson.build                  |   1 +
 src/qcam/format_converter.cpp    |  99 ++++++++++++++
 src/qcam/format_converter.h      |  14 ++
 src/qcam/main.cpp                |  75 +++++++++++
 src/qcam/main_window.cpp         | 223 +++++++++++++++++++++++++++++++
 src/qcam/main_window.h           |  54 ++++++++
 src/qcam/meson.build             |  19 +++
 src/qcam/qt_event_dispatcher.cpp | 145 ++++++++++++++++++++
 src/qcam/qt_event_dispatcher.h   |  62 +++++++++
 src/qcam/viewfinder.cpp          |  38 ++++++
 src/qcam/viewfinder.h            |  31 +++++
 11 files changed, 761 insertions(+)
 create mode 100644 src/qcam/format_converter.cpp
 create mode 100644 src/qcam/format_converter.h
 create mode 100644 src/qcam/main.cpp
 create mode 100644 src/qcam/main_window.cpp
 create mode 100644 src/qcam/main_window.h
 create mode 100644 src/qcam/meson.build
 create mode 100644 src/qcam/qt_event_dispatcher.cpp
 create mode 100644 src/qcam/qt_event_dispatcher.h
 create mode 100644 src/qcam/viewfinder.cpp
 create mode 100644 src/qcam/viewfinder.h

Comments

Niklas Söderlund March 27, 2019, 12:42 a.m. UTC | #1
Hi Laurent,

Thanks for your work.

On 2019-03-23 09:31:25 +0200, Laurent Pinchart wrote:
> qcam is a sample camera GUI application based on Qt. It demonstrates
> integration of the Qt event loop with libcamera.
> 
> The application lets the user select a camera through the GUI, and then
> captures a single stream from the camera and displays it in a window.

Maybe it's worth mentioning in the commit message that qcam only works 
for YVYU pixelformats ?

> 
> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> ---
>  src/meson.build                  |   1 +
>  src/qcam/format_converter.cpp    |  99 ++++++++++++++
>  src/qcam/format_converter.h      |  14 ++
>  src/qcam/main.cpp                |  75 +++++++++++
>  src/qcam/main_window.cpp         | 223 +++++++++++++++++++++++++++++++
>  src/qcam/main_window.h           |  54 ++++++++
>  src/qcam/meson.build             |  19 +++
>  src/qcam/qt_event_dispatcher.cpp | 145 ++++++++++++++++++++
>  src/qcam/qt_event_dispatcher.h   |  62 +++++++++
>  src/qcam/viewfinder.cpp          |  38 ++++++
>  src/qcam/viewfinder.h            |  31 +++++
>  11 files changed, 761 insertions(+)
>  create mode 100644 src/qcam/format_converter.cpp
>  create mode 100644 src/qcam/format_converter.h
>  create mode 100644 src/qcam/main.cpp
>  create mode 100644 src/qcam/main_window.cpp
>  create mode 100644 src/qcam/main_window.h
>  create mode 100644 src/qcam/meson.build
>  create mode 100644 src/qcam/qt_event_dispatcher.cpp
>  create mode 100644 src/qcam/qt_event_dispatcher.h
>  create mode 100644 src/qcam/viewfinder.cpp
>  create mode 100644 src/qcam/viewfinder.h
> 
> diff --git a/src/meson.build b/src/meson.build
> index a7f2e75de158..4e41fd3e7887 100644
> --- a/src/meson.build
> +++ b/src/meson.build
> @@ -1,2 +1,3 @@
>  subdir('libcamera')
>  subdir('cam')
> +subdir('qcam')
> diff --git a/src/qcam/format_converter.cpp b/src/qcam/format_converter.cpp
> new file mode 100644
> index 000000000000..0decad74f28e
> --- /dev/null
> +++ b/src/qcam/format_converter.cpp
> @@ -0,0 +1,99 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2019, Google Inc.
> + *
> + * format_convert.cpp - qcam - Convert buffer to RGB
> + */
> +
> +#include <errno.h>
> +
> +#include <linux/videodev2.h>
> +
> +#include "format_converter.h"
> +
> +#define RGBSHIFT		8
> +#ifndef MAX
> +#define MAX(a,b)		((a)>(b)?(a):(b))
> +#endif
> +#ifndef MIN
> +#define MIN(a,b)		((a)<(b)?(a):(b))
> +#endif
> +#ifndef CLAMP
> +#define CLAMP(a,low,high)	MAX((low),MIN((high),(a)))
> +#endif
> +#ifndef CLIP
> +#define CLIP(x)			CLAMP(x,0,255)
> +#endif
> +
> +static void yuv_to_rgb(int y, int u, int v, int *r, int *g, int *b)
> +{
> +	int c = y - 16;
> +	int d = u - 128;
> +	int e = v - 128;
> +	*r = CLIP(( 298 * c           + 409 * e + 128) >> RGBSHIFT);
> +	*g = CLIP(( 298 * c - 100 * d - 208 * e + 128) >> RGBSHIFT);
> +	*b = CLIP(( 298 * c + 516 * d           + 128) >> RGBSHIFT);
> +}
> +
> +int buffer_to_rgb32(unsigned char *src, unsigned int format,
> +		    unsigned int width, unsigned int height,
> +		    unsigned char *dst)
> +{
> +	unsigned int src_x, src_y, dst_x, dst_y;
> +	unsigned int src_stride;
> +	unsigned int dst_stride;
> +	int y_pos, cb_pos, cr_pos;
> +	int r, g, b, y, cr, cb;
> +
> +	switch (format) {
> +	case V4L2_PIX_FMT_VYUY:
> +		y_pos = 1;
> +		cb_pos = 2;
> +		break;
> +	case V4L2_PIX_FMT_YVYU:
> +		y_pos = 0;
> +		cb_pos = 3;
> +		break;
> +	case V4L2_PIX_FMT_UYVY:
> +		y_pos = 1;
> +		cb_pos = 0;
> +		break;
> +	case V4L2_PIX_FMT_YUYV:
> +		y_pos = 0;
> +		cb_pos = 1;
> +		break;
> +	default:
> +		return -EINVAL;
> +	};
> +
> +	cr_pos = (cb_pos + 2) % 4;
> +	src_stride = width * 2;
> +	dst_stride = width * 4;
> +
> +	for (src_y = 0, dst_y = 0; dst_y < height; src_y++, dst_y++) {
> +		for (src_x = 0, dst_x = 0; dst_x < width; ) {
> +			cb = src[src_y * src_stride + src_x * 4 + cb_pos];
> +			cr = src[src_y * src_stride + src_x * 4 + cr_pos];
> +
> +			y = src[src_y * src_stride + src_x * 4 + y_pos];
> +			yuv_to_rgb(y, cb, cr, &r, &g, &b);
> +			dst[dst_y * dst_stride + 4 * dst_x + 0] = b;
> +			dst[dst_y * dst_stride + 4 * dst_x + 1] = g;
> +			dst[dst_y * dst_stride + 4 * dst_x + 2] = r;
> +			dst[dst_y * dst_stride + 4 * dst_x + 3] = 0xff;
> +			dst_x++;
> +
> +			y = src[src_y * src_stride + src_x * 4 + y_pos + 2];
> +			yuv_to_rgb(y, cb, cr, &r, &g, &b);
> +			dst[dst_y * dst_stride + 4 * dst_x + 0] = b;
> +			dst[dst_y * dst_stride + 4 * dst_x + 1] = g;
> +			dst[dst_y * dst_stride + 4 * dst_x + 2] = r;
> +			dst[dst_y * dst_stride + 4 * dst_x + 3] = 0xff;
> +			dst_x++;
> +
> +			src_x++;
> +		}
> +	}
> +
> +	return 0;
> +}
> diff --git a/src/qcam/format_converter.h b/src/qcam/format_converter.h
> new file mode 100644
> index 000000000000..3904b139a4fd
> --- /dev/null
> +++ b/src/qcam/format_converter.h
> @@ -0,0 +1,14 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2019, Google Inc.
> + *
> + * format_convert.h - qcam - Convert buffer to RGB
> + */
> +#ifndef __QCAM_FORMAT_CONVERTER_H__
> +#define __QCAM_FORMAT_CONVERTER_H__
> +
> +int buffer_to_rgb32(unsigned char *src, unsigned int format,
> +		    unsigned int width, unsigned int height,
> +		    unsigned char *dst);
> +
> +#endif /* __QCAM_FORMAT_CONVERTER_H__ */
> diff --git a/src/qcam/main.cpp b/src/qcam/main.cpp
> new file mode 100644
> index 000000000000..106b8a162d9f
> --- /dev/null
> +++ b/src/qcam/main.cpp
> @@ -0,0 +1,75 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2019, Google Inc.
> + *
> + * main.cpp - cam - The libcamera swiss army knife
> + */
> +
> +#include <iostream>
> +#include <signal.h>
> +#include <string.h>
> +
> +#include <QApplication>
> +
> +#include <libcamera/camera_manager.h>
> +
> +#include "main_window.h"
> +#include "../cam/options.h"
> +#include "qt_event_dispatcher.h"
> +
> +void signalHandler(int signal)
> +{
> +	std::cout << "Exiting" << std::endl;
> +	qApp->quit();
> +}
> +
> +OptionsParser::Options parseOptions(int argc, char *argv[])
> +{
> +	OptionsParser parser;
> +	parser.addOption(OptCamera, OptionString,
> +			 "Specify which camera to operate on", "camera",
> +			 ArgumentRequired, "camera");
> +	parser.addOption(OptHelp, OptionNone, "Display this help message",
> +			 "help");
> +
> +	OptionsParser::Options options = parser.parse(argc, argv);
> +	if (options.isSet(OptHelp))
> +		parser.usage();
> +
> +	return options;
> +}
> +
> +int main(int argc, char **argv)
> +{
> +	QApplication app(argc, argv);
> +	int ret;
> +
> +	OptionsParser::Options options = parseOptions(argc, argv);
> +	if (!options.valid())
> +		return EXIT_FAILURE;
> +	if (options.isSet(OptHelp))
> +		return 0;
> +
> +	struct sigaction sa = {};
> +	sa.sa_handler = &signalHandler;
> +	sigaction(SIGINT, &sa, nullptr);
> +
> +	std::unique_ptr<EventDispatcher> dispatcher(new QtEventDispatcher());
> +	CameraManager *cm = CameraManager::instance();
> +	cm->setEventDispatcher(std::move(dispatcher));
> +
> +	ret = cm->start();
> +	if (ret) {
> +		std::cout << "Failed to start camera manager: "
> +			  << strerror(-ret) << std::endl;
> +		return EXIT_FAILURE;
> +	}
> +
> +	MainWindow *mainWindow = new MainWindow(options);
> +	mainWindow->show();
> +	ret = app.exec();
> +	delete mainWindow;
> +
> +	cm->stop();
> +	return ret;
> +}
> diff --git a/src/qcam/main_window.cpp b/src/qcam/main_window.cpp
> new file mode 100644
> index 000000000000..d39dead76b56
> --- /dev/null
> +++ b/src/qcam/main_window.cpp
> @@ -0,0 +1,223 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2019, Google Inc.
> + *
> + * main_window.cpp - qcam - Main application window
> + */
> +
> +#include <iomanip>
> +#include <iostream>
> +#include <string>
> +
> +#include <QCoreApplication>
> +#include <QInputDialog>
> +#include <QTimer>
> +
> +#include <libcamera/camera_manager.h>
> +
> +#include "main_window.h"
> +#include "viewfinder.h"
> +
> +using namespace libcamera;
> +
> +MainWindow::MainWindow(const OptionsParser::Options &options)
> +	: options_(options), isCapturing_(false)
> +{
> +	int ret;
> +
> +	viewfinder_ = new ViewFinder(this);
> +	setCentralWidget(viewfinder_);
> +	viewfinder_->setFixedSize(500, 500);
> +	adjustSize();
> +
> +	ret = openCamera();
> +	if (!ret)
> +		ret = startCapture();
> +
> +	if (ret < 0)
> +		QTimer::singleShot(0, QCoreApplication::instance(),
> +				   &QCoreApplication::quit);
> +}
> +
> +MainWindow::~MainWindow()
> +{
> +	if (camera_) {
> +		stopCapture();
> +		camera_->release();
> +		camera_.reset();

I know both release() and reset() is needed but it looks but ugly :-P

> +	}
> +
> +	CameraManager::instance()->stop();
> +}
> +
> +int MainWindow::openCamera()
> +{
> +	CameraManager *cm = CameraManager::instance();
> +	std::string cameraName;
> +
> +	if (!options_.isSet(OptCamera)) {
> +		QStringList cameras;
> +		bool result;
> +
> +		for (const std::shared_ptr<Camera> &cam : cm->cameras())
> +			cameras.append(QString::fromStdString(cam->name()));
> +
> +		QString name = QInputDialog::getItem(this, "Select Camera",
> +						     "Camera:", cameras, 0,
> +						     false, &result);
> +		if (!result)
> +			return -EINVAL;
> +
> +		cameraName = name.toStdString();
> +	} else {
> +		cameraName = static_cast<std::string>(options_[OptCamera]);

Is this static_cast really needed? We have context here so the compiler 
should do the right thing and use the operator std::string, right? The 
only time I found my self the need to cast this is when redirecting it 
to std::c{err,out} as no context exists so it picks operator int.

> +	}
> +
> +	camera_ = cm->get(cameraName);
> +	if (!camera_) {
> +		std::cout << "Camera " << cameraName << " not found"
> +			  << std::endl;
> +		return -ENODEV;
> +	}
> +
> +	if (camera_->acquire()) {
> +		std::cout << "Failed to acquire camera" << std::endl;
> +		camera_.reset();
> +		return -EBUSY;
> +	}
> +
> +	std::cout << "Using camera " << camera_->name() << std::endl;
> +
> +	camera_->requestCompleted.connect(this, &MainWindow::requestComplete);
> +
> +	return 0;
> +}
> +
> +int MainWindow::startCapture()
> +{
> +	int ret;
> +
> +	Stream *stream = *camera_->streams().begin();
> +	std::set<Stream *> streams{ stream };
> +	config_ = camera_->streamConfiguration(streams);

This will conflict with my upcoming work to add stream usage hints, but 
it should be easy to handle. Please scratch my fears discussed on IRC I 
think we should merge qcam as soon as it's ready.

> +	ret = camera_->configureStreams(config_);
> +	if (ret < 0) {
> +		std::cout << "Failed to configure camera" << std::endl;
> +		return ret;
> +	}
> +
> +	const StreamConfiguration &sconf = config_[stream];
> +	viewfinder_->setFormat(sconf.pixelFormat, sconf.width, sconf.height);
> +	adjustSize();
> +
> +	ret = camera_->allocateBuffers();
> +	if (ret) {
> +		std::cerr << "Failed to allocate buffers"
> +			  << std::endl;
> +		return ret;
> +	}
> +
> +	BufferPool &pool = stream->bufferPool();
> +	std::vector<Request *> requests;
> +
> +	for (Buffer &buffer : pool.buffers()) {
> +		Request *request = camera_->createRequest();
> +		if (!request) {
> +			std::cerr << "Can't create request" << std::endl;
> +			ret = -ENOMEM;
> +			goto error;
> +		}
> +
> +		std::map<Stream *, Buffer *> map;
> +		map[stream] = &buffer;
> +		ret = request->setBuffers(map);
> +		if (ret < 0) {
> +			std::cerr << "Can't set buffers for request" << std::endl;
> +			goto error;
> +		}
> +
> +		requests.push_back(request);
> +	}
> +
> +	ret = camera_->start();
> +	if (ret) {
> +		std::cout << "Failed to start capture" << std::endl;
> +		goto error;
> +	}
> +
> +	for (Request *request : requests) {
> +		ret = camera_->queueRequest(request);
> +		if (ret < 0) {
> +			std::cerr << "Can't queue request" << std::endl;
> +			goto error;
> +		}
> +	}
> +
> +	isCapturing_ = true;
> +	return 0;
> +
> +error:
> +	for (Request *request : requests)
> +		delete request;
> +
> +	camera_->freeBuffers();
> +	return ret;
> +}
> +
> +void MainWindow::stopCapture()
> +{
> +	if (!isCapturing_)
> +		return;
> +
> +	int ret = camera_->stop();
> +	if (ret)
> +		std::cout << "Failed to stop capture" << std::endl;
> +
> +	camera_->freeBuffers();
> +	isCapturing_ = false;
> +}
> +
> +void MainWindow::requestComplete(Request *request,
> +				 const std::map<Stream *, Buffer *> &buffers)
> +{
> +	static uint64_t last = 0;
> +
> +	if (request->status() == Request::RequestCancelled)
> +		return;
> +
> +	Buffer *buffer = buffers.begin()->second;
> +
> +	double fps = buffer->timestamp() - last;
> +	fps = last && fps ? 1000000000.0 / fps : 0.0;
> +	last = buffer->timestamp();
> +
> +	std::cout << "seq: " << std::setw(6) << std::setfill('0') << buffer->sequence()
> +		  << " buf: " << buffer->index()
> +		  << " bytesused: " << buffer->bytesused()
> +		  << " timestamp: " << buffer->timestamp()
> +		  << " fps: " << std::fixed << std::setprecision(2) << fps
> +		  << std::endl;
> +
> +	display(buffer);
> +
> +	request = camera_->createRequest();
> +	if (!request) {
> +		std::cerr << "Can't create request" << std::endl;
> +		return;
> +	}
> +
> +	request->setBuffers(buffers);
> +	camera_->queueRequest(request);
> +}
> +
> +int MainWindow::display(Buffer *buffer)
> +{
> +	if (buffer->planes().size() != 1)
> +		return -EINVAL;
> +
> +	Plane &plane = buffer->planes().front();
> +	unsigned char *raw = static_cast<unsigned char *>(plane.mem());
> +	viewfinder_->display(raw);
> +
> +	return 0;
> +}
> diff --git a/src/qcam/main_window.h b/src/qcam/main_window.h
> new file mode 100644
> index 000000000000..5e27a8fd6b4e
> --- /dev/null
> +++ b/src/qcam/main_window.h
> @@ -0,0 +1,54 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2019, Google Inc.
> + *
> + * main_window.h - qcam - Main application window
> + */
> +#ifndef __QCAM_MAIN_WINDOW_H__
> +#define __QCAM_MAIN_WINDOW_H__
> +
> +#include <map>
> +
> +#include <QMainWindow>
> +
> +#include <libcamera/camera.h>
> +#include <libcamera/stream.h>
> +
> +#include "../cam/options.h"
> +
> +using namespace libcamera;
> +
> +class ViewFinder;
> +
> +enum {
> +	OptCamera = 'c',
> +	OptHelp = 'h',
> +};
> +
> +class MainWindow : public QMainWindow
> +{
> +public:
> +	MainWindow(const OptionsParser::Options &options);
> +	~MainWindow();
> +
> +private:
> +	int openCamera();
> +
> +	int startCapture();
> +	int configureStreams(Camera *camera, std::set<Stream *> &streams);
> +	void stopCapture();
> +
> +	void requestComplete(Request *request,
> +			     const std::map<Stream *, Buffer *> &buffers);
> +	int display(Buffer *buffer);
> +
> +	const OptionsParser::Options &options_;
> +
> +	std::shared_ptr<Camera> camera_;
> +	bool isCapturing_;
> +	std::map<Stream *, StreamConfiguration> config_;
> +
> +	ViewFinder *viewfinder_;
> +};
> +
> +#endif /* __QCAM_MAIN_WINDOW__ */
> diff --git a/src/qcam/meson.build b/src/qcam/meson.build
> new file mode 100644
> index 000000000000..8a71cda3dfe5
> --- /dev/null
> +++ b/src/qcam/meson.build
> @@ -0,0 +1,19 @@
> +qcam_sources = files([
> +    'format_converter.cpp',
> +    'main.cpp',
> +    'main_window.cpp',
> +    '../cam/options.cpp',
> +    'qt_event_dispatcher.cpp',
> +    'viewfinder.cpp',
> +])
> +
> +import('qt5')
> +qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Widgets'], required : false)
> +
> +if qt5_dep.found()
> +    qcam  = executable('qcam', qcam_sources,
> +                       link_with : libcamera,
> +                       include_directories : libcamera_includes,
> +                       dependencies : qt5_dep,
> +                       cpp_args : '-DQT_NO_KEYWORDS')
> +endif
> diff --git a/src/qcam/qt_event_dispatcher.cpp b/src/qcam/qt_event_dispatcher.cpp
> new file mode 100644
> index 000000000000..5ba451bf88ce
> --- /dev/null
> +++ b/src/qcam/qt_event_dispatcher.cpp
> @@ -0,0 +1,145 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2019, Google Inc.
> + *
> + * qt_event_dispatcher.cpp - qcam - Qt-based event dispatcher
> + */
> +
> +#include <iostream>
> +
> +#include <QAbstractEventDispatcher>
> +#include <QCoreApplication>
> +#include <QSocketNotifier>
> +#include <QTimerEvent>
> +
> +#include <libcamera/event_notifier.h>
> +#include <libcamera/timer.h>
> +
> +#include "qt_event_dispatcher.h"
> +
> +using namespace libcamera;
> +
> +QtEventDispatcher::QtEventDispatcher()
> +{
> +}
> +
> +QtEventDispatcher::~QtEventDispatcher()
> +{
> +	for (auto &it : notifiers_) {
> +		NotifierSet &set = it.second;
> +		delete set.read.qnotifier;
> +		delete set.write.qnotifier;
> +		delete set.exception.qnotifier;
> +	}
> +}
> +
> +void QtEventDispatcher::registerEventNotifier(EventNotifier *notifier)
> +{
> +	NotifierSet &set = notifiers_[notifier->fd()];
> +	QSocketNotifier::Type qtype;
> +	void (QtEventDispatcher::*method)(int);
> +	NotifierPair *pair;
> +
> +	switch (notifier->type()) {
> +	case EventNotifier::Read:
> +	default:
> +		qtype = QSocketNotifier::Read;
> +		method = &QtEventDispatcher::readNotifierActivated;
> +		pair = &set.read;
> +		break;
> +
> +	case EventNotifier::Write:
> +		qtype = QSocketNotifier::Write;
> +		method = &QtEventDispatcher::writeNotifierActivated;
> +		pair = &set.write;
> +		break;
> +
> +	case EventNotifier::Exception:
> +		qtype = QSocketNotifier::Exception;
> +		method = &QtEventDispatcher::exceptionNotifierActivated;
> +		pair = &set.exception;
> +		break;
> +	}
> +
> +	QSocketNotifier *qnotifier = new QSocketNotifier(notifier->fd(), qtype);
> +	connect(qnotifier, &QSocketNotifier::activated, this, method);
> +	pair->notifier = notifier;
> +	pair->qnotifier = qnotifier;
> +}
> +
> +void QtEventDispatcher::unregisterEventNotifier(EventNotifier *notifier)
> +{
> +	NotifierSet &set = notifiers_[notifier->fd()];
> +	NotifierPair *pair;
> +
> +	switch (notifier->type()) {
> +	case EventNotifier::Read:
> +	default:
> +		pair = &set.read;
> +		break;
> +
> +	case EventNotifier::Write:
> +		pair = &set.write;
> +		break;
> +
> +	case EventNotifier::Exception:
> +		pair = &set.exception;
> +		break;
> +	}
> +
> +	delete pair->qnotifier;
> +	pair->qnotifier = nullptr;
> +	pair->notifier = nullptr;
> +}
> +
> +void QtEventDispatcher::readNotifierActivated(int socket)
> +{
> +	EventNotifier *notifier = notifiers_[socket].read.notifier;
> +	notifier->activated.emit(notifier);
> +}
> +
> +void QtEventDispatcher::writeNotifierActivated(int socket)
> +{
> +	EventNotifier *notifier = notifiers_[socket].write.notifier;
> +	notifier->activated.emit(notifier);
> +}
> +
> +void QtEventDispatcher::exceptionNotifierActivated(int socket)
> +{
> +	EventNotifier *notifier = notifiers_[socket].exception.notifier;
> +	notifier->activated.emit(notifier);
> +}
> +
> +void QtEventDispatcher::registerTimer(Timer *timer)
> +{
> +	int timerId = startTimer(timer->interval());
> +	timers_[timerId] = timer;
> +	timerIds_[timer] = timerId;
> +}
> +
> +void QtEventDispatcher::unregisterTimer(Timer *timer)
> +{
> +	auto it = timerIds_.find(timer);
> +	timers_.erase(it->second);
> +	killTimer(it->second);
> +	timerIds_.erase(it);
> +}
> +
> +void QtEventDispatcher::timerEvent(QTimerEvent *event)
> +{
> +	auto it = timers_.find(event->timerId());
> +	timerIds_.erase(it->second);
> +	killTimer(it->first);
> +	timers_.erase(it);
> +}
> +
> +void QtEventDispatcher::processEvents()
> +{
> +	std::cout << "QtEventDispatcher::processEvents() should not be called"
> +		  << std::endl;
> +}
> +
> +void QtEventDispatcher::interrupt()
> +{
> +	QCoreApplication::eventDispatcher()->interrupt();
> +}
> diff --git a/src/qcam/qt_event_dispatcher.h b/src/qcam/qt_event_dispatcher.h
> new file mode 100644
> index 000000000000..b0f123e52d06
> --- /dev/null
> +++ b/src/qcam/qt_event_dispatcher.h
> @@ -0,0 +1,62 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2019, Google Inc.
> + *
> + * qt_event_dispatcher.h - qcam - Qt-based event dispatcher
> + */
> +#ifndef __QCAM_QT_EVENT_DISPATCHER_H__
> +#define __QCAM_QT_EVENT_DISPATCHER_H__
> +
> +#include <map>
> +
> +#include <libcamera/event_dispatcher.h>
> +
> +using namespace libcamera;
> +
> +class QSocketNotifier;
> +
> +class QtEventDispatcher final : public EventDispatcher, public QObject
> +{
> +public:
> +	QtEventDispatcher();
> +	~QtEventDispatcher();
> +
> +	void registerEventNotifier(EventNotifier *notifier);
> +	void unregisterEventNotifier(EventNotifier *notifier);
> +
> +	void registerTimer(Timer *timer);
> +	void unregisterTimer(Timer *timer);
> +
> +	void processEvents();
> +
> +	void interrupt();
> +
> +protected:
> +	void timerEvent(QTimerEvent *event);
> +
> +private:
> +	void readNotifierActivated(int socket);
> +	void writeNotifierActivated(int socket);
> +	void exceptionNotifierActivated(int socket);
> +
> +	struct NotifierPair {
> +		NotifierPair()
> +			: notifier(nullptr), qnotifier(nullptr)
> +		{
> +		}
> +		EventNotifier *notifier;
> +		QSocketNotifier *qnotifier;
> +	};
> +
> +	struct NotifierSet {
> +		NotifierPair read;
> +		NotifierPair write;
> +		NotifierPair exception;
> +	};
> +
> +	std::map<int, NotifierSet> notifiers_;
> +	std::map<int, Timer *> timers_;
> +	std::map<Timer *, int> timerIds_;
> +};
> +
> +#endif /* __QCAM_QT_EVENT_DISPATCHER_H__ */
> diff --git a/src/qcam/viewfinder.cpp b/src/qcam/viewfinder.cpp
> new file mode 100644
> index 000000000000..5db55ebbf098
> --- /dev/null
> +++ b/src/qcam/viewfinder.cpp
> @@ -0,0 +1,38 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2019, Google Inc.
> + *
> + * viewfinder.cpp - qcam - Viewfinder
> + */
> +
> +#include <QImage>
> +#include <QPixmap>
> +
> +#include "format_converter.h"
> +#include "viewfinder.h"
> +
> +ViewFinder::ViewFinder(QWidget *parent)
> +	: QLabel(parent), format_(0), width_(0), height_(0), image_(nullptr)
> +{
> +}
> +
> +void ViewFinder::display(unsigned char *raw)
> +{
> +	buffer_to_rgb32(raw, format_, width_, height_, image_->bits());

You should check the return value here as if a none YVYU format is used 
it will return -EINVAL and no image will be displayed which might 
confuse the user.

> +
> +	QPixmap pixmap = QPixmap::fromImage(*image_);
> +	setPixmap(pixmap);
> +}
> +
> +void ViewFinder::setFormat(unsigned int format, unsigned int width,
> +			   unsigned int height)
> +{
> +	format_ = format;
> +	width_ = width;
> +	height_ = height;
> +
> +	setFixedSize(width, height);
> +
> +	delete image_;
> +	image_ = new QImage(width, height, QImage::Format_RGB32);
> +}
> diff --git a/src/qcam/viewfinder.h b/src/qcam/viewfinder.h
> new file mode 100644
> index 000000000000..ecae290a5043
> --- /dev/null
> +++ b/src/qcam/viewfinder.h
> @@ -0,0 +1,31 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2019, Google Inc.
> + *
> + * viewfinder.h - qcam - Viewfinder
> + */
> +#ifndef __QCAM_VIEWFINDER_H__
> +#define __QCAM_VIEWFINDER_H__
> +
> +#include <QLabel>
> +
> +class QImage;
> +
> +class ViewFinder : public QLabel
> +{
> +public:
> +	ViewFinder(QWidget *parent);
> +
> +	void setFormat(unsigned int format, unsigned int width,
> +		       unsigned int height);
> +	void display(unsigned char *rgb);
> +
> +private:
> +	unsigned int format_;
> +	unsigned int width_;
> +	unsigned int height_;
> +
> +	QImage *image_;
> +};
> +
> +#endif /* __QCAM_VIEWFINDER__ */
> -- 
> Regards,
> 
> Laurent Pinchart
> 
> _______________________________________________
> libcamera-devel mailing list
> libcamera-devel@lists.libcamera.org
> https://lists.libcamera.org/listinfo/libcamera-devel
Laurent Pinchart March 29, 2019, 2:50 p.m. UTC | #2
Hi Niklas,

On Wed, Mar 27, 2019 at 01:42:00AM +0100, Niklas Söderlund wrote:
> On 2019-03-23 09:31:25 +0200, Laurent Pinchart wrote:
> > qcam is a sample camera GUI application based on Qt. It demonstrates
> > integration of the Qt event loop with libcamera.
> > 
> > The application lets the user select a camera through the GUI, and then
> > captures a single stream from the camera and displays it in a window.
> 
> Maybe it's worth mentioning in the commit message that qcam only works 
> for YVYU pixelformats ?

I'll do so.

> > Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> > ---
> >  src/meson.build                  |   1 +
> >  src/qcam/format_converter.cpp    |  99 ++++++++++++++
> >  src/qcam/format_converter.h      |  14 ++
> >  src/qcam/main.cpp                |  75 +++++++++++
> >  src/qcam/main_window.cpp         | 223 +++++++++++++++++++++++++++++++
> >  src/qcam/main_window.h           |  54 ++++++++
> >  src/qcam/meson.build             |  19 +++
> >  src/qcam/qt_event_dispatcher.cpp | 145 ++++++++++++++++++++
> >  src/qcam/qt_event_dispatcher.h   |  62 +++++++++
> >  src/qcam/viewfinder.cpp          |  38 ++++++
> >  src/qcam/viewfinder.h            |  31 +++++
> >  11 files changed, 761 insertions(+)
> >  create mode 100644 src/qcam/format_converter.cpp
> >  create mode 100644 src/qcam/format_converter.h
> >  create mode 100644 src/qcam/main.cpp
> >  create mode 100644 src/qcam/main_window.cpp
> >  create mode 100644 src/qcam/main_window.h
> >  create mode 100644 src/qcam/meson.build
> >  create mode 100644 src/qcam/qt_event_dispatcher.cpp
> >  create mode 100644 src/qcam/qt_event_dispatcher.h
> >  create mode 100644 src/qcam/viewfinder.cpp
> >  create mode 100644 src/qcam/viewfinder.h

[snip]

> > diff --git a/src/qcam/main_window.cpp b/src/qcam/main_window.cpp
> > new file mode 100644
> > index 000000000000..d39dead76b56
> > --- /dev/null
> > +++ b/src/qcam/main_window.cpp
> > @@ -0,0 +1,223 @@
> > +/* SPDX-License-Identifier: GPL-2.0-or-later */
> > +/*
> > + * Copyright (C) 2019, Google Inc.
> > + *
> > + * main_window.cpp - qcam - Main application window
> > + */
> > +
> > +#include <iomanip>
> > +#include <iostream>
> > +#include <string>
> > +
> > +#include <QCoreApplication>
> > +#include <QInputDialog>
> > +#include <QTimer>
> > +
> > +#include <libcamera/camera_manager.h>
> > +
> > +#include "main_window.h"
> > +#include "viewfinder.h"
> > +
> > +using namespace libcamera;
> > +
> > +MainWindow::MainWindow(const OptionsParser::Options &options)
> > +	: options_(options), isCapturing_(false)
> > +{
> > +	int ret;
> > +
> > +	viewfinder_ = new ViewFinder(this);
> > +	setCentralWidget(viewfinder_);
> > +	viewfinder_->setFixedSize(500, 500);
> > +	adjustSize();
> > +
> > +	ret = openCamera();
> > +	if (!ret)
> > +		ret = startCapture();
> > +
> > +	if (ret < 0)
> > +		QTimer::singleShot(0, QCoreApplication::instance(),
> > +				   &QCoreApplication::quit);
> > +}
> > +
> > +MainWindow::~MainWindow()
> > +{
> > +	if (camera_) {
> > +		stopCapture();
> > +		camera_->release();
> > +		camera_.reset();
> 
> I know both release() and reset() is needed but it looks but ugly :-P

I agree. Any proposal to improve this ? :-)

> > +	}
> > +
> > +	CameraManager::instance()->stop();
> > +}
> > +
> > +int MainWindow::openCamera()
> > +{
> > +	CameraManager *cm = CameraManager::instance();
> > +	std::string cameraName;
> > +
> > +	if (!options_.isSet(OptCamera)) {
> > +		QStringList cameras;
> > +		bool result;
> > +
> > +		for (const std::shared_ptr<Camera> &cam : cm->cameras())
> > +			cameras.append(QString::fromStdString(cam->name()));
> > +
> > +		QString name = QInputDialog::getItem(this, "Select Camera",
> > +						     "Camera:", cameras, 0,
> > +						     false, &result);
> > +		if (!result)
> > +			return -EINVAL;
> > +
> > +		cameraName = name.toStdString();
> > +	} else {
> > +		cameraName = static_cast<std::string>(options_[OptCamera]);
> 
> Is this static_cast really needed? We have context here so the compiler 
> should do the right thing and use the operator std::string, right? The 
> only time I found my self the need to cast this is when redirecting it 
> to std::c{err,out} as no context exists so it picks operator int.

The compiler otherwise complains about an ambiguous overload for
operator=.

> > +	}
> > +
> > +	camera_ = cm->get(cameraName);
> > +	if (!camera_) {
> > +		std::cout << "Camera " << cameraName << " not found"
> > +			  << std::endl;
> > +		return -ENODEV;
> > +	}
> > +
> > +	if (camera_->acquire()) {
> > +		std::cout << "Failed to acquire camera" << std::endl;
> > +		camera_.reset();
> > +		return -EBUSY;
> > +	}
> > +
> > +	std::cout << "Using camera " << camera_->name() << std::endl;
> > +
> > +	camera_->requestCompleted.connect(this, &MainWindow::requestComplete);
> > +
> > +	return 0;
> > +}
> > +
> > +int MainWindow::startCapture()
> > +{
> > +	int ret;
> > +
> > +	Stream *stream = *camera_->streams().begin();
> > +	std::set<Stream *> streams{ stream };
> > +	config_ = camera_->streamConfiguration(streams);
> 
> This will conflict with my upcoming work to add stream usage hints, but 
> it should be easy to handle. Please scratch my fears discussed on IRC I 
> think we should merge qcam as soon as it's ready.

OK.

> > +	ret = camera_->configureStreams(config_);
> > +	if (ret < 0) {
> > +		std::cout << "Failed to configure camera" << std::endl;
> > +		return ret;
> > +	}
> > +
> > +	const StreamConfiguration &sconf = config_[stream];
> > +	viewfinder_->setFormat(sconf.pixelFormat, sconf.width, sconf.height);
> > +	adjustSize();
> > +
> > +	ret = camera_->allocateBuffers();
> > +	if (ret) {
> > +		std::cerr << "Failed to allocate buffers"
> > +			  << std::endl;
> > +		return ret;
> > +	}
> > +
> > +	BufferPool &pool = stream->bufferPool();
> > +	std::vector<Request *> requests;
> > +
> > +	for (Buffer &buffer : pool.buffers()) {
> > +		Request *request = camera_->createRequest();
> > +		if (!request) {
> > +			std::cerr << "Can't create request" << std::endl;
> > +			ret = -ENOMEM;
> > +			goto error;
> > +		}
> > +
> > +		std::map<Stream *, Buffer *> map;
> > +		map[stream] = &buffer;
> > +		ret = request->setBuffers(map);
> > +		if (ret < 0) {
> > +			std::cerr << "Can't set buffers for request" << std::endl;
> > +			goto error;
> > +		}
> > +
> > +		requests.push_back(request);
> > +	}
> > +
> > +	ret = camera_->start();
> > +	if (ret) {
> > +		std::cout << "Failed to start capture" << std::endl;
> > +		goto error;
> > +	}
> > +
> > +	for (Request *request : requests) {
> > +		ret = camera_->queueRequest(request);
> > +		if (ret < 0) {
> > +			std::cerr << "Can't queue request" << std::endl;
> > +			goto error;
> > +		}
> > +	}
> > +
> > +	isCapturing_ = true;
> > +	return 0;
> > +
> > +error:
> > +	for (Request *request : requests)
> > +		delete request;
> > +
> > +	camera_->freeBuffers();
> > +	return ret;
> > +}
> > +
> > +void MainWindow::stopCapture()
> > +{
> > +	if (!isCapturing_)
> > +		return;
> > +
> > +	int ret = camera_->stop();
> > +	if (ret)
> > +		std::cout << "Failed to stop capture" << std::endl;
> > +
> > +	camera_->freeBuffers();
> > +	isCapturing_ = false;
> > +}
> > +
> > +void MainWindow::requestComplete(Request *request,
> > +				 const std::map<Stream *, Buffer *> &buffers)
> > +{
> > +	static uint64_t last = 0;
> > +
> > +	if (request->status() == Request::RequestCancelled)
> > +		return;
> > +
> > +	Buffer *buffer = buffers.begin()->second;
> > +
> > +	double fps = buffer->timestamp() - last;
> > +	fps = last && fps ? 1000000000.0 / fps : 0.0;
> > +	last = buffer->timestamp();
> > +
> > +	std::cout << "seq: " << std::setw(6) << std::setfill('0') << buffer->sequence()
> > +		  << " buf: " << buffer->index()
> > +		  << " bytesused: " << buffer->bytesused()
> > +		  << " timestamp: " << buffer->timestamp()
> > +		  << " fps: " << std::fixed << std::setprecision(2) << fps
> > +		  << std::endl;
> > +
> > +	display(buffer);
> > +
> > +	request = camera_->createRequest();
> > +	if (!request) {
> > +		std::cerr << "Can't create request" << std::endl;
> > +		return;
> > +	}
> > +
> > +	request->setBuffers(buffers);
> > +	camera_->queueRequest(request);
> > +}
> > +
> > +int MainWindow::display(Buffer *buffer)
> > +{
> > +	if (buffer->planes().size() != 1)
> > +		return -EINVAL;
> > +
> > +	Plane &plane = buffer->planes().front();
> > +	unsigned char *raw = static_cast<unsigned char *>(plane.mem());
> > +	viewfinder_->display(raw);
> > +
> > +	return 0;
> > +}

[snip]

> > diff --git a/src/qcam/viewfinder.cpp b/src/qcam/viewfinder.cpp
> > new file mode 100644
> > index 000000000000..5db55ebbf098
> > --- /dev/null
> > +++ b/src/qcam/viewfinder.cpp
> > @@ -0,0 +1,38 @@
> > +/* SPDX-License-Identifier: GPL-2.0-or-later */
> > +/*
> > + * Copyright (C) 2019, Google Inc.
> > + *
> > + * viewfinder.cpp - qcam - Viewfinder
> > + */
> > +
> > +#include <QImage>
> > +#include <QPixmap>
> > +
> > +#include "format_converter.h"
> > +#include "viewfinder.h"
> > +
> > +ViewFinder::ViewFinder(QWidget *parent)
> > +	: QLabel(parent), format_(0), width_(0), height_(0), image_(nullptr)
> > +{
> > +}
> > +
> > +void ViewFinder::display(unsigned char *raw)
> > +{
> > +	buffer_to_rgb32(raw, format_, width_, height_, image_->bits());
> 
> You should check the return value here as if a none YVYU format is used 
> it will return -EINVAL and no image will be displayed which might 
> confuse the user.

I will instead do so in MainWindow::startCapture() and not start the
stream if YUYV is not supported.

> > +
> > +	QPixmap pixmap = QPixmap::fromImage(*image_);
> > +	setPixmap(pixmap);
> > +}
> > +
> > +void ViewFinder::setFormat(unsigned int format, unsigned int width,
> > +			   unsigned int height)
> > +{
> > +	format_ = format;
> > +	width_ = width;
> > +	height_ = height;
> > +
> > +	setFixedSize(width, height);
> > +
> > +	delete image_;
> > +	image_ = new QImage(width, height, QImage::Format_RGB32);
> > +}
> > diff --git a/src/qcam/viewfinder.h b/src/qcam/viewfinder.h
> > new file mode 100644
> > index 000000000000..ecae290a5043
> > --- /dev/null
> > +++ b/src/qcam/viewfinder.h
> > @@ -0,0 +1,31 @@
> > +/* SPDX-License-Identifier: GPL-2.0-or-later */
> > +/*
> > + * Copyright (C) 2019, Google Inc.
> > + *
> > + * viewfinder.h - qcam - Viewfinder
> > + */
> > +#ifndef __QCAM_VIEWFINDER_H__
> > +#define __QCAM_VIEWFINDER_H__
> > +
> > +#include <QLabel>
> > +
> > +class QImage;
> > +
> > +class ViewFinder : public QLabel
> > +{
> > +public:
> > +	ViewFinder(QWidget *parent);
> > +
> > +	void setFormat(unsigned int format, unsigned int width,
> > +		       unsigned int height);
> > +	void display(unsigned char *rgb);
> > +
> > +private:
> > +	unsigned int format_;
> > +	unsigned int width_;
> > +	unsigned int height_;
> > +
> > +	QImage *image_;
> > +};
> > +
> > +#endif /* __QCAM_VIEWFINDER__ */

Patch

diff --git a/src/meson.build b/src/meson.build
index a7f2e75de158..4e41fd3e7887 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,2 +1,3 @@ 
 subdir('libcamera')
 subdir('cam')
+subdir('qcam')
diff --git a/src/qcam/format_converter.cpp b/src/qcam/format_converter.cpp
new file mode 100644
index 000000000000..0decad74f28e
--- /dev/null
+++ b/src/qcam/format_converter.cpp
@@ -0,0 +1,99 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * format_convert.cpp - qcam - Convert buffer to RGB
+ */
+
+#include <errno.h>
+
+#include <linux/videodev2.h>
+
+#include "format_converter.h"
+
+#define RGBSHIFT		8
+#ifndef MAX
+#define MAX(a,b)		((a)>(b)?(a):(b))
+#endif
+#ifndef MIN
+#define MIN(a,b)		((a)<(b)?(a):(b))
+#endif
+#ifndef CLAMP
+#define CLAMP(a,low,high)	MAX((low),MIN((high),(a)))
+#endif
+#ifndef CLIP
+#define CLIP(x)			CLAMP(x,0,255)
+#endif
+
+static void yuv_to_rgb(int y, int u, int v, int *r, int *g, int *b)
+{
+	int c = y - 16;
+	int d = u - 128;
+	int e = v - 128;
+	*r = CLIP(( 298 * c           + 409 * e + 128) >> RGBSHIFT);
+	*g = CLIP(( 298 * c - 100 * d - 208 * e + 128) >> RGBSHIFT);
+	*b = CLIP(( 298 * c + 516 * d           + 128) >> RGBSHIFT);
+}
+
+int buffer_to_rgb32(unsigned char *src, unsigned int format,
+		    unsigned int width, unsigned int height,
+		    unsigned char *dst)
+{
+	unsigned int src_x, src_y, dst_x, dst_y;
+	unsigned int src_stride;
+	unsigned int dst_stride;
+	int y_pos, cb_pos, cr_pos;
+	int r, g, b, y, cr, cb;
+
+	switch (format) {
+	case V4L2_PIX_FMT_VYUY:
+		y_pos = 1;
+		cb_pos = 2;
+		break;
+	case V4L2_PIX_FMT_YVYU:
+		y_pos = 0;
+		cb_pos = 3;
+		break;
+	case V4L2_PIX_FMT_UYVY:
+		y_pos = 1;
+		cb_pos = 0;
+		break;
+	case V4L2_PIX_FMT_YUYV:
+		y_pos = 0;
+		cb_pos = 1;
+		break;
+	default:
+		return -EINVAL;
+	};
+
+	cr_pos = (cb_pos + 2) % 4;
+	src_stride = width * 2;
+	dst_stride = width * 4;
+
+	for (src_y = 0, dst_y = 0; dst_y < height; src_y++, dst_y++) {
+		for (src_x = 0, dst_x = 0; dst_x < width; ) {
+			cb = src[src_y * src_stride + src_x * 4 + cb_pos];
+			cr = src[src_y * src_stride + src_x * 4 + cr_pos];
+
+			y = src[src_y * src_stride + src_x * 4 + y_pos];
+			yuv_to_rgb(y, cb, cr, &r, &g, &b);
+			dst[dst_y * dst_stride + 4 * dst_x + 0] = b;
+			dst[dst_y * dst_stride + 4 * dst_x + 1] = g;
+			dst[dst_y * dst_stride + 4 * dst_x + 2] = r;
+			dst[dst_y * dst_stride + 4 * dst_x + 3] = 0xff;
+			dst_x++;
+
+			y = src[src_y * src_stride + src_x * 4 + y_pos + 2];
+			yuv_to_rgb(y, cb, cr, &r, &g, &b);
+			dst[dst_y * dst_stride + 4 * dst_x + 0] = b;
+			dst[dst_y * dst_stride + 4 * dst_x + 1] = g;
+			dst[dst_y * dst_stride + 4 * dst_x + 2] = r;
+			dst[dst_y * dst_stride + 4 * dst_x + 3] = 0xff;
+			dst_x++;
+
+			src_x++;
+		}
+	}
+
+	return 0;
+}
diff --git a/src/qcam/format_converter.h b/src/qcam/format_converter.h
new file mode 100644
index 000000000000..3904b139a4fd
--- /dev/null
+++ b/src/qcam/format_converter.h
@@ -0,0 +1,14 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * format_convert.h - qcam - Convert buffer to RGB
+ */
+#ifndef __QCAM_FORMAT_CONVERTER_H__
+#define __QCAM_FORMAT_CONVERTER_H__
+
+int buffer_to_rgb32(unsigned char *src, unsigned int format,
+		    unsigned int width, unsigned int height,
+		    unsigned char *dst);
+
+#endif /* __QCAM_FORMAT_CONVERTER_H__ */
diff --git a/src/qcam/main.cpp b/src/qcam/main.cpp
new file mode 100644
index 000000000000..106b8a162d9f
--- /dev/null
+++ b/src/qcam/main.cpp
@@ -0,0 +1,75 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * main.cpp - cam - The libcamera swiss army knife
+ */
+
+#include <iostream>
+#include <signal.h>
+#include <string.h>
+
+#include <QApplication>
+
+#include <libcamera/camera_manager.h>
+
+#include "main_window.h"
+#include "../cam/options.h"
+#include "qt_event_dispatcher.h"
+
+void signalHandler(int signal)
+{
+	std::cout << "Exiting" << std::endl;
+	qApp->quit();
+}
+
+OptionsParser::Options parseOptions(int argc, char *argv[])
+{
+	OptionsParser parser;
+	parser.addOption(OptCamera, OptionString,
+			 "Specify which camera to operate on", "camera",
+			 ArgumentRequired, "camera");
+	parser.addOption(OptHelp, OptionNone, "Display this help message",
+			 "help");
+
+	OptionsParser::Options options = parser.parse(argc, argv);
+	if (options.isSet(OptHelp))
+		parser.usage();
+
+	return options;
+}
+
+int main(int argc, char **argv)
+{
+	QApplication app(argc, argv);
+	int ret;
+
+	OptionsParser::Options options = parseOptions(argc, argv);
+	if (!options.valid())
+		return EXIT_FAILURE;
+	if (options.isSet(OptHelp))
+		return 0;
+
+	struct sigaction sa = {};
+	sa.sa_handler = &signalHandler;
+	sigaction(SIGINT, &sa, nullptr);
+
+	std::unique_ptr<EventDispatcher> dispatcher(new QtEventDispatcher());
+	CameraManager *cm = CameraManager::instance();
+	cm->setEventDispatcher(std::move(dispatcher));
+
+	ret = cm->start();
+	if (ret) {
+		std::cout << "Failed to start camera manager: "
+			  << strerror(-ret) << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	MainWindow *mainWindow = new MainWindow(options);
+	mainWindow->show();
+	ret = app.exec();
+	delete mainWindow;
+
+	cm->stop();
+	return ret;
+}
diff --git a/src/qcam/main_window.cpp b/src/qcam/main_window.cpp
new file mode 100644
index 000000000000..d39dead76b56
--- /dev/null
+++ b/src/qcam/main_window.cpp
@@ -0,0 +1,223 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * main_window.cpp - qcam - Main application window
+ */
+
+#include <iomanip>
+#include <iostream>
+#include <string>
+
+#include <QCoreApplication>
+#include <QInputDialog>
+#include <QTimer>
+
+#include <libcamera/camera_manager.h>
+
+#include "main_window.h"
+#include "viewfinder.h"
+
+using namespace libcamera;
+
+MainWindow::MainWindow(const OptionsParser::Options &options)
+	: options_(options), isCapturing_(false)
+{
+	int ret;
+
+	viewfinder_ = new ViewFinder(this);
+	setCentralWidget(viewfinder_);
+	viewfinder_->setFixedSize(500, 500);
+	adjustSize();
+
+	ret = openCamera();
+	if (!ret)
+		ret = startCapture();
+
+	if (ret < 0)
+		QTimer::singleShot(0, QCoreApplication::instance(),
+				   &QCoreApplication::quit);
+}
+
+MainWindow::~MainWindow()
+{
+	if (camera_) {
+		stopCapture();
+		camera_->release();
+		camera_.reset();
+	}
+
+	CameraManager::instance()->stop();
+}
+
+int MainWindow::openCamera()
+{
+	CameraManager *cm = CameraManager::instance();
+	std::string cameraName;
+
+	if (!options_.isSet(OptCamera)) {
+		QStringList cameras;
+		bool result;
+
+		for (const std::shared_ptr<Camera> &cam : cm->cameras())
+			cameras.append(QString::fromStdString(cam->name()));
+
+		QString name = QInputDialog::getItem(this, "Select Camera",
+						     "Camera:", cameras, 0,
+						     false, &result);
+		if (!result)
+			return -EINVAL;
+
+		cameraName = name.toStdString();
+	} else {
+		cameraName = static_cast<std::string>(options_[OptCamera]);
+	}
+
+	camera_ = cm->get(cameraName);
+	if (!camera_) {
+		std::cout << "Camera " << cameraName << " not found"
+			  << std::endl;
+		return -ENODEV;
+	}
+
+	if (camera_->acquire()) {
+		std::cout << "Failed to acquire camera" << std::endl;
+		camera_.reset();
+		return -EBUSY;
+	}
+
+	std::cout << "Using camera " << camera_->name() << std::endl;
+
+	camera_->requestCompleted.connect(this, &MainWindow::requestComplete);
+
+	return 0;
+}
+
+int MainWindow::startCapture()
+{
+	int ret;
+
+	Stream *stream = *camera_->streams().begin();
+	std::set<Stream *> streams{ stream };
+	config_ = camera_->streamConfiguration(streams);
+	ret = camera_->configureStreams(config_);
+	if (ret < 0) {
+		std::cout << "Failed to configure camera" << std::endl;
+		return ret;
+	}
+
+	const StreamConfiguration &sconf = config_[stream];
+	viewfinder_->setFormat(sconf.pixelFormat, sconf.width, sconf.height);
+	adjustSize();
+
+	ret = camera_->allocateBuffers();
+	if (ret) {
+		std::cerr << "Failed to allocate buffers"
+			  << std::endl;
+		return ret;
+	}
+
+	BufferPool &pool = stream->bufferPool();
+	std::vector<Request *> requests;
+
+	for (Buffer &buffer : pool.buffers()) {
+		Request *request = camera_->createRequest();
+		if (!request) {
+			std::cerr << "Can't create request" << std::endl;
+			ret = -ENOMEM;
+			goto error;
+		}
+
+		std::map<Stream *, Buffer *> map;
+		map[stream] = &buffer;
+		ret = request->setBuffers(map);
+		if (ret < 0) {
+			std::cerr << "Can't set buffers for request" << std::endl;
+			goto error;
+		}
+
+		requests.push_back(request);
+	}
+
+	ret = camera_->start();
+	if (ret) {
+		std::cout << "Failed to start capture" << std::endl;
+		goto error;
+	}
+
+	for (Request *request : requests) {
+		ret = camera_->queueRequest(request);
+		if (ret < 0) {
+			std::cerr << "Can't queue request" << std::endl;
+			goto error;
+		}
+	}
+
+	isCapturing_ = true;
+	return 0;
+
+error:
+	for (Request *request : requests)
+		delete request;
+
+	camera_->freeBuffers();
+	return ret;
+}
+
+void MainWindow::stopCapture()
+{
+	if (!isCapturing_)
+		return;
+
+	int ret = camera_->stop();
+	if (ret)
+		std::cout << "Failed to stop capture" << std::endl;
+
+	camera_->freeBuffers();
+	isCapturing_ = false;
+}
+
+void MainWindow::requestComplete(Request *request,
+				 const std::map<Stream *, Buffer *> &buffers)
+{
+	static uint64_t last = 0;
+
+	if (request->status() == Request::RequestCancelled)
+		return;
+
+	Buffer *buffer = buffers.begin()->second;
+
+	double fps = buffer->timestamp() - last;
+	fps = last && fps ? 1000000000.0 / fps : 0.0;
+	last = buffer->timestamp();
+
+	std::cout << "seq: " << std::setw(6) << std::setfill('0') << buffer->sequence()
+		  << " buf: " << buffer->index()
+		  << " bytesused: " << buffer->bytesused()
+		  << " timestamp: " << buffer->timestamp()
+		  << " fps: " << std::fixed << std::setprecision(2) << fps
+		  << std::endl;
+
+	display(buffer);
+
+	request = camera_->createRequest();
+	if (!request) {
+		std::cerr << "Can't create request" << std::endl;
+		return;
+	}
+
+	request->setBuffers(buffers);
+	camera_->queueRequest(request);
+}
+
+int MainWindow::display(Buffer *buffer)
+{
+	if (buffer->planes().size() != 1)
+		return -EINVAL;
+
+	Plane &plane = buffer->planes().front();
+	unsigned char *raw = static_cast<unsigned char *>(plane.mem());
+	viewfinder_->display(raw);
+
+	return 0;
+}
diff --git a/src/qcam/main_window.h b/src/qcam/main_window.h
new file mode 100644
index 000000000000..5e27a8fd6b4e
--- /dev/null
+++ b/src/qcam/main_window.h
@@ -0,0 +1,54 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * main_window.h - qcam - Main application window
+ */
+#ifndef __QCAM_MAIN_WINDOW_H__
+#define __QCAM_MAIN_WINDOW_H__
+
+#include <map>
+
+#include <QMainWindow>
+
+#include <libcamera/camera.h>
+#include <libcamera/stream.h>
+
+#include "../cam/options.h"
+
+using namespace libcamera;
+
+class ViewFinder;
+
+enum {
+	OptCamera = 'c',
+	OptHelp = 'h',
+};
+
+class MainWindow : public QMainWindow
+{
+public:
+	MainWindow(const OptionsParser::Options &options);
+	~MainWindow();
+
+private:
+	int openCamera();
+
+	int startCapture();
+	int configureStreams(Camera *camera, std::set<Stream *> &streams);
+	void stopCapture();
+
+	void requestComplete(Request *request,
+			     const std::map<Stream *, Buffer *> &buffers);
+	int display(Buffer *buffer);
+
+	const OptionsParser::Options &options_;
+
+	std::shared_ptr<Camera> camera_;
+	bool isCapturing_;
+	std::map<Stream *, StreamConfiguration> config_;
+
+	ViewFinder *viewfinder_;
+};
+
+#endif /* __QCAM_MAIN_WINDOW__ */
diff --git a/src/qcam/meson.build b/src/qcam/meson.build
new file mode 100644
index 000000000000..8a71cda3dfe5
--- /dev/null
+++ b/src/qcam/meson.build
@@ -0,0 +1,19 @@ 
+qcam_sources = files([
+    'format_converter.cpp',
+    'main.cpp',
+    'main_window.cpp',
+    '../cam/options.cpp',
+    'qt_event_dispatcher.cpp',
+    'viewfinder.cpp',
+])
+
+import('qt5')
+qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Widgets'], required : false)
+
+if qt5_dep.found()
+    qcam  = executable('qcam', qcam_sources,
+                       link_with : libcamera,
+                       include_directories : libcamera_includes,
+                       dependencies : qt5_dep,
+                       cpp_args : '-DQT_NO_KEYWORDS')
+endif
diff --git a/src/qcam/qt_event_dispatcher.cpp b/src/qcam/qt_event_dispatcher.cpp
new file mode 100644
index 000000000000..5ba451bf88ce
--- /dev/null
+++ b/src/qcam/qt_event_dispatcher.cpp
@@ -0,0 +1,145 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * qt_event_dispatcher.cpp - qcam - Qt-based event dispatcher
+ */
+
+#include <iostream>
+
+#include <QAbstractEventDispatcher>
+#include <QCoreApplication>
+#include <QSocketNotifier>
+#include <QTimerEvent>
+
+#include <libcamera/event_notifier.h>
+#include <libcamera/timer.h>
+
+#include "qt_event_dispatcher.h"
+
+using namespace libcamera;
+
+QtEventDispatcher::QtEventDispatcher()
+{
+}
+
+QtEventDispatcher::~QtEventDispatcher()
+{
+	for (auto &it : notifiers_) {
+		NotifierSet &set = it.second;
+		delete set.read.qnotifier;
+		delete set.write.qnotifier;
+		delete set.exception.qnotifier;
+	}
+}
+
+void QtEventDispatcher::registerEventNotifier(EventNotifier *notifier)
+{
+	NotifierSet &set = notifiers_[notifier->fd()];
+	QSocketNotifier::Type qtype;
+	void (QtEventDispatcher::*method)(int);
+	NotifierPair *pair;
+
+	switch (notifier->type()) {
+	case EventNotifier::Read:
+	default:
+		qtype = QSocketNotifier::Read;
+		method = &QtEventDispatcher::readNotifierActivated;
+		pair = &set.read;
+		break;
+
+	case EventNotifier::Write:
+		qtype = QSocketNotifier::Write;
+		method = &QtEventDispatcher::writeNotifierActivated;
+		pair = &set.write;
+		break;
+
+	case EventNotifier::Exception:
+		qtype = QSocketNotifier::Exception;
+		method = &QtEventDispatcher::exceptionNotifierActivated;
+		pair = &set.exception;
+		break;
+	}
+
+	QSocketNotifier *qnotifier = new QSocketNotifier(notifier->fd(), qtype);
+	connect(qnotifier, &QSocketNotifier::activated, this, method);
+	pair->notifier = notifier;
+	pair->qnotifier = qnotifier;
+}
+
+void QtEventDispatcher::unregisterEventNotifier(EventNotifier *notifier)
+{
+	NotifierSet &set = notifiers_[notifier->fd()];
+	NotifierPair *pair;
+
+	switch (notifier->type()) {
+	case EventNotifier::Read:
+	default:
+		pair = &set.read;
+		break;
+
+	case EventNotifier::Write:
+		pair = &set.write;
+		break;
+
+	case EventNotifier::Exception:
+		pair = &set.exception;
+		break;
+	}
+
+	delete pair->qnotifier;
+	pair->qnotifier = nullptr;
+	pair->notifier = nullptr;
+}
+
+void QtEventDispatcher::readNotifierActivated(int socket)
+{
+	EventNotifier *notifier = notifiers_[socket].read.notifier;
+	notifier->activated.emit(notifier);
+}
+
+void QtEventDispatcher::writeNotifierActivated(int socket)
+{
+	EventNotifier *notifier = notifiers_[socket].write.notifier;
+	notifier->activated.emit(notifier);
+}
+
+void QtEventDispatcher::exceptionNotifierActivated(int socket)
+{
+	EventNotifier *notifier = notifiers_[socket].exception.notifier;
+	notifier->activated.emit(notifier);
+}
+
+void QtEventDispatcher::registerTimer(Timer *timer)
+{
+	int timerId = startTimer(timer->interval());
+	timers_[timerId] = timer;
+	timerIds_[timer] = timerId;
+}
+
+void QtEventDispatcher::unregisterTimer(Timer *timer)
+{
+	auto it = timerIds_.find(timer);
+	timers_.erase(it->second);
+	killTimer(it->second);
+	timerIds_.erase(it);
+}
+
+void QtEventDispatcher::timerEvent(QTimerEvent *event)
+{
+	auto it = timers_.find(event->timerId());
+	timerIds_.erase(it->second);
+	killTimer(it->first);
+	timers_.erase(it);
+}
+
+void QtEventDispatcher::processEvents()
+{
+	std::cout << "QtEventDispatcher::processEvents() should not be called"
+		  << std::endl;
+}
+
+void QtEventDispatcher::interrupt()
+{
+	QCoreApplication::eventDispatcher()->interrupt();
+}
diff --git a/src/qcam/qt_event_dispatcher.h b/src/qcam/qt_event_dispatcher.h
new file mode 100644
index 000000000000..b0f123e52d06
--- /dev/null
+++ b/src/qcam/qt_event_dispatcher.h
@@ -0,0 +1,62 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * qt_event_dispatcher.h - qcam - Qt-based event dispatcher
+ */
+#ifndef __QCAM_QT_EVENT_DISPATCHER_H__
+#define __QCAM_QT_EVENT_DISPATCHER_H__
+
+#include <map>
+
+#include <libcamera/event_dispatcher.h>
+
+using namespace libcamera;
+
+class QSocketNotifier;
+
+class QtEventDispatcher final : public EventDispatcher, public QObject
+{
+public:
+	QtEventDispatcher();
+	~QtEventDispatcher();
+
+	void registerEventNotifier(EventNotifier *notifier);
+	void unregisterEventNotifier(EventNotifier *notifier);
+
+	void registerTimer(Timer *timer);
+	void unregisterTimer(Timer *timer);
+
+	void processEvents();
+
+	void interrupt();
+
+protected:
+	void timerEvent(QTimerEvent *event);
+
+private:
+	void readNotifierActivated(int socket);
+	void writeNotifierActivated(int socket);
+	void exceptionNotifierActivated(int socket);
+
+	struct NotifierPair {
+		NotifierPair()
+			: notifier(nullptr), qnotifier(nullptr)
+		{
+		}
+		EventNotifier *notifier;
+		QSocketNotifier *qnotifier;
+	};
+
+	struct NotifierSet {
+		NotifierPair read;
+		NotifierPair write;
+		NotifierPair exception;
+	};
+
+	std::map<int, NotifierSet> notifiers_;
+	std::map<int, Timer *> timers_;
+	std::map<Timer *, int> timerIds_;
+};
+
+#endif /* __QCAM_QT_EVENT_DISPATCHER_H__ */
diff --git a/src/qcam/viewfinder.cpp b/src/qcam/viewfinder.cpp
new file mode 100644
index 000000000000..5db55ebbf098
--- /dev/null
+++ b/src/qcam/viewfinder.cpp
@@ -0,0 +1,38 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * viewfinder.cpp - qcam - Viewfinder
+ */
+
+#include <QImage>
+#include <QPixmap>
+
+#include "format_converter.h"
+#include "viewfinder.h"
+
+ViewFinder::ViewFinder(QWidget *parent)
+	: QLabel(parent), format_(0), width_(0), height_(0), image_(nullptr)
+{
+}
+
+void ViewFinder::display(unsigned char *raw)
+{
+	buffer_to_rgb32(raw, format_, width_, height_, image_->bits());
+
+	QPixmap pixmap = QPixmap::fromImage(*image_);
+	setPixmap(pixmap);
+}
+
+void ViewFinder::setFormat(unsigned int format, unsigned int width,
+			   unsigned int height)
+{
+	format_ = format;
+	width_ = width;
+	height_ = height;
+
+	setFixedSize(width, height);
+
+	delete image_;
+	image_ = new QImage(width, height, QImage::Format_RGB32);
+}
diff --git a/src/qcam/viewfinder.h b/src/qcam/viewfinder.h
new file mode 100644
index 000000000000..ecae290a5043
--- /dev/null
+++ b/src/qcam/viewfinder.h
@@ -0,0 +1,31 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * viewfinder.h - qcam - Viewfinder
+ */
+#ifndef __QCAM_VIEWFINDER_H__
+#define __QCAM_VIEWFINDER_H__
+
+#include <QLabel>
+
+class QImage;
+
+class ViewFinder : public QLabel
+{
+public:
+	ViewFinder(QWidget *parent);
+
+	void setFormat(unsigned int format, unsigned int width,
+		       unsigned int height);
+	void display(unsigned char *rgb);
+
+private:
+	unsigned int format_;
+	unsigned int width_;
+	unsigned int height_;
+
+	QImage *image_;
+};
+
+#endif /* __QCAM_VIEWFINDER__ */