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");