Multi cameras support

This commit is contained in:
Bilal Elmoussaoui 2022-12-27 14:07:10 +01:00
parent db3afdc7b9
commit 80d8ea71a4
10 changed files with 232 additions and 90 deletions

1
Cargo.lock generated
View file

@ -107,6 +107,7 @@ dependencies = [
"rand",
"serde",
"serde_repr",
"tracing",
"url",
"zbus 3.6.2",
]

View file

@ -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"]}

View file

@ -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>

View 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

View file

@ -63,14 +63,30 @@
<object class="GtkStackPage">
<property name="name">stream</property>
<property name="child">
<object class="GtkPicture" id="picture">
<property name="keep-aspect-ratio">False</property>
<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>

View file

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

View file

@ -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);
}
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<F>(&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<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) {
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::<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}");
}
}
}

View file

@ -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 {
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<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
View 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);
}
}

View file

@ -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,