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 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 b7d7d97e..d10ee6fb 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.32", + "commit": "de1f0617b1ffa39c1980d1306343709c7dc0120e", + "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/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/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 73974e77..732a0eb1 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,26 +26,22 @@ 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; } - MediaDevices - { - id: mediaDevices + GPhotoCamera { + id: gphotoCamera } - function findDeviceId(cameraName) + MediaDevices { - 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 + id: mediaDevices } Component.onCompleted: 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: [{ diff --git a/qml/content/CameraRenderer.qml b/qml/content/CameraRenderer.qml index b3991213..5b25341c 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 @@ -13,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 @@ -65,83 +47,42 @@ Item { } } - 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: " + image) + + 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" + console.log("Camera error: " + errorString) + failed() + } } + ReplaceBackgroundVideoFilter { id: backgroundFilter - videoSink: output.videoSink + videoSink: cameraSource.output.videoSink background: backgroundImage onCaptureProcessingFinished: fileName => @@ -155,14 +96,6 @@ Item { } } - Connections { - id: cameraErrorListener - target: camera - function errorOccured(_, errorString) { - console.log("Camera Error: " + errorString) - } - } - VideoOutput { id: output @@ -222,31 +155,10 @@ Item { height: output.height } - /* 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() + cameraSource.captureImage() } else { renderer.state = "preview" failed() @@ -279,7 +191,7 @@ Item { } StateChangeScript { script: { - camera.start() + cameraSource.start() } } }, @@ -310,7 +222,7 @@ Item { } ScriptAction { script: { - camera.stop() + cameraSource.stop() } } } diff --git a/qml/content/CameraSource.qml b/qml/content/CameraSource.qml new file mode 100644 index 00000000..aa61b1b8 --- /dev/null +++ b/qml/content/CameraSource.qml @@ -0,0 +1,173 @@ +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 bool readyForCapture: ((cameraSession.imageCapture.readyForCapture) || (cameraSource.state === "GPhotoCamera")) + + 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.cameraName = cameraName + console.log("CameraSource using GPhoto camera device: " + cameraName) + return + } + } + console.log("CameraSource could not find camera device: " + cameraName) + 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") + { + console.log("Standard camera capture") + cameraSession.imageCapture.capture() + } + else if(state === "GPhotoCamera") + { + console.log("GPhoto capture") + gphotoCamera.captureImage() + } + else + { + console.log("No camera available to capture image!") + } + } + + MediaDevices { + id: mediaDevices + } + + GPhotoCamera { + id: gphotoCamera + + onErrorOccurred: function(errorString) { + cameraSource.errorOccurred(errorString) + } + onImageCaptured: function(image) { + cameraSource.imageCaptured(image) + } + onCaptureError: function(errorString) { + cameraSource.errorOccurred(errorString) + } + } + + Camera { + id: systemCamera + cameraDevice: mediaDevices.defaultVideoInput + exposureMode: Camera.ExposurePortrait + exposureCompensation: -1.0 + whiteBalanceMode: Camera.WhiteBalanceAuto + flashMode: Camera.FlashAuto + torchMode: Camera.TorchAuto + } + + Connections { + id: cameraErrorListener + target: systemCamera + function errorOccured(_, errorString) { + console.log("Camera Error: " + errorString) + } + } + + CaptureSession { + + id: cameraSession + + videoOutput: output + + imageCapture: ImageCapture { + id: imageCapture + + onImageCaptured: function(requestId, preview) + { + cameraSource.imageCaptured(preview) + } + onErrorOccurred: { + cameraSource.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 + } + } + ] +} diff --git a/qtbooth.pro b/qtbooth.pro index f27892ae..0a1fcd09 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 -lgphoto2_port !isEmpty(PREFIX) { INSTALLS += target 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 diff --git a/src/gphotocamera.cpp b/src/gphotocamera.cpp new file mode 100644 index 00000000..fc6410a9 --- /dev/null +++ b/src/gphotocamera.cpp @@ -0,0 +1,527 @@ +#include "gphotocamera.h" +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr auto capturingFailLimit = 10; +} + +GPhotoCameraDevice::GPhotoCameraDevice() : mWorker(new GPhotoCameraWorker()) { + mWorker->moveToThread(&mWorkerThread); + + connect(this, &QVideoFrameInput::readyToSendVideoFrame, mWorker.get(), + &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); + + 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::captureImage() const { + QMetaObject::invokeMethod(mWorker.get(), "captureImage", + Qt::QueuedConnection); +} + +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); +} + +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); + + mKeepAliveTimer.setInterval(1000 * 60); // Check every minute + mKeepAliveTimer.setSingleShot(true); +} +GPhotoCameraWorker::~GPhotoCameraWorker() {} + +void GPhotoCameraWorker::startCamera(const QString &cameraName) { + 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; + + if (!mKeepAliveTimer.isActive()) { + QMetaObject::invokeMethod(&mKeepAliveTimer, "start", Qt::QueuedConnection); + } + + getPreviewFrame(); +} + +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; +} + +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 { + QStringList cameraList; + + CameraList *list; + gp_list_new(&list); + gp_camera_autodetect(list, mContext.get()); + + 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); + + return cameraList; +} + +void GPhotoCameraWorker::getPreviewFrame() { + if (!mCameraStarted) { + emit errorOccurred("Camera not started"); + 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(), + 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(); + } + } +} + +void GPhotoCameraWorker::checkCaptureParameter() { + // this function checks and sets the capture parameter to keep the camera + // alive + if (!mCameraStarted || !mCamera) { + return; + } + + 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; + } + + 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 new file mode 100644 index 00000000..de18ed18 --- /dev/null +++ b/src/gphotocamera.h @@ -0,0 +1,96 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + + +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 +{ + Q_OBJECT + Q_PROPERTY(QString cameraName READ getCameraName WRITE setCameraName) +public: + GPhotoCameraDevice(); + ~GPhotoCameraDevice() override; + + Q_INVOKABLE QStringList availableCameras() const; + + Q_INVOKABLE QString getDefautCamera() const; + + Q_INVOKABLE void captureImage() 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); + +protected: + // worker object for camera operations in thread + std::unique_ptr mWorker; + +signals: + void errorOccurred(const QString &error); + void imageCaptured(const QImage &image); + void captureError(const QString &error); +}; + +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); + void imageCaptured(const QImage &image); + void captureError(const QString &error); +protected: + QTimer mKeepAliveTimer; + GPContextPtr mContext; + GPPortInfoListPtr mPortInfoList; + CameraAbilitiesListPtr mAbilitiesList; + CameraPtr mCamera; + CameraFilePtr mPreviewFile; + bool mCameraStarted = false; + uint32_t mCapturingFailCount = 0; + + 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(); +}; + + 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");