[fractal/fractal-next] qr-code-scanner: Use a singleton to access the Camera



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]