blob: dd0e51f55b70d6c6fc832d2e8917c4f75ac46acc [file] [log] [blame]
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2019, Google Inc.
*
* main_window.cpp - qcam - Main application window
*/
#include "main_window.h"
#include <assert.h>
#include <iomanip>
#include <string>
#include <libcamera/camera_manager.h>
#include <libcamera/version.h>
#include <QComboBox>
#include <QCoreApplication>
#include <QFileDialog>
#include <QImage>
#include <QImageWriter>
#include <QInputDialog>
#include <QMutexLocker>
#include <QStandardPaths>
#include <QStringList>
#include <QTimer>
#include <QToolBar>
#include <QToolButton>
#include <QtDebug>
#include "../cam/image.h"
#include "dng_writer.h"
#ifndef QT_NO_OPENGL
#include "viewfinder_gl.h"
#endif
#include "viewfinder_qt.h"
using namespace libcamera;
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
/*
* Qt::fixed was introduced in v5.14, and ::fixed deprecated in v5.15. Allow
* usage of Qt::fixed unconditionally.
*/
namespace Qt {
constexpr auto fixed = ::fixed;
} /* namespace Qt */
#endif
/**
* \brief Custom QEvent to signal capture completion
*/
class CaptureEvent : public QEvent
{
public:
CaptureEvent()
: QEvent(type())
{
}
static Type type()
{
static int type = QEvent::registerEventType();
return static_cast<Type>(type);
}
};
/**
* \brief Custom QEvent to signal hotplug or unplug
*/
class HotplugEvent : public QEvent
{
public:
enum PlugEvent {
HotPlug,
HotUnplug
};
HotplugEvent(std::shared_ptr<Camera> camera, PlugEvent event)
: QEvent(type()), camera_(std::move(camera)), plugEvent_(event)
{
}
static Type type()
{
static int type = QEvent::registerEventType();
return static_cast<Type>(type);
}
PlugEvent hotplugEvent() const { return plugEvent_; }
Camera *camera() const { return camera_.get(); }
private:
std::shared_ptr<Camera> camera_;
PlugEvent plugEvent_;
};
MainWindow::MainWindow(CameraManager *cm, const OptionsParser::Options &options)
: saveRaw_(nullptr), options_(options), cm_(cm), allocator_(nullptr),
isCapturing_(false), captureRaw_(false)
{
int ret;
/*
* Initialize the UI: Create the toolbar, set the window title and
* create the viewfinder widget.
*/
createToolbars();
title_ = "QCam " + QString::fromStdString(CameraManager::version());
setWindowTitle(title_);
connect(&titleTimer_, SIGNAL(timeout()), this, SLOT(updateTitle()));
/* Renderer type Qt or GLES, select Qt by default. */
std::string renderType = "qt";
if (options_.isSet(OptRenderer))
renderType = options_[OptRenderer].toString();
if (renderType == "qt") {
ViewFinderQt *viewfinder = new ViewFinderQt(this);
connect(viewfinder, &ViewFinderQt::renderComplete,
this, &MainWindow::queueRequest);
viewfinder_ = viewfinder;
setCentralWidget(viewfinder);
#ifndef QT_NO_OPENGL
} else if (renderType == "gles") {
ViewFinderGL *viewfinder = new ViewFinderGL(this);
connect(viewfinder, &ViewFinderGL::renderComplete,
this, &MainWindow::queueRequest);
viewfinder_ = viewfinder;
setCentralWidget(viewfinder);
#endif
} else {
qWarning() << "Invalid render type"
<< QString::fromStdString(renderType);
quit();
return;
}
adjustSize();
/* Hotplug/unplug support */
cm_->cameraAdded.connect(this, &MainWindow::addCamera);
cm_->cameraRemoved.connect(this, &MainWindow::removeCamera);
/* Open the camera and start capture. */
ret = openCamera();
if (ret < 0) {
quit();
return;
}
startStopAction_->setChecked(true);
}
MainWindow::~MainWindow()
{
if (camera_) {
stopCapture();
camera_->release();
camera_.reset();
}
}
bool MainWindow::event(QEvent *e)
{
if (e->type() == CaptureEvent::type()) {
processCapture();
return true;
} else if (e->type() == HotplugEvent::type()) {
processHotplug(static_cast<HotplugEvent *>(e));
return true;
}
return QMainWindow::event(e);
}
int MainWindow::createToolbars()
{
QAction *action;
toolbar_ = addToolBar("Main");
/* Disable right click context menu. */
toolbar_->setContextMenuPolicy(Qt::PreventContextMenu);
/* Quit action. */
action = toolbar_->addAction(QIcon::fromTheme("application-exit",
QIcon(":x-circle.svg")),
"Quit");
action->setShortcut(Qt::CTRL | Qt::Key_Q);
connect(action, &QAction::triggered, this, &MainWindow::quit);
/* Camera selector. */
cameraCombo_ = new QComboBox();
connect(cameraCombo_, QOverload<int>::of(&QComboBox::activated),
this, &MainWindow::switchCamera);
for (const std::shared_ptr<Camera> &cam : cm_->cameras())
cameraCombo_->addItem(QString::fromStdString(cam->id()));
toolbar_->addWidget(cameraCombo_);
toolbar_->addSeparator();
/* Start/Stop action. */
iconPlay_ = QIcon::fromTheme("media-playback-start",
QIcon(":play-circle.svg"));
iconStop_ = QIcon::fromTheme("media-playback-stop",
QIcon(":stop-circle.svg"));
action = toolbar_->addAction(iconPlay_, "Start Capture");
action->setCheckable(true);
action->setShortcut(Qt::Key_Space);
connect(action, &QAction::toggled, this, &MainWindow::toggleCapture);
startStopAction_ = action;
/* Save As... action. */
action = toolbar_->addAction(QIcon::fromTheme("document-save-as",
QIcon(":save.svg")),
"Save As...");
action->setShortcut(QKeySequence::SaveAs);
connect(action, &QAction::triggered, this, &MainWindow::saveImageAs);
#ifdef HAVE_DNG
/* Save Raw action. */
action = toolbar_->addAction(QIcon::fromTheme("camera-photo",
QIcon(":aperture.svg")),
"Save Raw");
action->setEnabled(false);
connect(action, &QAction::triggered, this, &MainWindow::captureRaw);
saveRaw_ = action;
#endif
return 0;
}
void MainWindow::quit()
{
QTimer::singleShot(0, QCoreApplication::instance(),
&QCoreApplication::quit);
}
void MainWindow::updateTitle()
{
/* Calculate the average frame rate over the last period. */
unsigned int duration = frameRateInterval_.elapsed();
unsigned int frames = framesCaptured_ - previousFrames_;
double fps = frames * 1000.0 / duration;
/* Restart counters. */
frameRateInterval_.start();
previousFrames_ = framesCaptured_;
setWindowTitle(title_ + " : " + QString::number(fps, 'f', 2) + " fps");
}
/* -----------------------------------------------------------------------------
* Camera Selection
*/
void MainWindow::switchCamera(int index)
{
/* Get and acquire the new camera. */
const auto &cameras = cm_->cameras();
if (static_cast<unsigned int>(index) >= cameras.size())
return;
const std::shared_ptr<Camera> &cam = cameras[index];
if (cam->acquire()) {
qInfo() << "Failed to acquire camera" << cam->id().c_str();
return;
}
qInfo() << "Switching to camera" << cam->id().c_str();
/*
* Stop the capture session, release the current camera, replace it with
* the new camera and start a new capture session.
*/
startStopAction_->setChecked(false);
camera_->release();
camera_ = cam;
startStopAction_->setChecked(true);
}
std::string MainWindow::chooseCamera()
{
QStringList cameras;
bool result;
/* If only one camera is available, use it automatically. */
if (cm_->cameras().size() == 1)
return cm_->cameras()[0]->id();
/* Present a dialog box to pick a camera. */
for (const std::shared_ptr<Camera> &cam : cm_->cameras())
cameras.append(QString::fromStdString(cam->id()));
QString id = QInputDialog::getItem(this, "Select Camera",
"Camera:", cameras, 0,
false, &result);
if (!result)
return std::string();
return id.toStdString();
}
int MainWindow::openCamera()
{
std::string cameraName;
/*
* Use the camera specified on the command line, if any, or display the
* camera selection dialog box otherwise.
*/
if (options_.isSet(OptCamera))
cameraName = static_cast<std::string>(options_[OptCamera]);
else
cameraName = chooseCamera();
if (cameraName == "")
return -EINVAL;
/* Get and acquire the camera. */
camera_ = cm_->get(cameraName);
if (!camera_) {
qInfo() << "Camera" << cameraName.c_str() << "not found";
return -ENODEV;
}
if (camera_->acquire()) {
qInfo() << "Failed to acquire camera";
camera_.reset();
return -EBUSY;
}
/* Set the combo-box entry with the currently selected Camera. */
cameraCombo_->setCurrentText(QString::fromStdString(cameraName));
return 0;
}
/* -----------------------------------------------------------------------------
* Capture Start & Stop
*/
void MainWindow::toggleCapture(bool start)
{
if (start) {
startCapture();
startStopAction_->setIcon(iconStop_);
startStopAction_->setText("Stop Capture");
} else {
stopCapture();
startStopAction_->setIcon(iconPlay_);
startStopAction_->setText("Start Capture");
}
}
/**
* \brief Start capture with the current camera
*
* This function shall not be called directly, use toggleCapture() instead.
*/
int MainWindow::startCapture()
{
StreamRoles roles = StreamKeyValueParser::roles(options_[OptStream]);
int ret;
/* Verify roles are supported. */
switch (roles.size()) {
case 1:
if (roles[0] != StreamRole::Viewfinder) {
qWarning() << "Only viewfinder supported for single stream";
return -EINVAL;
}
break;
case 2:
if (roles[0] != StreamRole::Viewfinder ||
roles[1] != StreamRole::Raw) {
qWarning() << "Only viewfinder + raw supported for dual streams";
return -EINVAL;
}
break;
default:
if (roles.size() != 1) {
qWarning() << "Unsupported stream configuration";
return -EINVAL;
}
break;
}
/* Configure the camera. */
config_ = camera_->generateConfiguration(roles);
if (!config_) {
qWarning() << "Failed to generate configuration from roles";
return -EINVAL;
}
StreamConfiguration &vfConfig = config_->at(0);
/* Use a format supported by the viewfinder if available. */
std::vector<PixelFormat> formats = vfConfig.formats().pixelformats();
for (const PixelFormat &format : viewfinder_->nativeFormats()) {
auto match = std::find_if(formats.begin(), formats.end(),
[&](const PixelFormat &f) {
return f == format;
});
if (match != formats.end()) {
vfConfig.pixelFormat = format;
break;
}
}
/* Allow user to override configuration. */
if (StreamKeyValueParser::updateConfiguration(config_.get(),
options_[OptStream])) {
qWarning() << "Failed to update configuration";
return -EINVAL;
}
CameraConfiguration::Status validation = config_->validate();
if (validation == CameraConfiguration::Invalid) {
qWarning() << "Failed to create valid camera configuration";
return -EINVAL;
}
if (validation == CameraConfiguration::Adjusted)
qInfo() << "Stream configuration adjusted to "
<< vfConfig.toString().c_str();
ret = camera_->configure(config_.get());
if (ret < 0) {
qInfo() << "Failed to configure camera";
return ret;
}
/* Store stream allocation. */
vfStream_ = config_->at(0).stream();
if (config_->size() == 2)
rawStream_ = config_->at(1).stream();
else
rawStream_ = nullptr;
/* Configure the viewfinder. */
ret = viewfinder_->setFormat(vfConfig.pixelFormat,
QSize(vfConfig.size.width, vfConfig.size.height),
vfConfig.stride);
if (ret < 0) {
qInfo() << "Failed to set viewfinder format";
return ret;
}
adjustSize();
/* Configure the raw capture button. */
if (saveRaw_)
saveRaw_->setEnabled(config_->size() == 2);
/* Allocate and map buffers. */
allocator_ = new FrameBufferAllocator(camera_);
for (StreamConfiguration &config : *config_) {
Stream *stream = config.stream();
ret = allocator_->allocate(stream);
if (ret < 0) {
qWarning() << "Failed to allocate capture buffers";
goto error;
}
for (const std::unique_ptr<FrameBuffer> &buffer : allocator_->buffers(stream)) {
/* Map memory buffers and cache the mappings. */
std::unique_ptr<Image> image =
Image::fromFrameBuffer(buffer.get(), Image::MapMode::ReadOnly);
assert(image != nullptr);
mappedBuffers_[buffer.get()] = std::move(image);
/* Store buffers on the free list. */
freeBuffers_[stream].enqueue(buffer.get());
}
}
/* Create requests and fill them with buffers from the viewfinder. */
while (!freeBuffers_[vfStream_].isEmpty()) {
FrameBuffer *buffer = freeBuffers_[vfStream_].dequeue();
std::unique_ptr<Request> request = camera_->createRequest();
if (!request) {
qWarning() << "Can't create request";
ret = -ENOMEM;
goto error;
}
ret = request->addBuffer(vfStream_, buffer);
if (ret < 0) {
qWarning() << "Can't set buffer for request";
goto error;
}
requests_.push_back(std::move(request));
}
/* Start the title timer and the camera. */
titleTimer_.start(2000);
frameRateInterval_.start();
previousFrames_ = 0;
framesCaptured_ = 0;
lastBufferTime_ = 0;
ret = camera_->start();
if (ret) {
qInfo() << "Failed to start capture";
goto error;
}
camera_->requestCompleted.connect(this, &MainWindow::requestComplete);
/* Queue all requests. */
for (std::unique_ptr<Request> &request : requests_) {
ret = camera_->queueRequest(request.get());
if (ret < 0) {
qWarning() << "Can't queue request";
goto error_disconnect;
}
}
isCapturing_ = true;
return 0;
error_disconnect:
camera_->requestCompleted.disconnect(this);
camera_->stop();
error:
requests_.clear();
mappedBuffers_.clear();
freeBuffers_.clear();
delete allocator_;
allocator_ = nullptr;
return ret;
}
/**
* \brief Stop ongoing capture
*
* This function may be called directly when tearing down the MainWindow. Use
* toggleCapture() instead in all other cases.
*/
void MainWindow::stopCapture()
{
if (!isCapturing_)
return;
viewfinder_->stop();
if (saveRaw_)
saveRaw_->setEnabled(false);
captureRaw_ = false;
int ret = camera_->stop();
if (ret)
qInfo() << "Failed to stop capture";
camera_->requestCompleted.disconnect(this);
mappedBuffers_.clear();
requests_.clear();
freeQueue_.clear();
delete allocator_;
isCapturing_ = false;
config_.reset();
/*
* A CaptureEvent may have been posted before we stopped the camera,
* but not processed yet. Clear the queue of done buffers to avoid
* racing with the event handler.
*/
freeBuffers_.clear();
doneQueue_.clear();
titleTimer_.stop();
setWindowTitle(title_);
}
/* -----------------------------------------------------------------------------
* Camera hotplugging support
*/
void MainWindow::processHotplug(HotplugEvent *e)
{
Camera *camera = e->camera();
HotplugEvent::PlugEvent event = e->hotplugEvent();
if (event == HotplugEvent::HotPlug) {
cameraCombo_->addItem(QString::fromStdString(camera->id()));
} else if (event == HotplugEvent::HotUnplug) {
/* Check if the currently-streaming camera is removed. */
if (camera == camera_.get()) {
toggleCapture(false);
camera_->release();
camera_.reset();
cameraCombo_->setCurrentIndex(0);
}
int camIndex = cameraCombo_->findText(QString::fromStdString(camera->id()));
cameraCombo_->removeItem(camIndex);
}
}
void MainWindow::addCamera(std::shared_ptr<Camera> camera)
{
qInfo() << "Adding new camera:" << camera->id().c_str();
QCoreApplication::postEvent(this,
new HotplugEvent(std::move(camera),
HotplugEvent::HotPlug));
}
void MainWindow::removeCamera(std::shared_ptr<Camera> camera)
{
qInfo() << "Removing camera:" << camera->id().c_str();
QCoreApplication::postEvent(this,
new HotplugEvent(std::move(camera),
HotplugEvent::HotUnplug));
}
/* -----------------------------------------------------------------------------
* Image Save
*/
void MainWindow::saveImageAs()
{
QImage image = viewfinder_->getCurrentImage();
QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
QString filename = QFileDialog::getSaveFileName(this, "Save Image", defaultPath,
"Image Files (*.png *.jpg *.jpeg)");
if (filename.isEmpty())
return;
QImageWriter writer(filename);
writer.setQuality(95);
writer.write(image);
}
void MainWindow::captureRaw()
{
captureRaw_ = true;
}
void MainWindow::processRaw(FrameBuffer *buffer,
[[maybe_unused]] const ControlList &metadata)
{
#ifdef HAVE_DNG
QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
QString filename = QFileDialog::getSaveFileName(this, "Save DNG", defaultPath,
"DNG Files (*.dng)");
if (!filename.isEmpty()) {
uint8_t *memory = mappedBuffers_[buffer]->data(0).data();
DNGWriter::write(filename.toStdString().c_str(), camera_.get(),
rawStream_->configuration(), metadata, buffer,
memory);
}
#endif
{
QMutexLocker locker(&mutex_);
freeBuffers_[rawStream_].enqueue(buffer);
}
}
/* -----------------------------------------------------------------------------
* Request Completion Handling
*/
void MainWindow::requestComplete(Request *request)
{
if (request->status() == Request::RequestCancelled)
return;
/*
* We're running in the libcamera thread context, expensive operations
* are not allowed. Add the buffer to the done queue and post a
* CaptureEvent for the application thread to handle.
*/
{
QMutexLocker locker(&mutex_);
doneQueue_.enqueue(request);
}
QCoreApplication::postEvent(this, new CaptureEvent);
}
void MainWindow::processCapture()
{
/*
* Retrieve the next buffer from the done queue. The queue may be empty
* if stopCapture() has been called while a CaptureEvent was posted but
* not processed yet. Return immediately in that case.
*/
Request *request;
{
QMutexLocker locker(&mutex_);
if (doneQueue_.isEmpty())
return;
request = doneQueue_.dequeue();
}
/* Process buffers. */
if (request->buffers().count(vfStream_))
processViewfinder(request->buffers().at(vfStream_));
if (request->buffers().count(rawStream_))
processRaw(request->buffers().at(rawStream_), request->metadata());
request->reuse();
QMutexLocker locker(&mutex_);
freeQueue_.enqueue(request);
}
void MainWindow::processViewfinder(FrameBuffer *buffer)
{
framesCaptured_++;
const FrameMetadata &metadata = buffer->metadata();
double fps = metadata.timestamp - lastBufferTime_;
fps = lastBufferTime_ && fps ? 1000000000.0 / fps : 0.0;
lastBufferTime_ = metadata.timestamp;
QStringList bytesused;
for (const FrameMetadata::Plane &plane : metadata.planes())
bytesused << QString::number(plane.bytesused);
qDebug().noquote()
<< QString("seq: %1").arg(metadata.sequence, 6, 10, QLatin1Char('0'))
<< "bytesused: {" << bytesused.join(", ")
<< "} timestamp:" << metadata.timestamp
<< "fps:" << Qt::fixed << qSetRealNumberPrecision(2) << fps;
/* Render the frame on the viewfinder. */
viewfinder_->render(buffer, mappedBuffers_[buffer].get());
}
void MainWindow::queueRequest(FrameBuffer *buffer)
{
Request *request;
{
QMutexLocker locker(&mutex_);
if (freeQueue_.isEmpty())
return;
request = freeQueue_.dequeue();
}
request->addBuffer(vfStream_, buffer);
if (captureRaw_) {
FrameBuffer *rawBuffer = nullptr;
{
QMutexLocker locker(&mutex_);
if (!freeBuffers_[rawStream_].isEmpty())
rawBuffer = freeBuffers_[rawStream_].dequeue();
}
if (rawBuffer) {
request->addBuffer(rawStream_, rawBuffer);
captureRaw_ = false;
} else {
qWarning() << "No free buffer available for RAW capture";
}
}
camera_->queueRequest(request);
}