| /* 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 <iomanip> |
| #include <string> |
| #include <sys/mman.h> |
| |
| #include <QComboBox> |
| #include <QCoreApplication> |
| #include <QFileDialog> |
| #include <QImage> |
| #include <QImageWriter> |
| #include <QInputDialog> |
| #include <QMutexLocker> |
| #include <QStandardPaths> |
| #include <QTimer> |
| #include <QToolBar> |
| #include <QToolButton> |
| #include <QtDebug> |
| |
| #include <libcamera/camera_manager.h> |
| #include <libcamera/version.h> |
| |
| #include "dng_writer.h" |
| |
| using namespace libcamera; |
| |
| /** |
| * \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); |
| } |
| }; |
| |
| 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())); |
| |
| viewfinder_ = new ViewFinder(this); |
| connect(viewfinder_, &ViewFinder::renderComplete, |
| this, &MainWindow::queueRequest); |
| setCentralWidget(viewfinder_); |
| adjustSize(); |
| |
| /* 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; |
| } |
| |
| 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->name())); |
| |
| 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->name().c_str(); |
| return; |
| } |
| |
| qInfo() << "Switching to camera" << cam->name().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]->name(); |
| |
| /* Present a dialog box to pick a camera. */ |
| 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 std::string(); |
| |
| return name.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]); |
| std::vector<Request *> requests; |
| 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::StillCaptureRaw) { |
| qWarning() << "Only viewfinder + raw supported for dual streams"; |
| return -EINVAL; |
| } |
| break; |
| default: |
| if (roles.size() != 1) { |
| qWarning() << "Unsuported 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)); |
| 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. */ |
| const FrameBuffer::Plane &plane = buffer->planes().front(); |
| void *memory = mmap(NULL, plane.length, PROT_READ, MAP_SHARED, |
| plane.fd.fd(), 0); |
| mappedBuffers_[buffer.get()] = { memory, plane.length }; |
| |
| /* 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(); |
| |
| 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(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 (Request *request : requests) { |
| ret = camera_->queueRequest(request); |
| if (ret < 0) { |
| qWarning() << "Can't queue request"; |
| goto error_disconnect; |
| } |
| } |
| |
| isCapturing_ = true; |
| |
| return 0; |
| |
| error_disconnect: |
| camera_->requestCompleted.disconnect(this, &MainWindow::requestComplete); |
| camera_->stop(); |
| |
| error: |
| for (Request *request : requests) |
| delete request; |
| |
| for (auto &iter : mappedBuffers_) { |
| const MappedBuffer &buffer = iter.second; |
| munmap(buffer.memory, buffer.size); |
| } |
| 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, &MainWindow::requestComplete); |
| |
| for (auto &iter : mappedBuffers_) { |
| const MappedBuffer &buffer = iter.second; |
| munmap(buffer.memory, buffer.size); |
| } |
| mappedBuffers_.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_); |
| } |
| |
| /* ----------------------------------------------------------------------------- |
| * 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, 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()) { |
| const MappedBuffer &mapped = mappedBuffers_[buffer]; |
| DNGWriter::write(filename.toStdString().c_str(), camera_.get(), |
| rawStream_->configuration(), metadata, buffer, |
| mapped.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->buffers(), request->metadata() }); |
| } |
| |
| 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. |
| */ |
| CaptureRequest request; |
| |
| { |
| QMutexLocker locker(&mutex_); |
| if (doneQueue_.isEmpty()) |
| return; |
| |
| request = doneQueue_.dequeue(); |
| } |
| |
| /* Process buffers. */ |
| if (request.buffers_.count(vfStream_)) |
| processViewfinder(request.buffers_[vfStream_]); |
| |
| if (request.buffers_.count(rawStream_)) |
| processRaw(request.buffers_[rawStream_], request.metadata_); |
| } |
| |
| 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; |
| |
| qInfo().noquote() |
| << QString("seq: %1").arg(metadata.sequence, 6, 10, QLatin1Char('0')) |
| << "bytesused:" << metadata.planes[0].bytesused |
| << "timestamp:" << metadata.timestamp |
| << "fps:" << fixed << qSetRealNumberPrecision(2) << fps; |
| |
| /* Render the frame on the viewfinder. */ |
| viewfinder_->render(buffer, &mappedBuffers_[buffer]); |
| } |
| |
| void MainWindow::queueRequest(FrameBuffer *buffer) |
| { |
| Request *request = camera_->createRequest(); |
| if (!request) { |
| qWarning() << "Can't create request"; |
| return; |
| } |
| |
| request->addBuffer(vfStream_, buffer); |
| |
| if (captureRaw_) { |
| FrameBuffer *buffer = nullptr; |
| |
| { |
| QMutexLocker locker(&mutex_); |
| if (!freeBuffers_[rawStream_].isEmpty()) |
| buffer = freeBuffers_[rawStream_].dequeue(); |
| } |
| |
| if (buffer) { |
| request->addBuffer(rawStream_, buffer); |
| captureRaw_ = false; |
| } else { |
| qWarning() << "No free buffer available for RAW capture"; |
| } |
| } |
| |
| camera_->queueRequest(request); |
| } |