From f36c277feed27249ae9d7cb1de327eca0cf71fff Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Tue, 30 Sep 2025 23:12:23 +0200 Subject: [PATCH 01/17] compile gphoto in flatpak --- .gitmodules | 3 +++ io.github.saeugetier.photobooth.json | 31 ++++++++++++++++++++++++++++ shared-modules | 1 + 3 files changed, 35 insertions(+) create mode 100644 .gitmodules create mode 160000 shared-modules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..ea3adf16 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "shared-modules"] + path = shared-modules + url = https://github.com/flathub/shared-modules diff --git a/io.github.saeugetier.photobooth.json b/io.github.saeugetier.photobooth.json index c2545845..23063ac3 100644 --- a/io.github.saeugetier.photobooth.json +++ b/io.github.saeugetier.photobooth.json @@ -131,6 +131,37 @@ } ] }, + { + "name": "libgphoto2", + "builddir": true, + "cleanup": [ + "/doc", + "*.la" + ], + "sources": [ + { + "type": "git", + "url": "https://github.com/gphoto/libgphoto2.git", + "tag": "v2.5.31", + "commit": "ba28af2d22fd4cb7fa76a8ff569ba498e8021db5", + "x-checker-data": { + "type": "anitya", + "project-id": 12558, + "stable-only": true + } + }, + { + "type": "script", + "dest-filename": "autogen.sh", + "commands": [ + "AUTOMAKE=\"automake --foreign\" autoreconf -vfis" + ] + } + ], + "modules": [ + "shared-modules/libusb/libusb.json" + ] + }, { "name": "qtbooth", "buildsystem": "qmake", diff --git a/shared-modules b/shared-modules new file mode 160000 index 00000000..21809f3f --- /dev/null +++ b/shared-modules @@ -0,0 +1 @@ +Subproject commit 21809f3f4ad91b130e754948fc139e2e3410db77 From 1388f789d27d5e367f3a3a9123a5beb6c141fe36 Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Tue, 30 Sep 2025 23:13:32 +0200 Subject: [PATCH 02/17] start implementation of gphoto camera --- qml/content/CameraRenderer.qml | 9 ++++ qtbooth.pro | 3 ++ src/gphotocamera.cpp | 94 ++++++++++++++++++++++++++++++++++ src/gphotocamera.h | 51 ++++++++++++++++++ src/main.cpp | 2 + 5 files changed, 159 insertions(+) create mode 100644 src/gphotocamera.cpp create mode 100644 src/gphotocamera.h diff --git a/qml/content/CameraRenderer.qml b/qml/content/CameraRenderer.qml index b3991213..293a5bf1 100644 --- a/qml/content/CameraRenderer.qml +++ b/qml/content/CameraRenderer.qml @@ -5,6 +5,7 @@ import Qt5Compat.GraphicalEffects import QtQuick.Layouts import BackgroundFilter import CaptureProcessor +import GPhotoCamera Item { id: renderer @@ -65,6 +66,14 @@ Item { } } + GPhotoCamera { + id: gphotoCamera + Component.onCompleted: + { + console.log(gphotoCamera.availableCameras()) + } + } + CaptureSession { camera: Camera { diff --git a/qtbooth.pro b/qtbooth.pro index f27892ae..37b89cf0 100644 --- a/qtbooth.pro +++ b/qtbooth.pro @@ -16,6 +16,7 @@ SOURCES += src/collageiconmodel.cpp \ src/fakeprinter.cpp \ src/fileio.cpp \ src/filesystem.cpp \ + src/gphotocamera.cpp \ src/gpio.cpp \ src/main.cpp \ src/modelparser.cpp \ @@ -65,6 +66,7 @@ HEADERS += \ src/fakeprinter.h \ src/fileio.h \ src/filesystem.h \ + src/gphotocamera.h \ src/gpio.h \ src/modelparser.h \ src/noprinter.h \ @@ -88,6 +90,7 @@ DEFINES += GIT_CURRENT_SHA1="$(shell git -C \""$$_PRO_FILE_PWD_"\" describe)" LIBS += -L"$$PWD/libs/onnxruntime/lib" -lonnxruntime LIBS += -L"$$PWD/libs/ncnn/lib" -lncnn +LIBS += -lgphoto2 !isEmpty(PREFIX) { INSTALLS += target diff --git a/src/gphotocamera.cpp b/src/gphotocamera.cpp new file mode 100644 index 00000000..6eb0dc0e --- /dev/null +++ b/src/gphotocamera.cpp @@ -0,0 +1,94 @@ +#include "gphotocamera.h" +#include +#include + +GPhotoCameraDevice::GPhotoCameraDevice() : mWorker(new GPhotoCameraWorker() ) +{ + mWorker->moveToThread(&mWorkerThread); + + connect(this, &QVideoFrameInput::readyToSendVideoFrame, mWorker.get(), &GPhotoCameraWorker::getPreviewFrame); + connect(mWorker.get(), &GPhotoCameraWorker::frameReady, this, &GPhotoCameraDevice::onFrameReady); + + mWorkerThread.start(); +} + +GPhotoCameraDevice::~GPhotoCameraDevice() +{ + mWorkerThread.quit(); + mWorkerThread.wait(); +} + +QString GPhotoCameraDevice::getDefautCamera() const +{ + QStringList cameras = availableCameras(); + if (!cameras.isEmpty()) { + return cameras.first(); + } + return QString(); +} + +QStringList GPhotoCameraDevice::availableCameras() const +{ + QStringList result; + QMetaObject::invokeMethod(mWorker.get(), "availableCameras", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QStringList, result)); + return result; +} + +void GPhotoCameraDevice::onFrameReady(const QVideoFrame &frame) +{ + sendVideoFrame(frame); +} + +GPhotoCameraWorker::GPhotoCameraWorker() +{ +} +GPhotoCameraWorker::~GPhotoCameraWorker() +{ +} + +void GPhotoCameraWorker::startCamera(const QString &cameraName) +{ + // Implement camera start logic using gPhoto2 +} + +void GPhotoCameraWorker::stopCamera() +{ + // Implement camera stop logic using gPhoto2 +} + +void GPhotoCameraWorker::captureImage() +{ + // Implement image capture logic using gPhoto2 +} + +QStringList GPhotoCameraWorker::availableCameras() const +{ + QStringList cameraList; + + GPContext *context = gp_context_new(); + CameraList *list; + gp_list_new(&list); + gp_camera_autodetect(list, context); + + int count = gp_list_count(list); + for (int i = 0; i < count; i++) { + const char *name; + const char *value; + gp_list_get_name(list, i, &name); + gp_list_get_value(list, i, &value); + cameraList.append(QString("%1 (%2)").arg(name).arg(value)); + } + + gp_list_free(list); + gp_context_unref(context); + + return cameraList; +} + +void GPhotoCameraWorker::getPreviewFrame() +{ + // Implement logic to get a preview frame from the camera and emit frameReady signal +} + + diff --git a/src/gphotocamera.h b/src/gphotocamera.h new file mode 100644 index 00000000..f389638f --- /dev/null +++ b/src/gphotocamera.h @@ -0,0 +1,51 @@ +#pragma once +#include +#include +#include + + +class GPhotoCameraWorker; + +class GPhotoCameraDevice : public QVideoFrameInput +{ + Q_OBJECT +public: + GPhotoCameraDevice(); + ~GPhotoCameraDevice() override; + + Q_INVOKABLE QStringList availableCameras() const; + + Q_INVOKABLE QString getDefautCamera() const; +protected: + QThread mWorkerThread; + +protected slots: + void onFrameReady(const QVideoFrame &frame); + +protected: + // worker object for camera operations in thread + std::unique_ptr mWorker; +}; + +class GPhotoCameraWorker : public QObject +{ + Q_OBJECT +public: + GPhotoCameraWorker(); + ~GPhotoCameraWorker() override; +public slots: + void startCamera(const QString &cameraName); + void stopCamera(); + void captureImage(); + + void getPreviewFrame(); + + QStringList availableCameras() const; +signals: + void frameReady(const QVideoFrame &frame); + void errorOccurred(const QString &error); +private: + // Add private members for camera handling +}; + + diff --git a/src/main.cpp b/src/main.cpp index 1dd7f46e..62e52d3e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include "gphotocamera.h" #include "translationhelper.h" #include "captureprocessor.h" #include "fakeprinter.h" @@ -126,6 +127,7 @@ int main(int argc, char *argv[]) qmlRegisterType("System", 1, 0, "System"); qmlRegisterType("CaptureProcessor", 1, 0, "CaptureProcessor"); + qmlRegisterType("GPhotoCamera", 1, 0, "GPhotoCamera"); qmlRegisterInterface("AbstractPrinter", 1); qmlRegisterUncreatableType("Printer", 1, 0, "Printer", "Printer can only be created via PrinterFactory"); From fcb95f0d9bb3ccf4ab2a0e612d6b62ee6adcae5c Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Wed, 1 Oct 2025 22:14:13 +0200 Subject: [PATCH 03/17] wip gphoto implementation --- qtbooth.pro | 2 +- src/gphotocamera.cpp | 21 ++++++++++++++++++++- src/gphotocamera.h | 13 ++++++++++++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/qtbooth.pro b/qtbooth.pro index 37b89cf0..0a1fcd09 100644 --- a/qtbooth.pro +++ b/qtbooth.pro @@ -90,7 +90,7 @@ DEFINES += GIT_CURRENT_SHA1="$(shell git -C \""$$_PRO_FILE_PWD_"\" describe)" LIBS += -L"$$PWD/libs/onnxruntime/lib" -lonnxruntime LIBS += -L"$$PWD/libs/ncnn/lib" -lncnn -LIBS += -lgphoto2 +LIBS += -lgphoto2 -lgphoto2_port !isEmpty(PREFIX) { INSTALLS += target diff --git a/src/gphotocamera.cpp b/src/gphotocamera.cpp index 6eb0dc0e..68d34ba7 100644 --- a/src/gphotocamera.cpp +++ b/src/gphotocamera.cpp @@ -1,6 +1,8 @@ #include "gphotocamera.h" #include #include +#include +#include GPhotoCameraDevice::GPhotoCameraDevice() : mWorker(new GPhotoCameraWorker() ) { @@ -40,8 +42,17 @@ void GPhotoCameraDevice::onFrameReady(const QVideoFrame &frame) sendVideoFrame(frame); } -GPhotoCameraWorker::GPhotoCameraWorker() +GPhotoCameraWorker::GPhotoCameraWorker() : mContext(gp_context_new(), gp_context_unref) + , mPortInfoList(nullptr, gp_port_info_list_free) + , mAbilitiesList(nullptr, gp_abilities_list_free) { + GPPortInfoList *piList; + gp_port_info_list_new(&piList); + mPortInfoList.reset(piList); + + CameraAbilitiesList *caList; + gp_abilities_list_new(&caList); + mAbilitiesList.reset(caList); } GPhotoCameraWorker::~GPhotoCameraWorker() { @@ -89,6 +100,14 @@ QStringList GPhotoCameraWorker::availableCameras() const void GPhotoCameraWorker::getPreviewFrame() { // Implement logic to get a preview frame from the camera and emit frameReady signal + + // retrieve frame from selected gphoto camera + + // convert to QImage + + // and convert to QVideoFrame + + // emit frameReady(videoFrame); } diff --git a/src/gphotocamera.h b/src/gphotocamera.h index f389638f..ddb52946 100644 --- a/src/gphotocamera.h +++ b/src/gphotocamera.h @@ -3,9 +3,17 @@ #include #include +#include +#include +#include + class GPhotoCameraWorker; +using CameraAbilitiesListPtr = std::unique_ptr; +using GPContextPtr = std::unique_ptr; +using GPPortInfoListPtr = std::unique_ptr; + class GPhotoCameraDevice : public QVideoFrameInput { Q_OBJECT @@ -44,7 +52,10 @@ public slots: signals: void frameReady(const QVideoFrame &frame); void errorOccurred(const QString &error); -private: +protected: + GPContextPtr mContext; + GPPortInfoListPtr mPortInfoList; + CameraAbilitiesListPtr mAbilitiesList; // Add private members for camera handling }; From 1f5c1a25620cb371526175ebb8e8569b6b5c5ae0 Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Wed, 1 Oct 2025 22:40:43 +0200 Subject: [PATCH 04/17] update gphoto2 version --- io.github.saeugetier.photobooth.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/io.github.saeugetier.photobooth.json b/io.github.saeugetier.photobooth.json index 23063ac3..4b141815 100644 --- a/io.github.saeugetier.photobooth.json +++ b/io.github.saeugetier.photobooth.json @@ -142,8 +142,8 @@ { "type": "git", "url": "https://github.com/gphoto/libgphoto2.git", - "tag": "v2.5.31", - "commit": "ba28af2d22fd4cb7fa76a8ff569ba498e8021db5", + "tag": "v2.5.32", + "commit": "de1f0617b1ffa39c1980d1306343709c7dc0120e", "x-checker-data": { "type": "anitya", "project-id": 12558, From 6b390a79f2189a0824a5795047d824158efcd813 Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Wed, 1 Oct 2025 23:09:03 +0200 Subject: [PATCH 05/17] implement capture preview --- src/gphotocamera.cpp | 179 ++++++++++++++++++++++++------------------- src/gphotocamera.h | 9 +++ 2 files changed, 108 insertions(+), 80 deletions(-) diff --git a/src/gphotocamera.cpp b/src/gphotocamera.cpp index 68d34ba7..70bd0bca 100644 --- a/src/gphotocamera.cpp +++ b/src/gphotocamera.cpp @@ -3,111 +3,130 @@ #include #include #include +#include +#include -GPhotoCameraDevice::GPhotoCameraDevice() : mWorker(new GPhotoCameraWorker() ) -{ - mWorker->moveToThread(&mWorkerThread); +namespace { +constexpr auto capturingFailLimit = 10; +} - connect(this, &QVideoFrameInput::readyToSendVideoFrame, mWorker.get(), &GPhotoCameraWorker::getPreviewFrame); - connect(mWorker.get(), &GPhotoCameraWorker::frameReady, this, &GPhotoCameraDevice::onFrameReady); +GPhotoCameraDevice::GPhotoCameraDevice() : mWorker(new GPhotoCameraWorker()) { + mWorker->moveToThread(&mWorkerThread); - mWorkerThread.start(); -} + connect(this, &QVideoFrameInput::readyToSendVideoFrame, mWorker.get(), + &GPhotoCameraWorker::getPreviewFrame); + connect(mWorker.get(), &GPhotoCameraWorker::frameReady, this, + &GPhotoCameraDevice::onFrameReady); -GPhotoCameraDevice::~GPhotoCameraDevice() -{ - mWorkerThread.quit(); - mWorkerThread.wait(); + mWorkerThread.start(); } -QString GPhotoCameraDevice::getDefautCamera() const -{ - QStringList cameras = availableCameras(); - if (!cameras.isEmpty()) { - return cameras.first(); - } - return QString(); +GPhotoCameraDevice::~GPhotoCameraDevice() { + mWorkerThread.quit(); + mWorkerThread.wait(); } -QStringList GPhotoCameraDevice::availableCameras() const -{ - QStringList result; - QMetaObject::invokeMethod(mWorker.get(), "availableCameras", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QStringList, result)); - return result; +QString GPhotoCameraDevice::getDefautCamera() const { + QStringList cameras = availableCameras(); + if (!cameras.isEmpty()) { + return cameras.first(); + } + return QString(); } -void GPhotoCameraDevice::onFrameReady(const QVideoFrame &frame) -{ - sendVideoFrame(frame); +QStringList GPhotoCameraDevice::availableCameras() const { + QStringList result; + QMetaObject::invokeMethod(mWorker.get(), "availableCameras", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QStringList, result)); + return result; } -GPhotoCameraWorker::GPhotoCameraWorker() : mContext(gp_context_new(), gp_context_unref) - , mPortInfoList(nullptr, gp_port_info_list_free) - , mAbilitiesList(nullptr, gp_abilities_list_free) -{ - GPPortInfoList *piList; - gp_port_info_list_new(&piList); - mPortInfoList.reset(piList); - - CameraAbilitiesList *caList; - gp_abilities_list_new(&caList); - mAbilitiesList.reset(caList); -} -GPhotoCameraWorker::~GPhotoCameraWorker() -{ +void GPhotoCameraDevice::onFrameReady(const QVideoFrame &frame) { + sendVideoFrame(frame); } -void GPhotoCameraWorker::startCamera(const QString &cameraName) -{ - // Implement camera start logic using gPhoto2 +GPhotoCameraWorker::GPhotoCameraWorker() + : mContext(gp_context_new(), gp_context_unref), + mPortInfoList(nullptr, gp_port_info_list_free), + mAbilitiesList(nullptr, gp_abilities_list_free), + mCamera(nullptr, gp_camera_free), mPreviewFile(nullptr, gp_file_free) { + GPPortInfoList *piList; + gp_port_info_list_new(&piList); + mPortInfoList.reset(piList); + + CameraAbilitiesList *caList; + gp_abilities_list_new(&caList); + mAbilitiesList.reset(caList); } +GPhotoCameraWorker::~GPhotoCameraWorker() {} -void GPhotoCameraWorker::stopCamera() -{ - // Implement camera stop logic using gPhoto2 +void GPhotoCameraWorker::startCamera(const QString &cameraName) { + // Implement camera start logic using gPhoto2 } -void GPhotoCameraWorker::captureImage() -{ - // Implement image capture logic using gPhoto2 +void GPhotoCameraWorker::stopCamera() { + mCameraStarted = false; + mCamera.reset(); + mPreviewFile.reset(); } -QStringList GPhotoCameraWorker::availableCameras() const -{ - QStringList cameraList; - - GPContext *context = gp_context_new(); - CameraList *list; - gp_list_new(&list); - gp_camera_autodetect(list, context); - - int count = gp_list_count(list); - for (int i = 0; i < count; i++) { - const char *name; - const char *value; - gp_list_get_name(list, i, &name); - gp_list_get_value(list, i, &value); - cameraList.append(QString("%1 (%2)").arg(name).arg(value)); - } - - gp_list_free(list); - gp_context_unref(context); - - return cameraList; +void GPhotoCameraWorker::captureImage() { + // Implement image capture logic using gPhoto2 } -void GPhotoCameraWorker::getPreviewFrame() -{ - // Implement logic to get a preview frame from the camera and emit frameReady signal +QStringList GPhotoCameraWorker::availableCameras() const { + QStringList cameraList; - // retrieve frame from selected gphoto camera - - // convert to QImage + CameraList *list; + gp_list_new(&list); + gp_camera_autodetect(list, mContext.get()); - // and convert to QVideoFrame + int count = gp_list_count(list); + for (int i = 0; i < count; i++) { + const char *name; + const char *value; + gp_list_get_name(list, i, &name); + gp_list_get_value(list, i, &value); + cameraList.append(QString("%1 (%2)").arg(name).arg(value)); + } - // emit frameReady(videoFrame); + gp_list_free(list); + + return cameraList; } +void GPhotoCameraWorker::getPreviewFrame() { + if (!mCameraStarted) { + emit errorOccurred("Camera not started"); + return; + } else { + + gp_file_clean(mPreviewFile.get()); + + auto ret = + gp_camera_capture_preview(mCamera.get(), mPreviewFile.get(), mContext.get()); + if (GP_OK == ret) { + const char *data = nullptr; + unsigned long int size = 0; + ret = gp_file_get_data_and_size(mPreviewFile.get(), &data, &size); + if (GP_OK == ret) { + mCapturingFailCount = 0; + if (!QThread::currentThread()->isInterruptionRequested()) { + auto image = QImage::fromData(QByteArray(data, int(size))).convertToFormat(QImage::Format_RGB32); + emit frameReady(QVideoFrame(image)); + } + return; + } + } + + qWarning() << "GPhoto: Failed retrieving preview" << ret; + ++mCapturingFailCount; + if (capturingFailLimit < mCapturingFailCount) { + qWarning() << "GPhoto: Closing camera because of capturing fail"; + emit errorOccurred(tr("Unable to capture frame")); + stopCamera(); + } + } +} diff --git a/src/gphotocamera.h b/src/gphotocamera.h index ddb52946..f4a1274b 100644 --- a/src/gphotocamera.h +++ b/src/gphotocamera.h @@ -2,10 +2,13 @@ #include #include #include +#include +#include #include #include #include +#include class GPhotoCameraWorker; @@ -13,6 +16,8 @@ class GPhotoCameraWorker; using CameraAbilitiesListPtr = std::unique_ptr; using GPContextPtr = std::unique_ptr; using GPPortInfoListPtr = std::unique_ptr; +using CameraFilePtr = std::unique_ptr; +using CameraPtr = std::unique_ptr; class GPhotoCameraDevice : public QVideoFrameInput { @@ -56,6 +61,10 @@ public slots: GPContextPtr mContext; GPPortInfoListPtr mPortInfoList; CameraAbilitiesListPtr mAbilitiesList; + CameraPtr mCamera; + CameraFilePtr mPreviewFile; + bool mCameraStarted = false; + uint32_t mCapturingFailCount = 0; // Add private members for camera handling }; From acbf5c4543f4d9e897bc630a698678f9d91b60c0 Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Tue, 28 Oct 2025 21:53:29 +0100 Subject: [PATCH 06/17] implement gphotoworker --- src/gphotocamera.cpp | 151 ++++++++++++++++++++++++++++++++++++++++--- src/gphotocamera.h | 8 +++ 2 files changed, 150 insertions(+), 9 deletions(-) diff --git a/src/gphotocamera.cpp b/src/gphotocamera.cpp index 70bd0bca..ed94c125 100644 --- a/src/gphotocamera.cpp +++ b/src/gphotocamera.cpp @@ -1,10 +1,11 @@ #include "gphotocamera.h" +#include +#include +#include #include #include #include #include -#include -#include namespace { constexpr auto capturingFailLimit = 10; @@ -42,6 +43,19 @@ QStringList GPhotoCameraDevice::availableCameras() const { return result; } +void GPhotoCameraDevice::setCameraName(const QString &name) { + mCameraName = name; + + if( mCameraName.isEmpty()) { + QMetaObject::invokeMethod(mWorker.get(), "stopCamera", + Qt::QueuedConnection); + } + else { + QMetaObject::invokeMethod(mWorker.get(), "startCamera", + Qt::QueuedConnection, Q_ARG(QString, name)); + } +} + void GPhotoCameraDevice::onFrameReady(const QVideoFrame &frame) { sendVideoFrame(frame); } @@ -62,17 +76,135 @@ GPhotoCameraWorker::GPhotoCameraWorker() GPhotoCameraWorker::~GPhotoCameraWorker() {} void GPhotoCameraWorker::startCamera(const QString &cameraName) { - // Implement camera start logic using gPhoto2 + if (mCameraStarted) { + stopCamera(); + } + + // Parse camera name and port from format "CameraName (port)" + QString name = cameraName.section(" (", 0, 0); + QString port = cameraName.section(" (", 1).chopped(1); + + // Initialize port info list + gp_port_info_list_load(mPortInfoList.get()); + + // Load camera abilities + gp_abilities_list_load(mAbilitiesList.get(), mContext.get()); + + // Create new camera object + Camera *camera; + gp_camera_new(&camera); + mCamera.reset(camera); + + // Find camera abilities + CameraAbilities abilities; + int abilitiesIndex = gp_abilities_list_lookup_model( + mAbilitiesList.get(), name.toLatin1().constData()); + if (abilitiesIndex < 0) { + emit errorOccurred(tr("Camera %1 not found").arg(name)); + return; + } + gp_abilities_list_get_abilities(mAbilitiesList.get(), abilitiesIndex, + &abilities); + gp_camera_set_abilities(mCamera.get(), abilities); + + // Find and set port + int portIndex = gp_port_info_list_lookup_path(mPortInfoList.get(), + port.toLatin1().constData()); + if (portIndex < 0) { + emit errorOccurred(tr("Port %1 not found").arg(port)); + return; + } + GPPortInfo portInfo; + gp_port_info_list_get_info(mPortInfoList.get(), portIndex, &portInfo); + gp_camera_set_port_info(mCamera.get(), portInfo); + + // Initialize camera connection + int ret = gp_camera_init(mCamera.get(), mContext.get()); + if (ret < GP_OK) { + emit errorOccurred(tr("Failed to initialize camera: %1").arg(ret)); + mCamera.reset(); + return; + } + + // Create preview file handle + CameraFile *file; + gp_file_new(&file); + mPreviewFile.reset(file); + + mCameraStarted = true; } void GPhotoCameraWorker::stopCamera() { + if (!mCameraStarted) { + return; + } + + // Reset capturing fail counter + mCapturingFailCount = 0; + + // Close camera connection first + if (mCamera) { + gp_camera_exit(mCamera.get(), mContext.get()); + } + + // Reset smart pointers in specific order + mPreviewFile.reset(); // Release preview file handle first + mCamera.reset(); // Release camera handle next + mCameraStarted = false; - mCamera.reset(); - mPreviewFile.reset(); } void GPhotoCameraWorker::captureImage() { // Implement image capture logic using gPhoto2 + if (!mCameraStarted || !mCamera) { + emit captureError("Camera not started"); + return; + } + + // Create a new file for the capture + CameraFile *file; + gp_file_new(&file); + CameraFilePtr captureFile(file, gp_file_free); + + // Capture the image + CameraFilePath camera_file_path; + int ret = gp_camera_capture(mCamera.get(), GP_CAPTURE_IMAGE, + &camera_file_path, mContext.get()); + if (ret < GP_OK) { + emit captureError(tr("Failed to capture image: %1").arg(ret)); + return; + } + + // Download the image from camera + ret = gp_camera_file_get(mCamera.get(), camera_file_path.folder, + camera_file_path.name, GP_FILE_TYPE_NORMAL, + captureFile.get(), mContext.get()); + if (ret < GP_OK) { + emit captureError(tr("Failed to download image: %1").arg(ret)); + return; + } + + // Get the image data + const char *data; + unsigned long int size = 0; + ret = gp_file_get_data_and_size(captureFile.get(), &data, &size); + if (ret < GP_OK) { + emit captureError(tr("Failed to get image data: %1").arg(ret)); + return; + } + + // Convert to QImage and emit + QImage image = QImage::fromData(QByteArray(data, int(size))); + if (image.isNull()) { + emit captureError("Failed to convert image data"); + return; + } + + // Delete the file from camera + gp_camera_file_delete(mCamera.get(), camera_file_path.folder, + camera_file_path.name, mContext.get()); + + emit imageCaptured(image); } QStringList GPhotoCameraWorker::availableCameras() const { @@ -104,8 +236,8 @@ void GPhotoCameraWorker::getPreviewFrame() { gp_file_clean(mPreviewFile.get()); - auto ret = - gp_camera_capture_preview(mCamera.get(), mPreviewFile.get(), mContext.get()); + auto ret = gp_camera_capture_preview(mCamera.get(), mPreviewFile.get(), + mContext.get()); if (GP_OK == ret) { const char *data = nullptr; unsigned long int size = 0; @@ -113,8 +245,9 @@ void GPhotoCameraWorker::getPreviewFrame() { if (GP_OK == ret) { mCapturingFailCount = 0; if (!QThread::currentThread()->isInterruptionRequested()) { - auto image = QImage::fromData(QByteArray(data, int(size))).convertToFormat(QImage::Format_RGB32); - emit frameReady(QVideoFrame(image)); + auto image = QImage::fromData(QByteArray(data, int(size))) + .convertToFormat(QImage::Format_RGB32); + emit frameReady(QVideoFrame(image)); } return; } diff --git a/src/gphotocamera.h b/src/gphotocamera.h index f4a1274b..4ff516fd 100644 --- a/src/gphotocamera.h +++ b/src/gphotocamera.h @@ -22,6 +22,7 @@ using CameraPtr = std::unique_ptr; class GPhotoCameraDevice : public QVideoFrameInput { Q_OBJECT + Q_PROPERTY(QString cameraName READ getCameraName WRITE setCameraName) public: GPhotoCameraDevice(); ~GPhotoCameraDevice() override; @@ -29,8 +30,13 @@ class GPhotoCameraDevice : public QVideoFrameInput Q_INVOKABLE QStringList availableCameras() const; Q_INVOKABLE QString getDefautCamera() const; + + QString getCameraName() const { return mCameraName; } + void setCameraName(const QString &name); protected: QThread mWorkerThread; + QString mCameraName; + bool mPreviewStarted = false; protected slots: void onFrameReady(const QVideoFrame &frame); @@ -57,6 +63,8 @@ public slots: signals: void frameReady(const QVideoFrame &frame); void errorOccurred(const QString &error); + void imageCaptured(const QImage &image); + void captureError(const QString &error); protected: GPContextPtr mContext; GPPortInfoListPtr mPortInfoList; From a44d637cf6161d7b87bb9760b62440926d849356 Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Sun, 2 Nov 2025 16:58:28 +0100 Subject: [PATCH 07/17] output error messages as log message --- qml/content/CameraRenderer.qml | 23 +++++++++++++++++++++++ src/gphotocamera.cpp | 6 +++++- src/gphotocamera.h | 3 +++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/qml/content/CameraRenderer.qml b/qml/content/CameraRenderer.qml index 293a5bf1..10f80d83 100644 --- a/qml/content/CameraRenderer.qml +++ b/qml/content/CameraRenderer.qml @@ -71,6 +71,10 @@ Item { Component.onCompleted: { console.log(gphotoCamera.availableCameras()) + cameraName = gphotoCamera.getDefautCamera() + } + onErrorOccurred: function(errorString) { + console.log("GPhotoCamera errorOccurred: " + errorString) } } @@ -231,6 +235,25 @@ Item { height: output.height } + CaptureSession + { + id: rendererSession + videoFrameInput: gphotoCamera + videoOutput: output2 + } + + VideoOutput { + id: output2 + + rotation: applicationSettings.cameraOrientation + + anchors.fill: parent + + visible: false + + //focus: visible // to receive focus and capture key events when visible + } + /* Timer { id: cameraDiscoveryTimer diff --git a/src/gphotocamera.cpp b/src/gphotocamera.cpp index ed94c125..e26f6ddb 100644 --- a/src/gphotocamera.cpp +++ b/src/gphotocamera.cpp @@ -18,6 +18,8 @@ GPhotoCameraDevice::GPhotoCameraDevice() : mWorker(new GPhotoCameraWorker()) { &GPhotoCameraWorker::getPreviewFrame); connect(mWorker.get(), &GPhotoCameraWorker::frameReady, this, &GPhotoCameraDevice::onFrameReady); + connect(mWorker.get(), &GPhotoCameraWorker::errorOccurred, + this, &GPhotoCameraDevice::errorOccurred); mWorkerThread.start(); } @@ -45,7 +47,7 @@ QStringList GPhotoCameraDevice::availableCameras() const { void GPhotoCameraDevice::setCameraName(const QString &name) { mCameraName = name; - + if( mCameraName.isEmpty()) { QMetaObject::invokeMethod(mWorker.get(), "stopCamera", Qt::QueuedConnection); @@ -132,6 +134,8 @@ void GPhotoCameraWorker::startCamera(const QString &cameraName) { mPreviewFile.reset(file); mCameraStarted = true; + + getPreviewFrame(); } void GPhotoCameraWorker::stopCamera() { diff --git a/src/gphotocamera.h b/src/gphotocamera.h index 4ff516fd..a2944df0 100644 --- a/src/gphotocamera.h +++ b/src/gphotocamera.h @@ -44,6 +44,9 @@ protected slots: protected: // worker object for camera operations in thread std::unique_ptr mWorker; + +signals: + void errorOccurred(const QString &error); }; class GPhotoCameraWorker : public QObject From 5cc732c7a400e4a5a9962f75d64c2721cb76e51a Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Sun, 2 Nov 2025 21:43:13 +0100 Subject: [PATCH 08/17] Add image capture functionality and error handling to GPhotoCamera --- qml/content/CameraRenderer.qml | 10 +++++++++- src/gphotocamera.cpp | 11 ++++++++++- src/gphotocamera.h | 4 ++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/qml/content/CameraRenderer.qml b/qml/content/CameraRenderer.qml index 10f80d83..61d24fdc 100644 --- a/qml/content/CameraRenderer.qml +++ b/qml/content/CameraRenderer.qml @@ -76,6 +76,12 @@ Item { onErrorOccurred: function(errorString) { console.log("GPhotoCamera errorOccurred: " + errorString) } + onImageCaptured: function(image) { + console.log("GPhotoCamera imageCaptured") + } + onCaptureError: function(errorString) { + console.log("GPhotoCamera captureError: " + errorString) + } } CaptureSession { @@ -249,7 +255,7 @@ Item { anchors.fill: parent - visible: false + visible: true //focus: visible // to receive focus and capture key events when visible } @@ -279,6 +285,8 @@ Item { if (cameraSession.imageCapture.readyForCapture) { state = "snapshot" cameraSession.imageCapture.capture() + + gphotoCamera.captureImage() } else { renderer.state = "preview" failed() diff --git a/src/gphotocamera.cpp b/src/gphotocamera.cpp index e26f6ddb..fde91ceb 100644 --- a/src/gphotocamera.cpp +++ b/src/gphotocamera.cpp @@ -20,7 +20,11 @@ GPhotoCameraDevice::GPhotoCameraDevice() : mWorker(new GPhotoCameraWorker()) { &GPhotoCameraDevice::onFrameReady); connect(mWorker.get(), &GPhotoCameraWorker::errorOccurred, this, &GPhotoCameraDevice::errorOccurred); - + connect(mWorker.get(), &GPhotoCameraWorker::imageCaptured, + this, &GPhotoCameraDevice::imageCaptured); + connect(mWorker.get(), &GPhotoCameraWorker::captureError, + this, &GPhotoCameraDevice::captureError); + mWorkerThread.start(); } @@ -45,6 +49,11 @@ QStringList GPhotoCameraDevice::availableCameras() const { return result; } +void GPhotoCameraDevice::captureImage() const { + QMetaObject::invokeMethod(mWorker.get(), "captureImage", + Qt::QueuedConnection); +} + void GPhotoCameraDevice::setCameraName(const QString &name) { mCameraName = name; diff --git a/src/gphotocamera.h b/src/gphotocamera.h index a2944df0..5d81568f 100644 --- a/src/gphotocamera.h +++ b/src/gphotocamera.h @@ -31,6 +31,8 @@ class GPhotoCameraDevice : public QVideoFrameInput Q_INVOKABLE QString getDefautCamera() const; + Q_INVOKABLE void captureImage() const; + QString getCameraName() const { return mCameraName; } void setCameraName(const QString &name); protected: @@ -47,6 +49,8 @@ protected slots: signals: void errorOccurred(const QString &error); + void imageCaptured(const QImage &image); + void captureError(const QString &error); }; class GPhotoCameraWorker : public QObject From a12e4b3da6d0f9cb1d2781b1c0f51437ccbbde4b Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Sun, 2 Nov 2025 22:09:22 +0100 Subject: [PATCH 09/17] add periodic parameter checks for keeping the camera alive --- src/gphotocamera.cpp | 55 +++++++++++++++++++++++++++++++++++--------- src/gphotocamera.h | 4 ++++ 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/gphotocamera.cpp b/src/gphotocamera.cpp index fde91ceb..d83ee049 100644 --- a/src/gphotocamera.cpp +++ b/src/gphotocamera.cpp @@ -18,13 +18,13 @@ GPhotoCameraDevice::GPhotoCameraDevice() : mWorker(new GPhotoCameraWorker()) { &GPhotoCameraWorker::getPreviewFrame); connect(mWorker.get(), &GPhotoCameraWorker::frameReady, this, &GPhotoCameraDevice::onFrameReady); - connect(mWorker.get(), &GPhotoCameraWorker::errorOccurred, - this, &GPhotoCameraDevice::errorOccurred); - connect(mWorker.get(), &GPhotoCameraWorker::imageCaptured, - this, &GPhotoCameraDevice::imageCaptured); - connect(mWorker.get(), &GPhotoCameraWorker::captureError, - this, &GPhotoCameraDevice::captureError); - + connect(mWorker.get(), &GPhotoCameraWorker::errorOccurred, this, + &GPhotoCameraDevice::errorOccurred); + connect(mWorker.get(), &GPhotoCameraWorker::imageCaptured, this, + &GPhotoCameraDevice::imageCaptured); + connect(mWorker.get(), &GPhotoCameraWorker::captureError, this, + &GPhotoCameraDevice::captureError); + mWorkerThread.start(); } @@ -57,13 +57,12 @@ void GPhotoCameraDevice::captureImage() const { void GPhotoCameraDevice::setCameraName(const QString &name) { mCameraName = name; - if( mCameraName.isEmpty()) { + if (mCameraName.isEmpty()) { QMetaObject::invokeMethod(mWorker.get(), "stopCamera", Qt::QueuedConnection); - } - else { + } else { QMetaObject::invokeMethod(mWorker.get(), "startCamera", - Qt::QueuedConnection, Q_ARG(QString, name)); + Qt::QueuedConnection, Q_ARG(QString, name)); } } @@ -83,6 +82,10 @@ GPhotoCameraWorker::GPhotoCameraWorker() CameraAbilitiesList *caList; gp_abilities_list_new(&caList); mAbilitiesList.reset(caList); + + mCaptureTimer.setInterval(1000 * 60); // Check every minute + connect(&mCaptureTimer, &QTimer::timeout, this, + &GPhotoCameraWorker::checkCaptureParameter); } GPhotoCameraWorker::~GPhotoCameraWorker() {} @@ -144,6 +147,10 @@ void GPhotoCameraWorker::startCamera(const QString &cameraName) { mCameraStarted = true; + if (!mCaptureTimer.isActive()) { + mCaptureTimer.start(); + } + getPreviewFrame(); } @@ -152,6 +159,10 @@ void GPhotoCameraWorker::stopCamera() { return; } + if (mCaptureTimer.isActive()) { + mCaptureTimer.stop(); + } + // Reset capturing fail counter mCapturingFailCount = 0; @@ -276,3 +287,25 @@ void GPhotoCameraWorker::getPreviewFrame() { } } } + +void GPhotoCameraWorker::checkCaptureParameter() { + // this function checks and sets the capture parameter to keep the camera + // alive + if (!mCameraStarted || !mCamera) { + return; + } + + CameraWidget *widget = nullptr; + int ret = gp_camera_get_config(mCamera.get(), &widget, mContext.get()); + if (ret >= GP_OK) { + CameraWidget *child = nullptr; + ret = gp_widget_get_child_by_name(widget, "capture", &child); + if (ret >= GP_OK) { + ret = gp_widget_set_value(child, 1); // Set capture to true + if (ret >= GP_OK) { + gp_camera_set_config(mCamera.get(), widget, mContext.get()); + } + } + gp_widget_free(widget); + } +} diff --git a/src/gphotocamera.h b/src/gphotocamera.h index 5d81568f..246c56e9 100644 --- a/src/gphotocamera.h +++ b/src/gphotocamera.h @@ -73,6 +73,7 @@ public slots: void imageCaptured(const QImage &image); void captureError(const QString &error); protected: + QTimer mCaptureTimer; GPContextPtr mContext; GPPortInfoListPtr mPortInfoList; CameraAbilitiesListPtr mAbilitiesList; @@ -81,6 +82,9 @@ public slots: bool mCameraStarted = false; uint32_t mCapturingFailCount = 0; // Add private members for camera handling +protected slots: + // check and set capture parameters to keep camera alive + void checkCaptureParameter(); }; From c6a13a2127b9f57e3e2a1d8c20729d2270e504d6 Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Sat, 8 Nov 2025 22:40:31 +0100 Subject: [PATCH 10/17] Add CameraSource component for handling camera selection and image capture --- qml.qrc | 1 + qml/content/CameraSource.qml | 123 +++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 qml/content/CameraSource.qml diff --git a/qml.qrc b/qml.qrc index 868c5dde..b013c3ee 100644 --- a/qml.qrc +++ b/qml.qrc @@ -34,6 +34,7 @@ qml/SnapshotSettings.qml qml/SnapshotSettingsForm.ui.qml qml/content/CameraRenderer.qml + qml/content/CameraSource.qml qml/content/CollageImageDelegate.qml qml/content/CollageRenderer.qml qml/content/Countdown.qml diff --git a/qml/content/CameraSource.qml b/qml/content/CameraSource.qml new file mode 100644 index 00000000..9ccd7ecf --- /dev/null +++ b/qml/content/CameraSource.qml @@ -0,0 +1,123 @@ +Item +{ + id: cameraSource + + property alias output: output + property string cameraName: "" + + signal imageCaptured(var image) + signal errorOccurred(var errorString) + + onCameraNameChanged: { + console.log("CameraSource selected cameraName: " + cameraName) + var availableCameras = mediaDevices.videoInputs + for (var i = 0; i < availableCameras.length; i++) { + if (availableCameras[i].description === cameraName) { + systemCamera.cameraDevice = availableCameras[i] + cameraSource.state = "StandardCamera" + console.log("CameraSource using standard camera device: " + cameraName) + return + } + } + var gphotoCameras = gphotoCamera.availableCameras() + for (var j = 0; j < gphotoCameras.length; j++) { + if (gphotoCameras[j] === cameraName) { + cameraSource.state = "GPhotoCamera" + gphotoCamera.setCameraByName(cameraName) + console.log("CameraSource using GPhoto camera device: " + cameraName) + return + } + } + console.log("CameraSource could not find camera device: " + cameraName) + cameraSource.state = "noCamera" + } + + function captureImage() + { + if(state === "StandardCamera") + { + imageCapture.capture() + } + else if(state === "GPhotoCamera") + { + gphotoCamera.captureImage() + } + else + { + console.log("No camera available to capture image!") + } + } + + MediaDevices { + id: mediaDevices + } + + GPhotoCamera { + id: gphotoCamera + + onErrorOccurred: function(errorString) { + errorOccured(errorString) + } + onImageCaptured: function(image) { + imageCaptured(image) + } + onCaptureError: function(errorString) { + errorOccured(errorString) + } + } + + Camera { + id: systemCamera + cameraDevice: mediaDevices.defaultVideoInput + exposureMode: Camera.ExposurePortrait + exposureCompensation: -1.0 + whiteBalanceMode: Camera.WhiteBalanceAuto + flashMode: Camera.FlashAuto + torchMode: Camera.TorchAuto + } + + CaptureSession { + + id: cameraSession + + videoOutput: output + + imageCapture: ImageCapture { + id: imageCapture + + onImageCaptured: + { + imageCaptured(image) + } + onErrorOccurred: { + errorOccurred(errorString) + } + } + } + + VideoOutput { + id: output + anchors.fill: parent + visible: false + } + + states: [ + State { + name: "noCamera" + }, + State { + name: "StandardCamera" + PropertyChanges { + target: cameraSession + camera: systemCamera + } + }, + State { + name: "GPhotoCamera" + PropertyChanges { + target: cameraSession + videoFrameInput: gphotoCamera + } + } + ] +} From 5f01d02d07658a877d413fb466b2698bfaa287c6 Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Sat, 8 Nov 2025 22:40:56 +0100 Subject: [PATCH 11/17] Add GPhotoCamera integration and adjust ComboBox widths in SettingsMenu --- qml/SettingsMenu.qml | 11 +++++++++++ qml/SettingsMenuForm.ui.qml | 2 ++ 2 files changed, 13 insertions(+) diff --git a/qml/SettingsMenu.qml b/qml/SettingsMenu.qml index 73974e77..dc36e44c 100644 --- a/qml/SettingsMenu.qml +++ b/qml/SettingsMenu.qml @@ -4,6 +4,7 @@ import QtQuick.Controls import QtQuick.Dialogs import Qt.labs.platform import QtQml +import GPhotoCamera import "content" SettingsMenuForm { @@ -25,9 +26,19 @@ SettingsMenuForm { { listModel.push(availableCameras[i].description) } + var gphotoCameras = gphotoCamera.availableCameras(); + console.log("GPhoto Camera Count: " + Number(gphotoCameras.length).toString()) + for(i = 0; i < gphotoCameras.length; i++) + { + listModel.push(gphotoCameras[i]) + } return listModel; } + GPhotoCamera { + id: gphotoCamera + } + MediaDevices { id: mediaDevices diff --git a/qml/SettingsMenuForm.ui.qml b/qml/SettingsMenuForm.ui.qml index fba36c09..dc0239fe 100644 --- a/qml/SettingsMenuForm.ui.qml +++ b/qml/SettingsMenuForm.ui.qml @@ -154,6 +154,7 @@ Item { } ComboBox { id: comboBoxCamera + Layout.preferredWidth: 300 } } @@ -206,6 +207,7 @@ Item { } ComboBox { id: comboBoxCameraOrientation + Layout.preferredWidth: 300 textRole: "text" valueRole: "value" model: [{ From 32905d5587dc843808bba1ea77e819e517a65911 Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Sun, 9 Nov 2025 22:51:07 +0100 Subject: [PATCH 12/17] implement selection between gphoto and standard camera --- qml/Application.qml | 10 +- qml/ApplicationFlow.qml | 2 + qml/SettingsMenu.qml | 14 --- qml/content/CameraRenderer.qml | 197 ++++++--------------------------- qml/content/CameraSource.qml | 54 ++++++++- 5 files changed, 88 insertions(+), 189 deletions(-) diff --git a/qml/Application.qml b/qml/Application.qml index b9bcea90..de1d8266 100644 --- a/qml/Application.qml +++ b/qml/Application.qml @@ -138,7 +138,7 @@ ApplicationWindow { console.log("Window mode changed to: " + applicationSettings.windowMode) } - settingsMenu.comboBoxCamera.onCurrentIndexChanged: + settingsMenu.comboBoxCamera.onCurrentTextChanged: { applicationSettings.cameraName = settingsMenu.comboBoxCamera.currentText } @@ -225,13 +225,5 @@ ApplicationWindow { { flow.imagePreview.effectButton.visible = !disableEffectPopup } - - onCameraNameChanged: - { - print("Camera changed to " + cameraName) - var id = flow.settingsMenu.findDeviceId(cameraName) - print("Found ID: " + id) - flow.snapshotMenu.cameraRenderer.deviceId = id - } } } diff --git a/qml/ApplicationFlow.qml b/qml/ApplicationFlow.qml index 6110c1da..01e1879e 100644 --- a/qml/ApplicationFlow.qml +++ b/qml/ApplicationFlow.qml @@ -61,6 +61,8 @@ ApplicationFlowForm { state = "collageSelection" } + snapshotMenu.cameraRenderer.cameraName: applicationSettings.cameraName + imagePreview.onAccept: (filename, effect) => { if(applicationSettings.printEnable) diff --git a/qml/SettingsMenu.qml b/qml/SettingsMenu.qml index dc36e44c..732a0eb1 100644 --- a/qml/SettingsMenu.qml +++ b/qml/SettingsMenu.qml @@ -44,20 +44,6 @@ SettingsMenuForm { id: mediaDevices } - function findDeviceId(cameraName) - { - var i; - var availableCameras = mediaDevices.videoInputs; - for(i = 0; i < availableCameras.length; i++) - { - if(availableCameras[i].description === cameraName) - { - return availableCameras[i].id; - } - } - return mediaDevices.defaultVideoInput.id - } - Component.onCompleted: { versionText = "Version: " + system.getGitHash() diff --git a/qml/content/CameraRenderer.qml b/qml/content/CameraRenderer.qml index 61d24fdc..601e3c7f 100644 --- a/qml/content/CameraRenderer.qml +++ b/qml/content/CameraRenderer.qml @@ -14,35 +14,16 @@ Item { property bool photoProcessing: (state === "snapshot") property bool mirrored: true - property string deviceId: camera.deviceId + property string cameraName: "" property alias backgroundFilter: backgroundFilter property bool backgroundFilterEnabled: false property url backgroundImage: "" - function printDevicesToConsole(devices) { - console.log("Found " + devices.length + " camera devices!") - for (var i = 0; i < devices.length; i++) { - console.log( - "Found device: " + devices[i].deviceId + " with number " + i) - } - } - - MediaDevices { - id: mediaDevices + onCameraNameChanged: + { + print("Camera changed to " + cameraName) } - onDeviceIdChanged: id => { - // get the camera device with id from mediaDevices - console.log("Selected camera: " + id) - var availableCameras = mediaDevices.videoInputs - for (var i = 0; i < availableCameras.length; i++) { - if (availableCameras[i].deviceId === id) { - camera.cameraDevice = availableCameras[i] - break - } - } - } - CaptureProcessor { id: captureProcessor @@ -66,101 +47,41 @@ Item { } } - GPhotoCamera { - id: gphotoCamera - Component.onCompleted: - { - console.log(gphotoCamera.availableCameras()) - cameraName = gphotoCamera.getDefautCamera() - } - onErrorOccurred: function(errorString) { - console.log("GPhotoCamera errorOccurred: " + errorString) - } - onImageCaptured: function(image) { - console.log("GPhotoCamera imageCaptured") - } - onCaptureError: function(errorString) { - console.log("GPhotoCamera captureError: " + errorString) - } - } - - CaptureSession { + CameraSource + { + id: cameraSource - camera: Camera { - id: camera - cameraDevice: mediaDevices.defaultVideoInput - exposureMode: Camera.ExposurePortrait - exposureCompensation: -1.0 - whiteBalanceMode: Camera.WhiteBalanceAuto - flashMode: Camera.FlashAuto - torchMode: Camera.TorchAuto - } + cameraName: renderer.cameraName - id: cameraSession - - videoOutput: output - - imageCapture: ImageCapture { - id: imageCapture - - onImageCaptured: - { - whiteOverlay.state = "released" - renderer.state = "store" - console.log("Captured") - - console.log(applicationSettings.foldername.toString()) - var path = applicationSettings.foldername.toString() - if(backgroundFilterEnabled) - { - path = path + "/raw" - } - path = path.replace(/^(file:\/{2})/, "") - var cleanPath = decodeURIComponent(path) - console.log(cleanPath) - - captureProcessor.saveCapture(preview, cleanPath + "/Pict_" + new Date().toLocaleString( - locale, "dd_MM_yyyy_hh_mm_ss") + ".jpg") - } - onErrorOccurred: { - renderer.state = "preview" - failed() - } - onErrorStringChanged: { - console.log("Camera error: " + errorString) + onImageCaptured: function(image) { + whiteOverlay.state = "released" + renderer.state = "store" + console.log("Captured") + + console.log(applicationSettings.foldername.toString()) + var path = applicationSettings.foldername.toString() + if(backgroundFilterEnabled) + { + path = path + "/raw" } - } + path = path.replace(/^(file:\/{2})/, "") + var cleanPath = decodeURIComponent(path) + console.log(cleanPath) + captureProcessor.saveCapture(image, cleanPath + "/Pict_" + new Date().toLocaleString( + locale, "dd_MM_yyyy_hh_mm_ss") + ".jpg") + } - /*onCameraStateChanged: - { - if(camera.cameraState == Camera.UnloadedState) - { - console.log("Camera State Changed: Unloaded") - printDevicesToConsole(QtMultimedia.availableCameras) - camera.stop() - cameraDiscoveryTimer.start() - } - else if(camera.cameraState == Camera.LoadedState) - { - console.log("Camera State Changed: Loaded") - printDevicesToConsole(QtMultimedia.availableCameras) - } - else if(camera.cameraState == Camera.ActiveState) - { - console.log("Camera State Changed: Active"); - cameraDiscoveryTimer.stop() - } - else - { - console.log("Camera State Changed: Unknown"); - } - }*/ + onErrorOccurred: function(errorString) { + renderer.state = "preview" + failed() + } } + ReplaceBackgroundVideoFilter { id: backgroundFilter - videoSink: output.videoSink + videoSink: cameraSource.output.videoSink background: backgroundImage onCaptureProcessingFinished: fileName => @@ -174,14 +95,6 @@ Item { } } - Connections { - id: cameraErrorListener - target: camera - function errorOccured(_, errorString) { - console.log("Camera Error: " + errorString) - } - } - VideoOutput { id: output @@ -241,52 +154,10 @@ Item { height: output.height } - CaptureSession - { - id: rendererSession - videoFrameInput: gphotoCamera - videoOutput: output2 - } - - VideoOutput { - id: output2 - - rotation: applicationSettings.cameraOrientation - - anchors.fill: parent - - visible: true - - //focus: visible // to receive focus and capture key events when visible - } - - /* Timer - { - id: cameraDiscoveryTimer - - interval: 1000 - repeat: true - - onTriggered: - { - //camera discovery is delayed - var availableCameras = QtMultimedia.availableCameras - printDevicesToConsole(availableCameras) - - if(availableCameras.length > 0) - { - camera.deviceId = availableCameras[0].deviceId - camera.start() - } - - } - }*/ function takePhoto() { - if (cameraSession.imageCapture.readyForCapture) { + if (cameraSource.readyForCapture) { state = "snapshot" - cameraSession.imageCapture.capture() - - gphotoCamera.captureImage() + cameraSource.captureImage() } else { renderer.state = "preview" failed() @@ -319,7 +190,7 @@ Item { } StateChangeScript { script: { - camera.start() + cameraSource.start() } } }, @@ -350,7 +221,7 @@ Item { } ScriptAction { script: { - camera.stop() + cameraSource.stop() } } } diff --git a/qml/content/CameraSource.qml b/qml/content/CameraSource.qml index 9ccd7ecf..72e9af34 100644 --- a/qml/content/CameraSource.qml +++ b/qml/content/CameraSource.qml @@ -1,9 +1,17 @@ +import GPhotoCamera +import QtQuick +import QtMultimedia +import QtQuick.Controls +import Qt5Compat.GraphicalEffects +import QtQuick.Layouts + Item { id: cameraSource property alias output: output property string cameraName: "" + property alias readyForCapture: cameraSession.imageCapture.readyForCapture signal imageCaptured(var image) signal errorOccurred(var errorString) @@ -23,7 +31,7 @@ Item for (var j = 0; j < gphotoCameras.length; j++) { if (gphotoCameras[j] === cameraName) { cameraSource.state = "GPhotoCamera" - gphotoCamera.setCameraByName(cameraName) + gphotoCamera.cameraName = cameraName console.log("CameraSource using GPhoto camera device: " + cameraName) return } @@ -32,11 +40,43 @@ Item cameraSource.state = "noCamera" } + function start() + { + if(state === "StandardCamera") + { + systemCamera.start() + } + else if(state === "GPhotoCamera") + { + //gphotoCamera.startCamera() + } + else + { + console.log("No camera available to start!") + } + } + + function stop() + { + if(state === "StandardCamera") + { + systemCamera.stop() + } + else if(state === "GPhotoCamera") + { + //gphotoCamera.stopCamera() + } + else + { + console.log("No camera available to stop!") + } + } + function captureImage() { if(state === "StandardCamera") { - imageCapture.capture() + cameraSession.imageCapture.capture() } else if(state === "GPhotoCamera") { @@ -75,6 +115,14 @@ Item flashMode: Camera.FlashAuto torchMode: Camera.TorchAuto } + + Connections { + id: cameraErrorListener + target: systemCamera + function errorOccured(_, errorString) { + console.log("Camera Error: " + errorString) + } + } CaptureSession { @@ -87,7 +135,7 @@ Item onImageCaptured: { - imageCaptured(image) + imageCaptured(preview) } onErrorOccurred: { errorOccurred(errorString) From 3ca846ee334a49d8f317be75346b0a6d815776ff Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Tue, 11 Nov 2025 22:24:09 +0100 Subject: [PATCH 13/17] fix image capture handling --- qml/content/CameraRenderer.qml | 2 +- qml/content/CameraSource.qml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qml/content/CameraRenderer.qml b/qml/content/CameraRenderer.qml index 601e3c7f..301a2060 100644 --- a/qml/content/CameraRenderer.qml +++ b/qml/content/CameraRenderer.qml @@ -56,7 +56,7 @@ Item { onImageCaptured: function(image) { whiteOverlay.state = "released" renderer.state = "store" - console.log("Captured") + console.log("Captured: " + image) console.log(applicationSettings.foldername.toString()) var path = applicationSettings.foldername.toString() diff --git a/qml/content/CameraSource.qml b/qml/content/CameraSource.qml index 72e9af34..b3b07f84 100644 --- a/qml/content/CameraSource.qml +++ b/qml/content/CameraSource.qml @@ -133,9 +133,9 @@ Item imageCapture: ImageCapture { id: imageCapture - onImageCaptured: + onImageCaptured: function(requestId, preview) { - imageCaptured(preview) + cameraSource.imageCaptured(preview) } onErrorOccurred: { errorOccurred(errorString) From 42319ae150e374fcb87906bd05359d2e838a2e6c Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Tue, 11 Nov 2025 22:48:04 +0100 Subject: [PATCH 14/17] log camera errors in CameraRenderer --- qml/content/CameraRenderer.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/qml/content/CameraRenderer.qml b/qml/content/CameraRenderer.qml index 301a2060..5b25341c 100644 --- a/qml/content/CameraRenderer.qml +++ b/qml/content/CameraRenderer.qml @@ -74,6 +74,7 @@ Item { onErrorOccurred: function(errorString) { renderer.state = "preview" + console.log("Camera error: " + errorString) failed() } } From 8deaaf4edb1ae9224f6707282e5071e4ee45b227 Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Tue, 11 Nov 2025 22:49:09 +0100 Subject: [PATCH 15/17] improve readyForCapture logic and enhance error handling --- qml/content/CameraSource.qml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/qml/content/CameraSource.qml b/qml/content/CameraSource.qml index b3b07f84..aa61b1b8 100644 --- a/qml/content/CameraSource.qml +++ b/qml/content/CameraSource.qml @@ -11,7 +11,7 @@ Item property alias output: output property string cameraName: "" - property alias readyForCapture: cameraSession.imageCapture.readyForCapture + property bool readyForCapture: ((cameraSession.imageCapture.readyForCapture) || (cameraSource.state === "GPhotoCamera")) signal imageCaptured(var image) signal errorOccurred(var errorString) @@ -76,10 +76,12 @@ Item { if(state === "StandardCamera") { + console.log("Standard camera capture") cameraSession.imageCapture.capture() } else if(state === "GPhotoCamera") { + console.log("GPhoto capture") gphotoCamera.captureImage() } else @@ -96,13 +98,13 @@ Item id: gphotoCamera onErrorOccurred: function(errorString) { - errorOccured(errorString) + cameraSource.errorOccurred(errorString) } onImageCaptured: function(image) { - imageCaptured(image) + cameraSource.imageCaptured(image) } onCaptureError: function(errorString) { - errorOccured(errorString) + cameraSource.errorOccurred(errorString) } } @@ -138,7 +140,7 @@ Item cameraSource.imageCaptured(preview) } onErrorOccurred: { - errorOccurred(errorString) + cameraSource.errorOccurred(errorString) } } } From 1095e47f145f5b4737e3bb16bc9cd08679da304c Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Wed, 12 Nov 2025 22:44:55 +0100 Subject: [PATCH 16/17] add keepalive timer in order to keep camera from switching into standby --- src/gphotocamera.cpp | 254 +++++++++++++++++++++++++++++++++++++++---- src/gphotocamera.h | 10 +- 2 files changed, 243 insertions(+), 21 deletions(-) diff --git a/src/gphotocamera.cpp b/src/gphotocamera.cpp index d83ee049..fc6410a9 100644 --- a/src/gphotocamera.cpp +++ b/src/gphotocamera.cpp @@ -83,9 +83,8 @@ GPhotoCameraWorker::GPhotoCameraWorker() gp_abilities_list_new(&caList); mAbilitiesList.reset(caList); - mCaptureTimer.setInterval(1000 * 60); // Check every minute - connect(&mCaptureTimer, &QTimer::timeout, this, - &GPhotoCameraWorker::checkCaptureParameter); + mKeepAliveTimer.setInterval(1000 * 60); // Check every minute + mKeepAliveTimer.setSingleShot(true); } GPhotoCameraWorker::~GPhotoCameraWorker() {} @@ -147,8 +146,8 @@ void GPhotoCameraWorker::startCamera(const QString &cameraName) { mCameraStarted = true; - if (!mCaptureTimer.isActive()) { - mCaptureTimer.start(); + if (!mKeepAliveTimer.isActive()) { + QMetaObject::invokeMethod(&mKeepAliveTimer, "start", Qt::QueuedConnection); } getPreviewFrame(); @@ -159,10 +158,6 @@ void GPhotoCameraWorker::stopCamera() { return; } - if (mCaptureTimer.isActive()) { - mCaptureTimer.stop(); - } - // Reset capturing fail counter mCapturingFailCount = 0; @@ -258,6 +253,11 @@ void GPhotoCameraWorker::getPreviewFrame() { return; } else { + if(!mKeepAliveTimer.isActive()) { + checkCaptureParameter(); + QMetaObject::invokeMethod(&mKeepAliveTimer, "start", Qt::QueuedConnection); + } + gp_file_clean(mPreviewFile.get()); auto ret = gp_camera_capture_preview(mCamera.get(), mPreviewFile.get(), @@ -295,17 +295,233 @@ void GPhotoCameraWorker::checkCaptureParameter() { return; } - CameraWidget *widget = nullptr; - int ret = gp_camera_get_config(mCamera.get(), &widget, mContext.get()); - if (ret >= GP_OK) { - CameraWidget *child = nullptr; - ret = gp_widget_get_child_by_name(widget, "capture", &child); - if (ret >= GP_OK) { - ret = gp_widget_set_value(child, 1); // Set capture to true - if (ret >= GP_OK) { - gp_camera_set_config(mCamera.get(), widget, mContext.get()); + QVariant captureParameter = parameter("capture"); + + if (captureParameter.isValid()) + { + if (captureParameter.toBool() == false) + { + setParameter("capture", true); + qWarning() << "Set parameter 'capture' to 'true'"; + } + } +} + +void GPhotoCameraWorker::waitForOperationCompleted() { + CameraEventType type; + void *data; + int ret; + + do { + ret = gp_camera_wait_for_event(mCamera.get(), 10, &type, &data, mContext.get()); + } while ((ret == GP_OK) && (type != GP_EVENT_TIMEOUT)); +} + +QVariant GPhotoCameraWorker::parameter(const QString &name) { + CameraWidget *root; + int ret = gp_camera_get_config(mCamera.get(), &root, mContext.get()); + if (ret < GP_OK) { + qWarning() << "Unable to get root option from gphoto"; + return QVariant(); + } + + CameraWidget *option; + ret = gp_widget_get_child_by_name(root, qPrintable(name), &option); + if (ret < GP_OK) { + qWarning() << "Unable to get config widget from gphoto"; + return QVariant(); + } + + CameraWidgetType type; + ret = gp_widget_get_type(option, &type); + if (ret < GP_OK) { + qWarning() << "Unable to get config widget type from gphoto"; + return QVariant(); + } + + if (type == GP_WIDGET_RADIO) { + char *value; + ret = gp_widget_get_value(option, &value); + if (ret < GP_OK) { + qWarning() << "Unable to get value for option" << qPrintable(name) + << "from gphoto"; + return QVariant(); + } else { + return QString::fromLocal8Bit(value); + } + } else if (type == GP_WIDGET_TOGGLE) { + int value; + ret = gp_widget_get_value(option, &value); + if (ret < GP_OK) { + qWarning() << "Unable to get value for option" << qPrintable(name) + << "from gphoto"; + return QVariant(); + } else { + return value == 0 ? false : true; + } + } else { + qWarning() << "Options of type" << type << "are currently not supported"; + } + + return QVariant(); +} + +bool GPhotoCameraWorker::setParameter(const QString &name, + const QVariant &value) { + CameraWidget *root; + int ret = gp_camera_get_config(mCamera.get(), &root, mContext.get()); + if (ret < GP_OK) { + qWarning() << "Unable to get root option from gphoto"; + return false; + } + + // Get widget pointer + CameraWidget *option; + ret = gp_widget_get_child_by_name(root, qPrintable(name), &option); + if (ret < GP_OK) { + qWarning() << "Unable to get option" << qPrintable(name) << "from gphoto"; + return false; + } + + // Get option type + CameraWidgetType type; + ret = gp_widget_get_type(option, &type); + if (ret < GP_OK) { + qWarning() << "Unable to get option type from gphoto"; + gp_widget_free(option); + return false; + } + + if (type == GP_WIDGET_RADIO) { + if (value.type() == QVariant::String) { + // String, need no conversion + ret = gp_widget_set_value(option, qPrintable(value.toString())); + + if (ret < GP_OK) { + qWarning() << "Failed to set value" << value << "to" << name + << "option:" << ret; + return false; + } + + ret = gp_camera_set_config(mCamera.get(), root, mContext.get()); + + if (ret < GP_OK) { + qWarning() << "Failed to set config to camera"; + return false; } + + waitForOperationCompleted(); + return true; + } else if (value.type() == QVariant::Double) { + // Trying to find nearest possible value (with the distance of 0.1) and + // set it to property + double v = value.toDouble(); + + int count = gp_widget_count_choices(option); + for (int i = 0; i < count; ++i) { + const char *choice; + gp_widget_get_choice(option, i, &choice); + + // We use a workaround for flawed russian i18n of gphoto2 strings + bool ok; + double choiceValue = + QString::fromLocal8Bit(choice).replace(',', '.').toDouble(&ok); + if (!ok) { + qDebug() << "Failed to convert value" << choice << "to double"; + continue; + } + + if (qAbs(choiceValue - v) < 0.1) { + ret = gp_widget_set_value(option, choice); + if (ret < GP_OK) { + qWarning() << "Failed to set value" << choice << "to" << name + << "option:" << ret; + return false; + } + + ret = gp_camera_set_config(mCamera.get(), root, mContext.get()); + if (ret < GP_OK) { + qWarning() << "Failed to set config to camera"; + return false; + } + + waitForOperationCompleted(); + return true; + } + } + + qWarning() << "Can't find value matching to" << v << "for option" << name; + return false; + } else if (value.type() == QVariant::Int) { + // Little hacks for 'ISO' option: if the value is -1, we pick the first + // non-integer value we found and set it as a parameter + int v = value.toInt(); + + int count = gp_widget_count_choices(option); + for (int i = 0; i < count; ++i) { + const char *choice; + gp_widget_get_choice(option, i, &choice); + + bool ok; + int choiceValue = QString::fromLocal8Bit(choice).toInt(&ok); + + if ((ok && choiceValue == v) || (!ok && v == -1)) { + ret = gp_widget_set_value(option, choice); + if (ret < GP_OK) { + qWarning() << "Failed to set value" << choice << "to" << name + << "option:" << ret; + return false; + } + + ret = gp_camera_set_config(mCamera.get(), root, mContext.get()); + if (ret < GP_OK) { + qWarning() << "Failed to set config to camera"; + return false; + } + + waitForOperationCompleted(); + return true; + } + } + + qWarning() << "Can't find value matching to" << v << "for option" << name; + return false; + } else { + qWarning() << "Failed to set value" << value << "to" << name + << "option. Type" << value.type() << "is not supported"; + gp_widget_free(option); + return false; + } + } else if (type == GP_WIDGET_TOGGLE) { + int v = 0; + if (value.canConvert()) { + v = value.toInt(); + } else { + qWarning() << "Failed to set value" << value << "to" << name + << "option. Type" << value.type() << "is not supported"; + gp_widget_free(option); + return false; } - gp_widget_free(widget); + + ret = gp_widget_set_value(option, &v); + if (ret < GP_OK) { + qWarning() << "Failed to set value" << v << "to" << name + << "option:" << ret; + return false; + } + + ret = gp_camera_set_config(mCamera.get(), root, mContext.get()); + if (ret < GP_OK) { + qWarning() << "Failed to set config to camera"; + return false; + } + + waitForOperationCompleted(); + return true; + } else { + qWarning() << "Options of type" << type << "are currently not supported"; } + + gp_widget_free(option); + return false; } diff --git a/src/gphotocamera.h b/src/gphotocamera.h index 246c56e9..de18ed18 100644 --- a/src/gphotocamera.h +++ b/src/gphotocamera.h @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #include #include @@ -73,7 +76,7 @@ public slots: void imageCaptured(const QImage &image); void captureError(const QString &error); protected: - QTimer mCaptureTimer; + QTimer mKeepAliveTimer; GPContextPtr mContext; GPPortInfoListPtr mPortInfoList; CameraAbilitiesListPtr mAbilitiesList; @@ -81,7 +84,10 @@ public slots: CameraFilePtr mPreviewFile; bool mCameraStarted = false; uint32_t mCapturingFailCount = 0; - // Add private members for camera handling + + void waitForOperationCompleted(); + QVariant parameter(const QString &name); + bool setParameter(const QString &name, const QVariant &value); protected slots: // check and set capture parameters to keep camera alive void checkCaptureParameter(); From 8bf7914ea85560a5ce6c77853aa895b0bc084619 Mon Sep 17 00:00:00 2001 From: Timm Eversmeyer Date: Wed, 12 Nov 2025 22:52:58 +0100 Subject: [PATCH 17/17] fetch submodules --- .github/workflows/flatpak.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index e2202937..23ad7add 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -21,6 +21,10 @@ jobs: runs-on: ${{ matrix.variant.runner }} steps: - uses: actions/checkout@v4 + with: + submodules: recursive # fetch submodules (recursively) + fetch-depth: 0 # ensure full history so submodule refs are resolvable + token: ${{ secrets.GITHUB_TOKEN }} - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: bundle: photobooth.flatpak