mirror of
https://gitlab.gnome.org/World/Authenticator.git
synced 2025-03-04 08:44:40 +01:00
Multi cameras support
This commit is contained in:
parent
db3afdc7b9
commit
80d8ea71a4
10 changed files with 232 additions and 90 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -107,6 +107,7 @@ dependencies = [
|
|||
"rand",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tracing",
|
||||
"url",
|
||||
"zbus 3.6.2",
|
||||
]
|
||||
|
|
|
@ -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"]}
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
<file preprocess="xml-stripblanks" alias="refresh-symbolic.svg">resources/icons/refresh-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks" alias="copy-symbolic.svg">resources/icons/copy-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks" alias="link-symbolic.svg">resources/icons/link-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks" alias="video-camera-symbolic.svg">resources/icons/video-camera-symbolic.svg</file>
|
||||
</gresource>
|
||||
<gresource prefix="/com/belmoussaoui/Authenticator/icons/48x48/status/">
|
||||
<file preprocess="xml-stripblanks" alias="provider-fallback.svg">resources/icons/provider-fallback.svg</file>
|
||||
|
|
2
data/resources/icons/video-camera-symbolic.svg
Normal file
2
data/resources/icons/video-camera-symbolic.svg
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 6.5 1 c -1.921875 0 -3.5 1.578125 -3.5 3.5 c 0 0.941406 0.386719 1.789062 1 2.417969 v 1.882812 l -2.445312 -1.632812 c -0.164063 -0.109375 -0.359376 -0.164063 -0.554688 -0.167969 h -1 v 6 h 1 v -0.003906 c 0.1875 0.003906 0.378906 -0.046875 0.554688 -0.164063 l 2.445312 -1.632812 v 1.800781 c 0 0.832031 0.5625 1.523438 1.050781 1.769531 c 0.492188 0.246094 0.949219 0.230469 0.949219 0.230469 h 7 s 0.457031 0.015625 0.949219 -0.230469 c 0.488281 -0.246093 1.050781 -0.9375 1.050781 -1.769531 v -2 l 1 -1 v -2 h -1 v -1.082031 c 0.613281 -0.628907 1 -1.476563 1 -2.417969 c 0 -1.921875 -1.578125 -3.5 -3.5 -3.5 c -1.277344 0 -2.386719 0.703125 -3 1.734375 c -0.613281 -1.03125 -1.722656 -1.734375 -3 -1.734375 z m 0 2 c 0.839844 0 1.5 0.660156 1.5 1.5 s -0.660156 1.5 -1.5 1.5 s -1.5 -0.660156 -1.5 -1.5 s 0.660156 -1.5 1.5 -1.5 z m 6 0 c 0.839844 0 1.5 0.660156 1.5 1.5 s -0.660156 1.5 -1.5 1.5 s -1.5 -0.660156 -1.5 -1.5 s 0.660156 -1.5 1.5 -1.5 z m 0 0" fill="#222222"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -63,14 +63,30 @@
|
|||
<object class="GtkStackPage">
|
||||
<property name="name">stream</property>
|
||||
<property name="child">
|
||||
<object class="GtkOverlay">
|
||||
<child>
|
||||
<object class="GtkPicture" id="picture">
|
||||
<property name="keep-aspect-ratio">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="GtkMenuButton" id="camera_selection_button">
|
||||
<property name="icon-name">video-camera-symbolic</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="margin-end">18</property>
|
||||
<property name="margin-bottom">18</property>
|
||||
<style>
|
||||
<class name="osd" />
|
||||
<class name="circular" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
|
|
@ -201,10 +201,15 @@ impl AccountAddDialog {
|
|||
}
|
||||
|
||||
#[template_callback]
|
||||
fn camera_code_detected(&self, _camera: Camera, code: String) {
|
||||
if let Ok(otp_uri) = OTPUri::from_str(&code) {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
|
|
|
@ -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<u8> = 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<Option<(RawFd, Vec<ashpd::desktop::camera::Stream>)>> {
|
||||
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<Option<Receiver<CameraEvent>>>,
|
||||
pub started: Cell<bool>,
|
||||
#[template_child]
|
||||
pub previous: TemplateChild<gtk::Button>,
|
||||
#[template_child]
|
||||
|
@ -107,6 +89,10 @@ mod imp {
|
|||
pub spinner: TemplateChild<gtk::Spinner>,
|
||||
#[template_child]
|
||||
pub screenshot: TemplateChild<gtk::Button>,
|
||||
#[template_child]
|
||||
pub camera_selection_button: TemplateChild<gtk::MenuButton>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
let imp = self.imp();
|
||||
if imp.started.get() {
|
||||
imp.paintable.stop();
|
||||
imp.started.set(false);
|
||||
}
|
||||
imp.stream_list.remove_all();
|
||||
imp.selection.set_selected(gtk::INVALID_LIST_POSITION);
|
||||
}
|
||||
|
||||
pub fn connect_close<F>(&self, callback: F) -> glib::SignalHandlerId
|
||||
|
@ -225,14 +210,33 @@ impl Camera {
|
|||
)
|
||||
}
|
||||
|
||||
fn set_streams(&self, streams: Vec<ashpd::desktop::camera::Stream>) {
|
||||
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) {
|
||||
if !self.imp().started.get() {
|
||||
spawn!(clone!(@weak self as camera => async move {
|
||||
match screenshot::stream().await {
|
||||
match ashpd::desktop::camera::request().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(),
|
||||
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}"),
|
||||
};
|
||||
},
|
||||
|
@ -243,7 +247,6 @@ impl Camera {
|
|||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn scan_from_screenshot(&self) -> anyhow::Result<()> {
|
||||
let screenshot_file = screenshot::capture(
|
||||
|
@ -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::<gtk::ListItem>()
|
||||
.unwrap()
|
||||
.set_child(Some(&camera_row));
|
||||
});
|
||||
let selection = &imp.selection;
|
||||
factory.connect_bind(glib::clone!(@weak selection => move |_, item| {
|
||||
let item = item.clone().downcast::<gtk::ListItem>().unwrap();
|
||||
let child = item.child().unwrap();
|
||||
let row = child.downcast_ref::<CameraRow>().unwrap();
|
||||
|
||||
let item = item.item().unwrap().downcast::<glib::BoxedAnyObject>().unwrap();
|
||||
let camera_item = item.borrow::<CameraItem>();
|
||||
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::<glib::BoxedAnyObject>().unwrap().borrow::<CameraItem>().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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ mod imp {
|
|||
pub struct CameraPaintable {
|
||||
pub sender: RefCell<Option<Sender<CameraEvent>>>,
|
||||
pub pipeline: RefCell<Option<gst::Pipeline>>,
|
||||
pub pipewire_element: RefCell<Option<gst::Element>>,
|
||||
pub sink_paintable: RefCell<Option<gdk::Paintable>>,
|
||||
}
|
||||
|
||||
|
@ -110,25 +111,25 @@ impl CameraPaintable {
|
|||
paintable
|
||||
}
|
||||
|
||||
pub fn set_pipewire_node_id<F: AsRawFd>(
|
||||
&self,
|
||||
fd: F,
|
||||
node_id: Option<u32>,
|
||||
) -> 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 {
|
||||
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: {} with FD: {}", node_id, raw_fd);
|
||||
} else {
|
||||
tracing::debug!("Loading PipeWire with FD: {}", raw_fd);
|
||||
}
|
||||
self.init_pipeline(pipewire_element)?;
|
||||
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<F: AsRawFd>(&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])?;
|
||||
|
||||
|
|
66
src/widgets/camera_row.rs
Normal file
66
src/widgets/camera_row.rs
Normal file
|
@ -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<imp::CameraRow>)
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue