[fractal/fractal-next] qr-code-scanner: Use a singleton to access the Camera
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] qr-code-scanner: Use a singleton to access the Camera
- Date: Wed, 2 Feb 2022 17:19:30 +0000 (UTC)
commit d2f8f105ece42f082ed42aea1a16742d560442c4
Author: Julian Sparber <julian sparber net>
Date: Wed Feb 2 11:15:42 2022 +0100
qr-code-scanner: Use a singleton to access the Camera
This also adds a no-camera page.
data/resources/ui/qr-code-scanner.ui | 42 +++++++-
po/POTFILES.in | 1 +
src/contrib/mod.rs | 2 +-
src/contrib/qr_code_scanner/camera.rs | 118 ++++++++++++++++++++
src/contrib/qr_code_scanner/camera_paintable.rs | 46 ++++----
src/contrib/qr_code_scanner/mod.rs | 120 +++++----------------
.../verification/identity_verification_widget.rs | 26 ++---
src/utils.rs | 22 ++++
8 files changed, 240 insertions(+), 137 deletions(-)
---
diff --git a/data/resources/ui/qr-code-scanner.ui b/data/resources/ui/qr-code-scanner.ui
index 668f889d3..be92a2b8c 100644
--- a/data/resources/ui/qr-code-scanner.ui
+++ b/data/resources/ui/qr-code-scanner.ui
@@ -1,11 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="QrCodeScanner" parent="AdwBin">
- <property name="child">
- <object class="GtkPicture" id="picture">
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="transition-type">crossfade</property>
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">no-camera</property>
+ <property name="child">
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">camera-hardware-disabled-symbolic</property>
+ <property name="pixel-size">148</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Unable to connect to Camera</property>
+ </object>
+ </child>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">camera</property>
+ <property name="child">
+ <object class="GtkPicture" id="picture">
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ </object>
+ </property>
+ </object>
+ </child>
</object>
- </property>
+ </child>
</template>
</interface>
+
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 39576bb61..a32604cbd 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -33,6 +33,7 @@ data/resources/ui/session-verification.ui
data/resources/ui/shortcuts.ui
data/resources/ui/sidebar-room-row.ui
data/resources/ui/sidebar.ui
+data/resources/ui/qr-code-scanner.ui
# Rust files
src/application.rs
diff --git a/src/contrib/mod.rs b/src/contrib/mod.rs
index f6b74ebd0..a4548336d 100644
--- a/src/contrib/mod.rs
+++ b/src/contrib/mod.rs
@@ -3,5 +3,5 @@ mod qr_code_scanner;
pub use self::{
qr_code::{QRCode, QRCodeExt},
- qr_code_scanner::{screenshot, QrCodeScanner},
+ qr_code_scanner::{Camera, screenshot, QrCodeScanner},
};
diff --git a/src/contrib/qr_code_scanner/camera.rs b/src/contrib/qr_code_scanner/camera.rs
new file mode 100644
index 000000000..3941179ad
--- /dev/null
+++ b/src/contrib/qr_code_scanner/camera.rs
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+use std::{os::unix::prelude::RawFd, time::Duration};
+
+use ashpd::{desktop::camera, zbus};
+use gtk::{glib, subclass::prelude::*};
+use once_cell::sync::Lazy;
+use tokio::time::timeout;
+
+use super::camera_paintable::CameraPaintable;
+
+mod imp {
+ use std::sync::Arc;
+
+ use tokio::sync::OnceCell;
+
+ use super::*;
+
+ #[derive(Debug, Default)]
+ pub struct Camera {
+ pub connection: Arc<OnceCell<zbus::Connection>>,
+ pub paintable: glib::WeakRef<CameraPaintable>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Camera {
+ const NAME: &'static str = "Camera";
+ type Type = super::Camera;
+ type ParentType = glib::Object;
+ }
+
+ impl ObjectImpl for Camera {}
+}
+
+glib::wrapper! {
+ pub struct Camera(ObjectSubclass<imp::Camera>);
+}
+
+impl Camera {
+ /// Create a new `Camera`. You should consider using `Camera::default()` to
+ /// get a shared Object
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create a Camera")
+ }
+
+ async fn connection(&self) -> Result<zbus::Connection, ashpd::Error> {
+ let connection = self.imp().connection.clone();
+ Ok(connection
+ .get_or_try_init(zbus::Connection::session)
+ .await?
+ .clone())
+ }
+
+ async fn file_descriptor(&self) -> Result<(RawFd, Option<u32>), ashpd::Error> {
+ let proxy = camera::CameraProxy::new(&self.connection().await?).await?;
+ proxy.access_camera().await?;
+ let stream_fd = proxy.open_pipe_wire_remote().await?;
+ let node_id = camera::pipewire_node_id(stream_fd).await.ok();
+
+ Ok((stream_fd, node_id))
+ }
+
+ pub async fn has_camera(&self) -> Result<bool, ashpd::Error> {
+ let proxy = camera::CameraProxy::new(&self.connection().await?).await?;
+
+ if proxy.is_camera_present().await? {
+ // Apparently is-camera-present doesn't report the correct value:
https://github.com/flatpak/xdg-desktop-portal/issues/486#issuecomment-897636589
+ // We need to use the proper timeout based on the executer
+ if glib::MainContext::default().is_owner() {
+ Ok(
+ crate::utils::timeout_future(Duration::from_secs(1), self.file_descriptor())
+ .await
+ .is_ok(),
+ )
+ } else {
+ Ok(timeout(Duration::from_secs(1), self.file_descriptor())
+ .await
+ .is_ok())
+ }
+ } else {
+ Ok(false)
+ }
+ }
+
+ /// Get the a `gdk::Paintable` displaying the content of a camera
+ /// This will panic if not called from the `MainContext` gtk is running on
+ pub async fn paintable(&self) -> Option<CameraPaintable> {
+ // We need to make sure that the Paintable is taken only from the MainContext
+ assert!(glib::MainContext::default().is_owner());
+
+ crate::utils::timeout_future(Duration::from_secs(1), self.paintable_internal())
+ .await
+ .ok()?
+ }
+
+ async fn paintable_internal(&self) -> Option<CameraPaintable> {
+ if let Some(paintable) = self.imp().paintable.upgrade() {
+ Some(paintable)
+ } else if let Ok((stream_fd, node_id)) = self.file_descriptor().await {
+ let paintable = CameraPaintable::new(stream_fd, node_id).await;
+ self.imp().paintable.set(Some(&paintable));
+ Some(paintable)
+ } else {
+ None
+ }
+ }
+}
+
+impl Default for Camera {
+ fn default() -> Self {
+ static CAMERA: Lazy<Camera> =
+ Lazy::new(|| glib::Object::new(&[]).expect("Failed to create a Camera"));
+
+ CAMERA.to_owned()
+ }
+}
+
+unsafe impl Send for Camera {}
+unsafe impl Sync for Camera {}
diff --git a/src/contrib/qr_code_scanner/camera_paintable.rs b/src/contrib/qr_code_scanner/camera_paintable.rs
index c8b927988..462e970df 100644
--- a/src/contrib/qr_code_scanner/camera_paintable.rs
+++ b/src/contrib/qr_code_scanner/camera_paintable.rs
@@ -10,6 +10,7 @@
// queue -- videoconvert -- gst paintable sink
use std::{
+ cell::Cell,
os::unix::io::AsRawFd,
sync::{Arc, Mutex},
};
@@ -54,7 +55,7 @@ mod imp {
impl ObjectImpl for CameraPaintable {
fn dispose(&self, paintable: &Self::Type) {
- paintable.close_pipeline();
+ paintable.set_pipeline(None);
}
fn signals() -> &'static [subclass::Signal] {
@@ -120,11 +121,6 @@ mod imp {
snapshot.translate(&p);
image.snapshot(snapshot.upcast_ref(), new_width, new_height);
- } else {
- snapshot.append_color(
- &gdk::RGBA::BLACK,
- &graphene::Rect::new(0f32, 0f32, width as f32, height as f32),
- );
}
}
}
@@ -134,21 +130,24 @@ glib::wrapper! {
pub struct CameraPaintable(ObjectSubclass<imp::CameraPaintable>) @implements gdk::Paintable;
}
-impl Default for CameraPaintable {
- fn default() -> Self {
- glib::Object::new(&[]).expect("Failed to create a CameraPaintable")
+impl CameraPaintable {
+ pub async fn new<F: AsRawFd>(fd: F, node_id: Option<u32>) -> Self {
+ let self_: Self = glib::Object::new(&[]).expect("Failed to create a CameraPaintable");
+
+ self_.set_pipewire_fd(fd, node_id).await;
+ self_
}
-}
-impl CameraPaintable {
- pub fn set_pipewire_fd<F: AsRawFd>(&self, fd: F, node_id: u32) {
+ async fn set_pipewire_fd<F: AsRawFd>(&self, fd: F, node_id: Option<u32>) {
// Make sure that the previous pipeline is closed so that we can be sure that it
// doesn't use the webcam
- self.close_pipeline();
+ self.set_pipeline(None);
let pipewire_src = gst::ElementFactory::make("pipewiresrc", None).unwrap();
pipewire_src.set_property("fd", &fd.as_raw_fd());
- pipewire_src.set_property("path", &node_id.to_string());
+ if let Some(node_id) = node_id {
+ pipewire_src.set_property("path", &node_id.to_string());
+ }
let pipeline = gst::Pipeline::new(None);
let detector = QrCodeDetector::new(self.create_sender()).upcast();
@@ -208,9 +207,22 @@ impl CameraPaintable {
)
.expect("Failed to add bus watch");
- self.set_sink_paintable(sink.property::<gdk::Paintable>("paintable"));
+ let paintable = sink.property::<gdk::Paintable>("paintable");
+
+ // Workaround: we wait for the first frame so that we don't show a black frame
+ let (sender, receiver) = futures::channel::oneshot::channel();
+ let sender = Cell::new(Some(sender));
+
+ paintable.connect_invalidate_contents(move |_| {
+ if let Some(sender) = sender.take() {
+ sender.send(()).unwrap();
+ }
+ });
+
+ self.set_sink_paintable(paintable);
pipeline.set_state(gst::State::Playing).unwrap();
self.set_pipeline(Some(pipeline));
+ receiver.await.unwrap();
}
fn set_sink_paintable(&self, paintable: gdk::Paintable) {
@@ -244,10 +256,6 @@ impl CameraPaintable {
priv_.pipeline.replace(pipeline);
}
- pub fn close_pipeline(&self) {
- self.set_pipeline(None);
- }
-
fn create_sender(&self) -> glib::Sender<Action> {
let (sender, receiver) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
diff --git a/src/contrib/qr_code_scanner/mod.rs b/src/contrib/qr_code_scanner/mod.rs
index 72db9c414..75978b1d9 100644
--- a/src/contrib/qr_code_scanner/mod.rs
+++ b/src/contrib/qr_code_scanner/mod.rs
@@ -1,37 +1,31 @@
// SPDX-License-Identifier: GPL-3.0-or-later
-use std::os::unix::prelude::RawFd;
-
-use ashpd::{desktop::camera, zbus};
-use glib::{clone, subclass};
-use gtk::{glib, prelude::*, subclass::prelude::*};
+use gtk::{gdk, glib, glib::subclass, prelude::*, subclass::prelude::*};
use matrix_sdk::encryption::verification::QrVerificationData;
-use crate::spawn;
-
+mod camera;
mod camera_paintable;
mod qr_code_detector;
pub mod screenshot;
-use camera_paintable::CameraPaintable;
+pub use camera::Camera;
mod imp {
- use std::cell::Cell;
+ use std::cell::RefCell;
use adw::subclass::prelude::*;
use gtk::CompositeTemplate;
use once_cell::sync::Lazy;
- use tokio::sync::OnceCell;
use super::*;
#[derive(Debug, CompositeTemplate, Default)]
#[template(resource = "/org/gnome/FractalNext/qr-code-scanner.ui")]
pub struct QrCodeScanner {
- pub paintable: CameraPaintable,
+ #[template_child]
+ pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub picture: TemplateChild<gtk::Picture>,
- pub connection: OnceCell<zbus::Connection>,
- pub has_camera: Cell<bool>,
+ pub handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
@@ -49,41 +43,6 @@ mod imp {
}
}
impl ObjectImpl for QrCodeScanner {
- fn properties() -> &'static [glib::ParamSpec] {
- static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
- vec![glib::ParamSpecBoolean::new(
- "has-camera",
- "Has Camera",
- "Whether we have a working camera",
- false,
- glib::ParamFlags::READABLE,
- )]
- });
-
- PROPERTIES.as_ref()
- }
-
- fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
- match pspec.name() {
- "has-camera" => obj.has_camera().to_value(),
- _ => unimplemented!(),
- }
- }
-
- fn constructed(&self, obj: &Self::Type) {
- self.picture.set_paintable(Some(&self.paintable));
-
- let callback = glib::clone!(@weak obj => @default-return None, move |args: &[glib::Value]| {
- let code = args.get(1).unwrap().get::<QrVerificationDataBoxed>().unwrap();
- obj.emit_by_name::<()>("code-detected", &[&code]);
-
- None
- });
- self.paintable
- .connect_local("code-detected", false, callback);
- obj.init_has_camera();
- }
-
fn signals() -> &'static [subclass::Signal] {
static SIGNALS: Lazy<Vec<subclass::Signal>> = Lazy::new(|| {
vec![subclass::Signal::builder(
@@ -116,64 +75,41 @@ impl QrCodeScanner {
glib::Object::new(&[]).expect("Failed to create a QrCodeScanner")
}
- async fn connection(&self) -> Result<&zbus::Connection, ashpd::Error> {
- Ok(self
- .imp()
- .connection
- .get_or_try_init(zbus::Connection::session)
- .await?)
- }
-
pub fn stop(&self) {
- self.imp().paintable.close_pipeline();
- }
+ let priv_ = self.imp();
- pub async fn start(&self) -> bool {
- if let Ok(stream_fd) = self.stream().await {
- if let Ok(node_id) = camera::pipewire_node_id(stream_fd).await {
- self.imp().paintable.set_pipewire_fd(stream_fd, node_id);
- self.set_has_camera(true);
- return true;
+ if let Some(paintable) = priv_.picture.paintable() {
+ priv_.picture.set_paintable(gdk::Paintable::NONE);
+ if let Some(handler) = priv_.handler.take() {
+ paintable.disconnect(handler);
}
}
-
- self.set_has_camera(false);
- false
}
- async fn has_camera_internal(&self) -> Result<bool, ashpd::Error> {
- let proxy = camera::CameraProxy::new(self.connection().await?).await?;
-
- proxy.is_camera_present().await
- }
+ pub async fn start(&self) {
+ let priv_ = self.imp();
+ let camera = camera::Camera::default();
- async fn stream(&self) -> Result<RawFd, ashpd::Error> {
- let proxy = camera::CameraProxy::new(self.connection().await?).await?;
+ if let Some(paintable) = camera.paintable().await {
+ self.stop();
- proxy.access_camera().await?;
- proxy.open_pipe_wire_remote().await
- }
+ priv_.picture.set_paintable(Some(&paintable));
- fn init_has_camera(&self) {
- spawn!(clone!(@weak self as obj => async move {
- obj.set_has_camera(obj.has_camera_internal().await.unwrap_or_default());
- }));
- }
+ let callback = glib::clone!(@weak self as obj => @default-return None, move |args:
&[glib::Value]| {
+ let code = args.get(1).unwrap().get::<QrVerificationDataBoxed>().unwrap();
+ obj.emit_by_name::<()>("code-detected", &[&code]);
- pub fn has_camera(&self) -> bool {
- self.imp().has_camera.get()
- }
+ None
+ });
+ let handler = paintable.connect_local("code-detected", false, callback);
- fn set_has_camera(&self, has_camera: bool) {
- if has_camera == self.has_camera() {
- return;
+ priv_.handler.replace(Some(handler));
+ priv_.stack.set_visible_child_name("camera");
+ } else {
+ priv_.stack.set_visible_child_name("no-camera");
}
-
- self.imp().has_camera.set(has_camera);
- self.notify("has-camera");
}
- /// Connects the prepared signals to the function f given in input
pub fn connect_code_detected<F: Fn(&Self, QrVerificationData) + 'static>(
&self,
f: F,
diff --git a/src/session/content/verification/identity_verification_widget.rs
b/src/session/content/verification/identity_verification_widget.rs
index 7e5f61bf7..5c62ca2f4 100644
--- a/src/session/content/verification/identity_verification_widget.rs
+++ b/src/session/content/verification/identity_verification_widget.rs
@@ -209,11 +209,7 @@ mod imp {
let priv_ = obj.imp();
button.set_loading(true);
priv_.start_emoji_btn.set_sensitive(false);
- if priv_.qr_code_scanner.has_camera() {
- obj.start_scanning();
- } else {
- obj.take_screenshot();
- }
+ obj.start_scanning();
}));
self.take_screenshot_btn2
@@ -447,11 +443,8 @@ impl IdentityVerificationWidget {
fn start_scanning(&self) {
spawn!(clone!(@weak self as obj => async move {
let priv_ = obj.imp();
- if priv_.qr_code_scanner.start().await {
- priv_.main_stack.set_visible_child_name("scan-qr-code");
- } else {
- priv_.main_stack.set_visible_child_name("no-camera");
- }
+ priv_.qr_code_scanner.start().await;
+ priv_.main_stack.set_visible_child_name("scan-qr-code");
}));
}
@@ -476,16 +469,9 @@ impl IdentityVerificationWidget {
}
fn update_camera_state(&self) {
- let priv_ = self.imp();
- if priv_.qr_code_scanner.has_camera() {
- priv_
- .scan_qr_code_btn
- .set_label(&gettext("Scan QR code with this session"))
- } else {
- priv_
- .scan_qr_code_btn
- .set_label(&gettext("Take a Screenshot of a Qr Code"))
- }
+ self.imp()
+ .scan_qr_code_btn
+ .set_label(&gettext("Scan QR code with this session"))
}
fn init_mode(&self) {
diff --git a/src/utils.rs b/src/utils.rs
index 1d0b40924..1ff7e3860 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -214,3 +214,25 @@ pub fn pending_event_ids() -> (Uuid, Box<EventId>) {
let event_id = EventId::parse(format!("${}:fractal.gnome.org", txn_id)).unwrap();
(txn_id, event_id)
}
+
+pub enum TimeoutFuture {
+ Timeout,
+}
+
+use futures::{
+ future::{self, Either, Future},
+ pin_mut,
+};
+
+pub async fn timeout_future<T>(
+ timeout: std::time::Duration,
+ fut: impl Future<Output = T>,
+) -> Result<T, TimeoutFuture> {
+ let timeout = glib::timeout_future(timeout);
+ pin_mut!(fut);
+
+ match future::select(fut, timeout).await {
+ Either::Left((x, _)) => Ok(x),
+ _ => Err(TimeoutFuture::Timeout),
+ }
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]