From 80d8ea71a46cd3b1a2f283dd9d1635149987a7ad Mon Sep 17 00:00:00 2001 From: Bilal Elmoussaoui Date: Tue, 27 Dec 2022 14:07:10 +0100 Subject: [PATCH] Multi cameras support --- Cargo.lock | 1 + Cargo.toml | 2 +- data/resources.gresource.xml | 1 + .../resources/icons/video-camera-symbolic.svg | 2 + data/resources/ui/camera.ui | 22 ++- src/widgets/accounts/add.rs | 11 +- src/widgets/camera.rs | 171 ++++++++++++------ src/widgets/camera_paintable.rs | 44 ++--- src/widgets/camera_row.rs | 66 +++++++ src/widgets/mod.rs | 2 + 10 files changed, 232 insertions(+), 90 deletions(-) create mode 100644 data/resources/icons/video-camera-symbolic.svg create mode 100644 src/widgets/camera_row.rs diff --git a/Cargo.lock b/Cargo.lock index 649ae25..bdb8faf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,7 @@ dependencies = [ "rand", "serde", "serde_repr", + "tracing", "url", "zbus 3.6.2", ] diff --git a/Cargo.toml b/Cargo.toml index e7627ad..c4e3ca6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ version = "4.1.6" [dependencies] adw = {package = "libadwaita", version = "0.2", features = ["v1_2"]} anyhow = "1.0" -ashpd = {version = "0.4.0-alpha.1", features = ["pipewire", "gtk4"]} +ashpd = {version = "0.4.0-alpha.1", features = ["pipewire", "gtk4", "tracing"]} binascii = "0.1" diesel = {version = "2.0", features = ["sqlite", "r2d2"]} diesel_migrations = {version = "2.0", features = ["sqlite"]} diff --git a/data/resources.gresource.xml b/data/resources.gresource.xml index 4522a15..daa6121 100644 --- a/data/resources.gresource.xml +++ b/data/resources.gresource.xml @@ -35,6 +35,7 @@ resources/icons/refresh-symbolic.svg resources/icons/copy-symbolic.svg resources/icons/link-symbolic.svg + resources/icons/video-camera-symbolic.svg resources/icons/provider-fallback.svg diff --git a/data/resources/icons/video-camera-symbolic.svg b/data/resources/icons/video-camera-symbolic.svg new file mode 100644 index 0000000..e25e823 --- /dev/null +++ b/data/resources/icons/video-camera-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/resources/ui/camera.ui b/data/resources/ui/camera.ui index 16bec4d..04a3a61 100644 --- a/data/resources/ui/camera.ui +++ b/data/resources/ui/camera.ui @@ -63,14 +63,30 @@ stream - - False + + + + False + + + + + video-camera-symbolic + end + end + 18 + 18 + + + - diff --git a/src/widgets/accounts/add.rs b/src/widgets/accounts/add.rs index 3ce1947..59996bb 100644 --- a/src/widgets/accounts/add.rs +++ b/src/widgets/accounts/add.rs @@ -201,9 +201,14 @@ impl AccountAddDialog { } #[template_callback] - fn camera_code_detected(&self, _camera: Camera, code: String) { - if let Ok(otp_uri) = OTPUri::from_str(&code) { - self.set_from_otp_uri(&otp_uri); + fn camera_code_detected(&self, code: String, _camera: Camera) { + match OTPUri::from_str(&code) { + Ok(otp_uri) => { + self.set_from_otp_uri(&otp_uri); + } + Err(err) => { + tracing::error!("Failed to parse OTP uri code {err}"); + } } } diff --git a/src/widgets/camera.rs b/src/widgets/camera.rs index 95da978..1499e57 100644 --- a/src/widgets/camera.rs +++ b/src/widgets/camera.rs @@ -1,27 +1,20 @@ -use std::{ - cell::{Cell, RefCell}, - os::unix::prelude::RawFd, -}; +use std::cell::RefCell; use adw::subclass::prelude::*; use anyhow::Result; use ashpd::desktop::screenshot::ScreenshotRequest; +use gettextrs::gettext; use gst::prelude::*; use gtk::{ gio, - glib::{ - self, clone, - subclass::{InitializingObject, Signal}, - Receiver, - }, + glib::{self, clone, Receiver}, prelude::*, - CompositeTemplate, }; use gtk_macros::spawn; use image::GenericImageView; use once_cell::sync::Lazy; -use zbar_rust::ZBarImageScanner; +use super::{CameraItem, CameraRow}; use crate::widgets::CameraPaintable; mod screenshot { @@ -34,7 +27,7 @@ mod screenshot { let (width, height) = img.dimensions(); let img_data: Vec = img.to_luma8().to_vec(); - let mut scanner = ZBarImageScanner::new(); + let mut scanner = zbar_rust::ZBarImageScanner::new(); let results = scanner .scan_y800(&img_data, width, height) @@ -62,18 +55,6 @@ mod screenshot { Ok(gio::File::for_uri(uri.as_str())) } - - pub async fn stream() -> Result)>> { - let proxy = ashpd::desktop::camera::Camera::new().await?; - if !proxy.is_present().await? { - return Ok(None); - } - proxy.request_access().await?; - - let stream_fd = proxy.open_pipe_wire_remote().await?; - let nodes_id = ashpd::desktop::camera::pipewire_streams(stream_fd).await?; - Ok(Some((stream_fd, nodes_id))) - } } #[derive(Debug)] @@ -89,14 +70,15 @@ pub enum CameraState { } mod imp { + use glib::subclass::{InitializingObject, Signal}; + use super::*; - #[derive(Debug, CompositeTemplate)] + #[derive(Debug, gtk::CompositeTemplate)] #[template(resource = "/com/belmoussaoui/Authenticator/camera.ui")] pub struct Camera { pub paintable: CameraPaintable, pub receiver: RefCell>>, - pub started: Cell, #[template_child] pub previous: TemplateChild, #[template_child] @@ -107,6 +89,10 @@ mod imp { pub spinner: TemplateChild, #[template_child] pub screenshot: TemplateChild, + #[template_child] + pub camera_selection_button: TemplateChild, + pub stream_list: gio::ListStore, + pub selection: gtk::SingleSelection, } #[glib::object_subclass] @@ -131,12 +117,14 @@ mod imp { Self { paintable: CameraPaintable::new(sender), receiver, - started: Cell::default(), previous: TemplateChild::default(), + camera_selection_button: TemplateChild::default(), spinner: TemplateChild::default(), stack: TemplateChild::default(), picture: TemplateChild::default(), screenshot: TemplateChild::default(), + stream_list: gio::ListStore::new(glib::BoxedAnyObject::static_type()), + selection: Default::default(), } } } @@ -159,6 +147,7 @@ mod imp { self.parent_constructed(); let obj = self.obj(); obj.setup_receiver(); + obj.setup_widget(); obj.set_state(CameraState::NotFound); self.picture.set_paintable(Some(&self.paintable)); } @@ -181,19 +170,15 @@ glib::wrapper! { impl Camera { pub fn start(&self) { let imp = self.imp(); - if !imp.started.get() { - imp.paintable.start(); - imp.started.set(true); - self.set_state(CameraState::Ready); - } + imp.paintable.start(); + self.set_state(CameraState::Ready); } pub fn stop(&self) { let imp = self.imp(); - if imp.started.get() { - imp.paintable.stop(); - imp.started.set(false); - } + imp.paintable.stop(); + imp.stream_list.remove_all(); + imp.selection.set_selected(gtk::INVALID_LIST_POSITION); } pub fn connect_close(&self, callback: F) -> glib::SignalHandlerId @@ -225,24 +210,42 @@ impl Camera { ) } - pub fn scan_from_camera(&self) { - if !self.imp().started.get() { - spawn!(clone!(@weak self as camera => async move { - match screenshot::stream().await { - Ok(Some((stream_fd, nodes_id))) => { - let node_id = nodes_id.get(0).map(|s| s.node_id()); - match camera.imp().paintable.set_pipewire_node_id(stream_fd, node_id) { - Ok(_) => camera.start(), - Err(err) => tracing::error!("Failed to start the camera stream {err}"), - }; - }, - Ok(None) => { - camera.set_state(CameraState::NotFound); - } - Err(e) => tracing::error!("Failed to stream {}", e), - } - })); + fn set_streams(&self, streams: Vec) { + let imp = self.imp(); + for stream in streams { + let default = gettext("Unknown Device"); + let nick = stream + .properties() + .get("node.nick") + .unwrap_or(&default) + .to_string(); + + let item = CameraItem { + nick, + node_id: stream.node_id(), + }; + imp.stream_list.append(&glib::BoxedAnyObject::new(item)); } + imp.selection.set_selected(0); + } + + pub fn scan_from_camera(&self) { + spawn!(clone!(@weak self as camera => async move { + match ashpd::desktop::camera::request().await { + Ok(Some((stream_fd, nodes_id))) => { + match camera.imp().paintable.set_pipewire_fd(stream_fd) { + Ok(_) => { + camera.set_streams(nodes_id); + }, + Err(err) => tracing::error!("Failed to start the camera stream {err}"), + }; + }, + Ok(None) => { + camera.set_state(CameraState::NotFound); + } + Err(e) => tracing::error!("Failed to stream {}", e), + } + })); } pub async fn scan_from_screenshot(&self) -> anyhow::Result<()> { @@ -295,17 +298,69 @@ impl Camera { ); } + fn setup_widget(&self) { + let imp = self.imp(); + let popover = gtk::Popover::new(); + popover.add_css_class("menu"); + + imp.selection.set_model(Some(&imp.stream_list)); + let factory = gtk::SignalListItemFactory::new(); + factory.connect_setup(|_, item| { + let camera_row = CameraRow::default(); + + item.downcast_ref::() + .unwrap() + .set_child(Some(&camera_row)); + }); + let selection = &imp.selection; + factory.connect_bind(glib::clone!(@weak selection => move |_, item| { + let item = item.clone().downcast::().unwrap(); + let child = item.child().unwrap(); + let row = child.downcast_ref::().unwrap(); + + let item = item.item().unwrap().downcast::().unwrap(); + let camera_item = item.borrow::(); + row.set_label(&camera_item.nick); + + selection.connect_selected_item_notify(glib::clone!(@weak row, @weak item => move |selection| { + if let Some(selected_item) = selection.selected_item() { + row.set_selected(selected_item == item); + } else { + row.set_selected(false); + } + })); + })); + let list_view = gtk::ListView::new(Some(&imp.selection), Some(&factory)); + popover.set_child(Some(&list_view)); + + imp.selection.connect_selected_item_notify(glib::clone!(@weak self as obj, @weak popover => move |selection| { + if let Some(selected_item) = selection.selected_item() { + let node_id = selected_item.downcast_ref::().unwrap().borrow::().node_id; + match obj.imp().paintable.set_pipewire_node_id(node_id) { + Ok(_) => { + obj.start(); + }, + Err(err) => { + tracing::error!("Failed to start a camera stream {err}"); + } + } + } + popover.popdown(); + })); + + imp.camera_selection_button.set_popover(Some(&popover)); + } + #[template_callback] fn on_previous_clicked(&self, _btn: gtk::Button) { self.emit_by_name::<()>("close", &[]); } #[template_callback] - fn on_screenshot_clicked(&self, _btn: gtk::Button) { - spawn!(clone!(@strong self as camera => async move { - // TODO: Error handling? - let _ = camera.scan_from_screenshot().await; - })); + async fn on_screenshot_clicked(&self, _btn: gtk::Button) { + if let Err(err) = self.scan_from_screenshot().await { + tracing::error!("Failed to scan from screenshot {err}"); + } } } diff --git a/src/widgets/camera_paintable.rs b/src/widgets/camera_paintable.rs index 3b85933..8b061fb 100644 --- a/src/widgets/camera_paintable.rs +++ b/src/widgets/camera_paintable.rs @@ -31,6 +31,7 @@ mod imp { pub struct CameraPaintable { pub sender: RefCell>>, pub pipeline: RefCell>, + pub pipewire_element: RefCell>, pub sink_paintable: RefCell>, } @@ -110,25 +111,25 @@ impl CameraPaintable { paintable } - pub fn set_pipewire_node_id( - &self, - fd: F, - node_id: Option, - ) -> anyhow::Result<()> { - let raw_fd = fd.as_raw_fd(); - let pipewire_element = gst::ElementFactory::make_with_name("pipewiresrc", None)?; - pipewire_element.set_property("fd", &raw_fd); - if let Some(node_id) = node_id { - pipewire_element.set_property("path", &node_id.to_string()); - tracing::debug!("Loading PipeWire Node ID: {} with FD: {}", node_id, raw_fd); - } else { - tracing::debug!("Loading PipeWire with FD: {}", raw_fd); - } - self.init_pipeline(pipewire_element)?; + pub fn set_pipewire_node_id(&self, node_id: u32) -> anyhow::Result<()> { + let pipewire_element = self.imp().pipewire_element.borrow().clone().unwrap(); + pipewire_element.set_property("path", &node_id.to_string()); + tracing::debug!("Loading PipeWire Node ID: {node_id}"); + self.close_pipeline(); + self.init_pipeline(&pipewire_element)?; Ok(()) } - fn init_pipeline(&self, pipewire_src: gst::Element) -> anyhow::Result<()> { + pub fn set_pipewire_fd(&self, fd: F) -> anyhow::Result<()> { + let raw_fd = fd.as_raw_fd(); + let pipewire_element = gst::ElementFactory::make_with_name("pipewiresrc", None)?; + pipewire_element.set_property("fd", &raw_fd); + tracing::debug!("Loading PipeWire with FD: {}", raw_fd); + self.imp().pipewire_element.replace(Some(pipewire_element)); + Ok(()) + } + + fn init_pipeline(&self, pipewire_src: &gst::Element) -> anyhow::Result<()> { tracing::debug!("Init pipeline"); let imp = self.imp(); let pipeline = gst::Pipeline::new(None); @@ -175,7 +176,7 @@ impl CameraPaintable { imp.sink_paintable.replace(Some(paintable)); pipeline.add_many(&[ - &pipewire_src, + pipewire_src, &tee, &queue1, &videoconvert, @@ -185,14 +186,7 @@ impl CameraPaintable { &sink, ])?; - gst::Element::link_many(&[ - &pipewire_src, - &tee, - &queue1, - &videoconvert, - &zbar, - &fakesink, - ])?; + gst::Element::link_many(&[pipewire_src, &tee, &queue1, &videoconvert, &zbar, &fakesink])?; tee.link_pads(None, &queue2, None)?; gst::Element::link_many(&[&queue2, &sink])?; diff --git a/src/widgets/camera_row.rs b/src/widgets/camera_row.rs new file mode 100644 index 0000000..a0c7cea --- /dev/null +++ b/src/widgets/camera_row.rs @@ -0,0 +1,66 @@ +use gtk::{glib, prelude::*, subclass::prelude::*}; + +#[derive(Debug, Clone, glib::Boxed)] +#[boxed_type(name = "CameraItem")] +pub struct CameraItem { + pub nick: String, + pub node_id: u32, +} + +mod imp { + use super::*; + + #[derive(Debug, Default)] + pub struct CameraRow { + pub label: gtk::Label, + pub checkmark: gtk::Image, + } + + #[glib::object_subclass] + impl ObjectSubclass for CameraRow { + const NAME: &'static str = "CameraRow"; + type Type = super::CameraRow; + type ParentType = gtk::Box; + } + + impl ObjectImpl for CameraRow { + fn constructed(&self) { + self.parent_constructed(); + let obj = self.obj(); + + obj.set_spacing(6); + self.checkmark.set_icon_name(Some("object-select-symbolic")); + self.checkmark.hide(); + + obj.append(&self.label); + obj.append(&self.checkmark); + } + } + impl WidgetImpl for CameraRow {} + impl BoxImpl for CameraRow {} +} + +glib::wrapper! { + pub struct CameraRow(ObjectSubclass) + @extends gtk::Widget, gtk::Box; +} + +impl Default for CameraRow { + fn default() -> Self { + glib::Object::new(&[]) + } +} + +impl CameraRow { + pub fn set_label(&self, label: &str) { + self.imp().label.set_label(label); + } + + pub fn set_selected(&self, selected: bool) { + self.imp().checkmark.set_visible(selected); + } + + pub fn set_item(&self, item: &CameraItem) { + self.imp().label.set_label(&item.nick); + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index b5e3cd3..eb78a4e 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,6 +1,7 @@ mod accounts; mod camera; mod camera_paintable; +mod camera_row; mod editable_label; mod error_revealer; mod preferences; @@ -13,6 +14,7 @@ pub use self::{ accounts::{AccountAddDialog, QRCodeData}, camera::{Camera, CameraEvent}, camera_paintable::CameraPaintable, + camera_row::{CameraItem, CameraRow}, editable_label::EditableLabel, error_revealer::ErrorRevealer, preferences::PreferencesWindow,