add initial scanning from a camera stream

closes #107
This commit is contained in:
Bilal Elmoussaoui 2021-01-23 01:41:57 +01:00
parent 3c5898fbe9
commit e40843c102
14 changed files with 2658 additions and 27 deletions

77
Cargo.lock generated
View file

@ -252,6 +252,8 @@ dependencies = [
"diesel_migrations", "diesel_migrations",
"futures", "futures",
"gettext-rs", "gettext-rs",
"gstreamer",
"gstreamer-base",
"gtk-macros", "gtk-macros",
"gtk4", "gtk4",
"hex", "hex",
@ -1254,6 +1256,63 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "gstreamer"
version = "0.17.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#beee75dabec062d35533c3d785b55d6c9301ca73"
dependencies = [
"bitflags",
"cfg-if 1.0.0",
"futures-channel",
"futures-core",
"futures-util",
"glib",
"gstreamer-sys",
"libc",
"muldiv",
"num-rational",
"once_cell",
"paste",
"pretty-hex",
"thiserror",
]
[[package]]
name = "gstreamer-base"
version = "0.17.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#beee75dabec062d35533c3d785b55d6c9301ca73"
dependencies = [
"bitflags",
"cfg-if 1.0.0",
"glib",
"gstreamer",
"gstreamer-base-sys",
"libc",
]
[[package]]
name = "gstreamer-base-sys"
version = "0.17.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#beee75dabec062d35533c3d785b55d6c9301ca73"
dependencies = [
"glib-sys",
"gobject-sys",
"gstreamer-sys",
"libc",
"system-deps",
]
[[package]]
name = "gstreamer-sys"
version = "0.17.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#beee75dabec062d35533c3d785b55d6c9301ca73"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
]
[[package]] [[package]]
name = "gtk-macros" name = "gtk-macros"
version = "0.2.0" version = "0.2.0"
@ -1747,6 +1806,12 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "muldiv"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5136edda114182728ccdedb9f5eda882781f35fa6e80cc360af12a8932507f3"
[[package]] [[package]]
name = "nb-connect" name = "nb-connect"
version = "1.0.2" version = "1.0.2"
@ -1982,6 +2047,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "paste"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.1.0" version = "2.1.0"
@ -2074,6 +2145,12 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "pretty-hex"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131"
[[package]] [[package]]
name = "pretty_env_logger" name = "pretty_env_logger"
version = "0.4.0" version = "0.4.0"

View file

@ -13,6 +13,8 @@ diesel = {version = "1.4", features = ["sqlite", "r2d2"]}
diesel_migrations = {version = "1.4", features = ["sqlite"]} diesel_migrations = {version = "1.4", features = ["sqlite"]}
futures = "0.3" futures = "0.3"
gettext-rs = {version = "0.5", features = ["gettext-system"]} gettext-rs = {version = "0.5", features = ["gettext-system"]}
gst = {package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"}
gst_base = {package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"}
gtk = {git = "https://github.com/gtk-rs/gtk4-rs", package = "gtk4"} gtk = {git = "https://github.com/gtk-rs/gtk4-rs", package = "gtk4"}
gtk-macros = "0.2" gtk-macros = "0.2"
hex = "0.4" hex = "0.4"

1952
build-aux/767.patch Normal file

File diff suppressed because it is too large Load diff

View file

@ -6,15 +6,17 @@
"sdk-extensions": ["org.freedesktop.Sdk.Extension.rust-stable"], "sdk-extensions": ["org.freedesktop.Sdk.Extension.rust-stable"],
"command": "authenticator", "command": "authenticator",
"finish-args": [ "finish-args": [
"--socket=pulseaudio",
"--device=all",
"--share=network", "--share=network",
"--share=ipc", "--share=ipc",
"--device=dri",
"--socket=fallback-x11", "--socket=fallback-x11",
"--socket=wayland", "--socket=wayland",
"--talk-name=org.a11y.Bus", "--talk-name=org.a11y.Bus",
"--talk-name=org.freedesktop.secrets", "--talk-name=org.freedesktop.secrets",
"--env=RUST_LOG=authenticator=debug", "--env=RUST_LOG=authenticator=debug",
"--env=G_MESSAGES_DEBUG=none" "--env=G_MESSAGES_DEBUG=none",
"--env=GST_PLUGIN_SYSTEM_PATH=/app/lib/gstreamer-1.0"
], ],
"build-options": { "build-options": {
"append-path": "/usr/lib/sdk/rust-stable/bin", "append-path": "/usr/lib/sdk/rust-stable/bin",
@ -30,7 +32,7 @@
}, },
"modules": [ "modules": [
{ {
"name": "libhandy", "name": "libadwaita",
"buildsystem": "meson", "buildsystem": "meson",
"config-opts": [ "config-opts": [
"-Dintrospection=disabled", "-Dintrospection=disabled",
@ -47,8 +49,7 @@
"branch": "main" "branch": "main"
} }
] ]
}, },{
{
"name": "zbar", "name": "zbar",
"config-opts": [ "config-opts": [
"--without-qt", "--without-qt",
@ -76,6 +77,82 @@
} }
] ]
}, },
{
"name" : "gstreamer",
"buildsystem" : "meson",
"config-opts" : [
"-Dexamples=disabled",
"-Dtests=disabled",
"-Ddoc=disabled"
],
"sources" : [
{
"type" : "git",
"branch" : "1.18",
"url" : "https://gitlab.freedesktop.org/gstreamer/gstreamer.git"
}
]
},
{
"name" : "gst-plugins-base",
"buildsystem" : "meson",
"config-opts" : [
"-Ddoc=disabled",
"-Dexamples=disabled",
"-Dtests=disabled",
"-Dorc=enabled",
"--wrap-mode=nodownload"
],
"sources" : [
{
"type" : "git",
"branch" : "1.18",
"url" : "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git"
}
]
},
{
"name" : "gst-plugins-good",
"buildsystem" : "meson",
"config-opts" : [
"-Dgtk3=disabled",
"-Dgtk4=enabled",
"-Dgtk4-experiments=true",
"-Ddoc=disabled",
"-Dexamples=disabled",
"-Dtests=disabled"
],
"sources" : [
{
"type" : "git",
"branch" : "1.18",
"url" : "https://gitlab.freedesktop.org/gstreamer/gst-plugins-good.git"
},
{
"type" : "patch",
"path" : "767.patch"
}
]
},
{
"name" : "gst-bad-plugins",
"buildsystem": "meson",
"config-opts" : [
"-Dzbar=enabled",
"-Drsvg=disabled",
"-Dvulkan=disabled",
"-Dexamples=disabled",
"-Dtests=disabled",
"-Dintrospection=disabled"
],
"sources" : [
{
"type" : "git",
"url" : "https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad.git",
"branch" : "1.18"
}
]
},
{ {
"name": "authenticator", "name": "authenticator",
"buildsystem": "meson", "buildsystem": "meson",

View file

@ -18,6 +18,9 @@
<!-- UI Files --> <!-- UI Files -->
<file compressed="true" preprocess="xml-stripblanks" alias="preferences.ui">resources/ui/preferences.ui</file> <file compressed="true" preprocess="xml-stripblanks" alias="preferences.ui">resources/ui/preferences.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="preferences_password_page.ui">resources/ui/preferences_password_page.ui</file> <file compressed="true" preprocess="xml-stripblanks" alias="preferences_password_page.ui">resources/ui/preferences_password_page.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="camera.ui">resources/ui/camera.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="shortcuts.ui">resources/ui/shortcuts.ui</file> <file compressed="true" preprocess="xml-stripblanks" alias="shortcuts.ui">resources/ui/shortcuts.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="window.ui">resources/ui/window.ui</file> <file compressed="true" preprocess="xml-stripblanks" alias="window.ui">resources/ui/window.ui</file>
</gresource> </gresource>

View file

@ -1,5 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<interface> <interface>
<menu id="scan_menu">
<section>
<item>
<attribute name="label" translatable="yes">_Camera</attribute>
<attribute name="action">add.camera</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Screenshot</attribute>
<attribute name="action">add.screenshot</attribute>
</item>
</section>
</menu>
<object class="GtkAdjustment" id="counter_adjustment"> <object class="GtkAdjustment" id="counter_adjustment">
<property name="upper">60</property> <property name="upper">60</property>
<property name="step-increment">1</property> <property name="step-increment">1</property>
@ -11,9 +23,12 @@
<property name="default-height">550</property> <property name="default-height">550</property>
<property name="title" translatable="yes">Add a new account</property> <property name="title" translatable="yes">Add a new account</property>
<child> <child>
<object class="AdwLeaflet"> <object class="AdwLeaflet" id="deck">
<property name="can-unfold">False</property>
<property name="can-swipe-back">True</property>
<child> <child>
<object class="AdwLeafletPage"> <object class="AdwLeafletPage">
<property name="name">main</property>
<property name="child"> <property name="child">
<object class="GtkBox"> <object class="GtkBox">
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
@ -41,10 +56,9 @@
</object> </object>
</child> </child>
<child type="end"> <child type="end">
<object class="GtkButton" id="scan_btn"> <object class="GtkMenuButton" id="scan_btn">
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Scan QR Code</property> <property name="tooltip-text" translatable="yes">Scan QR Code</property>
<property name="action-name">add.scan-qr</property> <property name="menu-model">scan_menu</property>
<property name="icon-name">qrscanner-symbolic</property> <property name="icon-name">qrscanner-symbolic</property>
</object> </object>
</child> </child>
@ -219,6 +233,14 @@
</property> </property>
</object> </object>
</child> </child>
<child>
<object class="AdwLeafletPage">
<property name="name">camera</property>
<property name="child">
<object class="Camera" id="camera" />
</property>
</object>
</child>
</object> </object>
</child> </child>
<child> <child>

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template parent="GtkWidget" class="Camera">
<child>
<object class="GtkStack" id="stack">
<property name="transition-type">crossfade</property>
<property name="hhomogeneous">False</property>
<child>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="GtkSpinner" id="spinner">
<property name="halign">center</property>
<property name="vexpand">True</property>
<property name="valign">center</property>
<property name="width-request">48</property>
<property name="height-request">48</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">not-found</property>
<property name="child">
<object class="AdwStatusPage">
<property name="icon-name">camera-hardware-disabled-symbolic</property>
<property name="title" translatable="yes">No Camera Found</property>
<property name="description" translatable="yes"></property>
<property name="child">
<object class="GtkButton">
<property name="label" translatable="yes">_From A Screenshot</property>
<property name="action-name">add.screenshot</property>
<property name="halign">center</property>
<property name="margin-top">24</property>
<property name="use-underline">True</property>
<style>
<class name="large-button" />
<class name="pill-button" />
<class name="suggested-action" />
</style>
</object>
</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">stream</property>
<property name="child">
<object class="GtkOverlay" id="overlay">
<child type="overlay">
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="valign">end</property>
<property name="halign">fill</property>
<property name="margin-start">18</property>
<property name="margin-end">18</property>
<property name="margin-bottom">18</property>
<property name="spacing">8</property>
<child>
<object class="GtkButton">
<property name="icon-name">qrscanner-symbolic</property>
<property name="tooltip-text" translatable="yes">Capture from a screenshot</property>
<property name="width-request">44</property>
<property name="height-request">44</property>
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="valign">center</property>
<property name="action-name">camera.screenshot-scan</property>
<style>
<class name="osd" />
<class name="circular-button" />
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -9,6 +9,12 @@ dependency('gio-2.0', version: '>= 2.56')
dependency('gdk-pixbuf-2.0') dependency('gdk-pixbuf-2.0')
dependency('gtk4', version: '>= 4.0.0') dependency('gtk4', version: '>= 4.0.0')
dependency('libadwaita-1', version: '>=1.0.0') dependency('libadwaita-1', version: '>=1.0.0')
dependency('zbar', version: '>= 0.20')
dependency('gstreamer-1.0', version: '>= 1.18')
dependency('gstreamer-base-1.0', version: '>= 1.18')
dependency('gstreamer-plugins-base-1.0', version: '>= 1.18')
dependency('gstreamer-plugins-bad-1.0', version: '>= 1.18')
cargo = find_program('cargo', required: true) cargo = find_program('cargo', required: true)
glib_compile_schemas = find_program('glib-compile-schemas', required: true) glib_compile_schemas = find_program('glib-compile-schemas', required: true)

View file

@ -4,6 +4,7 @@ data/com.belmoussaoui.Authenticator.metainfo.xml.in.in
data/resources/ui/account_add.ui data/resources/ui/account_add.ui
data/resources/ui/account_details_page.ui data/resources/ui/account_details_page.ui
data/resources/ui/account_row.ui data/resources/ui/account_row.ui
data/resources/ui/camera.ui
data/resources/ui/preferences_password_page.ui data/resources/ui/preferences_password_page.ui
data/resources/ui/preferences.ui data/resources/ui/preferences.ui
data/resources/ui/provider_page.ui data/resources/ui/provider_page.ui

View file

@ -22,6 +22,7 @@ use config::{GETTEXT_PACKAGE, LOCALEDIR};
fn main() { fn main() {
pretty_env_logger::init(); pretty_env_logger::init();
gtk::init().expect("failed to init gtk4 "); gtk::init().expect("failed to init gtk4 ");
gst::init().expect("failed to init gstreamer");
// Prepare i18n // Prepare i18n
setlocale(LocaleCategory::LcAll, ""); setlocale(LocaleCategory::LcAll, "");
bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR); bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);

View file

@ -71,6 +71,7 @@ sources = files(
'widgets/providers/mod.rs', 'widgets/providers/mod.rs',
'widgets/providers/page.rs', 'widgets/providers/page.rs',
'widgets/providers/row.rs', 'widgets/providers/row.rs',
'widgets/camera.rs',
'widgets/mod.rs', 'widgets/mod.rs',
'widgets/url_row.rs', 'widgets/url_row.rs',
'widgets/window.rs', 'widgets/window.rs',

View file

@ -1,13 +1,13 @@
use crate::{ use crate::{
helpers::qrcode,
models::{Account, OTPMethod, OTPUri, Provider, ProvidersModel}, models::{Account, OTPMethod, OTPUri, Provider, ProvidersModel},
widgets::{ProviderImage, UrlRow}, widgets::{Camera, ProviderImage, UrlRow},
}; };
use anyhow::Result; use anyhow::Result;
use glib::{clone, signal::Inhibit}; use glib::{clone, signal::Inhibit};
use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate}; use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use gtk_macros::{action, get_action}; use gtk_macros::{action, get_action};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use std::str::FromStr;
mod imp { mod imp {
use super::*; use super::*;
@ -20,6 +20,10 @@ mod imp {
pub selected_provider: RefCell<Option<Provider>>, pub selected_provider: RefCell<Option<Provider>>,
pub actions: gio::SimpleActionGroup, pub actions: gio::SimpleActionGroup,
#[template_child] #[template_child]
pub camera: TemplateChild<Camera>,
#[template_child]
pub deck: TemplateChild<adw::Leaflet>,
#[template_child]
pub image: TemplateChild<ProviderImage>, pub image: TemplateChild<ProviderImage>,
#[template_child] #[template_child]
pub provider_website_row: TemplateChild<UrlRow>, pub provider_website_row: TemplateChild<UrlRow>,
@ -79,12 +83,15 @@ mod imp {
algorithm_label: TemplateChild::default(), algorithm_label: TemplateChild::default(),
counter_row: TemplateChild::default(), counter_row: TemplateChild::default(),
period_row: TemplateChild::default(), period_row: TemplateChild::default(),
deck: TemplateChild::default(),
camera: TemplateChild::default(),
} }
} }
fn class_init(klass: &mut Self::Class) { fn class_init(klass: &mut Self::Class) {
UrlRow::static_type(); UrlRow::static_type();
ProviderImage::static_type(); ProviderImage::static_type();
Camera::static_type();
klass.set_template_from_resource("/com/belmoussaoui/Authenticator/account_add.ui"); klass.set_template_from_resource("/com/belmoussaoui/Authenticator/account_add.ui");
Self::bind_template_children(klass); Self::bind_template_children(klass);
klass.add_signal("added", glib::SignalFlags::ACTION, &[], glib::Type::Unit); klass.add_signal("added", glib::SignalFlags::ACTION, &[], glib::Type::Unit);
@ -137,20 +144,21 @@ impl AccountAddDialog {
.connect_changed(clone!(@weak self as win => move |_| win.validate())); .connect_changed(clone!(@weak self as win => move |_| win.validate()));
} }
fn scan_qr(&self) -> Result<()> { fn scan_from_screenshot(&self) {
qrcode::screenshot_area( let self_ = imp::AccountAddDialog::from_instance(self);
self.clone().upcast::<gtk::Window>(), self_.camera.from_screenshot();
clone!(@weak self as dialog => move |screenshot| { }
if let Ok(otp_uri) = qrcode::scan(&screenshot) {
dialog.set_from_otp_uri(otp_uri); fn scan_from_camera(&self) {
} let self_ = imp::AccountAddDialog::from_instance(self);
}), self_.deck.set_visible_child_name("camera");
)?;
Ok(()) self_.camera.start();
} }
fn set_from_otp_uri(&self, otp_uri: OTPUri) { fn set_from_otp_uri(&self, otp_uri: OTPUri) {
let self_ = imp::AccountAddDialog::from_instance(self); let self_ = imp::AccountAddDialog::from_instance(self);
self_.deck.set_visible_child_name("main"); // Switch back the form view
self_.token_entry.set_text(&otp_uri.secret); self_.token_entry.set_text(&otp_uri.secret);
self_.username_entry.set_text(&otp_uri.label); self_.username_entry.set_text(&otp_uri.label);
@ -234,7 +242,7 @@ impl AccountAddDialog {
self_.actions, self_.actions,
"back", "back",
clone!(@weak self as dialog => move |_, _| { clone!(@weak self as dialog => move |_, _| {
dialog.destroy(); dialog.close();
}) })
); );
action!( action!(
@ -249,11 +257,17 @@ impl AccountAddDialog {
action!( action!(
self_.actions, self_.actions,
"scan-qr", "camera",
clone!(@weak self as dialog => move |_, _| { clone!(@weak self as dialog => move |_, _| {
if let Err(err) = dialog.scan_qr() { dialog.scan_from_camera();
warn!("Failed to scan a QR code {}", err) })
} );
action!(
self_.actions,
"screenshot",
clone!(@weak self as dialog => move |_, _| {
dialog.scan_from_screenshot();
}) })
); );
self.insert_action_group("add", Some(&self_.actions)); self.insert_action_group("add", Some(&self_.actions));
@ -268,12 +282,28 @@ impl AccountAddDialog {
self_.provider_completion.connect_match_selected( self_.provider_completion.connect_match_selected(
clone!(@weak self as dialog, @strong self_.model as model => move |_, store, iter| { clone!(@weak self as dialog, @strong self_.model as model => move |_, store, iter| {
let provider_id = store.get_value(iter, 0). get_some::<i32>().unwrap(); let provider_id = store.get_value(iter, 0).get_some::<i32>().unwrap();
let provider = model.get().unwrap().find_by_id(provider_id).unwrap(); let provider = model.get().unwrap().find_by_id(provider_id).unwrap();
dialog.set_provider(provider); dialog.set_provider(provider);
Inhibit(false) Inhibit(false)
}), }),
); );
self_
.camera
.connect_local(
"code-detected",
false,
clone!(@weak self as dialog => move |args| {
let code = args.get(1).unwrap().get::<String>().unwrap().unwrap();
if let Ok(otp_uri) = OTPUri::from_str(&code) {
dialog.set_from_otp_uri(otp_uri);
}
None
}),
)
.unwrap();
} }
} }

370
src/widgets/camera.rs Normal file
View file

@ -0,0 +1,370 @@
use glib::{Receiver, Sender};
use gst::prelude::*;
use gtk::{
gio,
glib::{self, clone},
prelude::*,
subclass::prelude::*,
CompositeTemplate,
};
use gtk_macros::send;
use once_cell::sync::Lazy;
/// Fancy Camera with QR code detection using ZBar
///
/// Pipeline:
/// queue -- videoconvert -- zbar -- fakesink
/// /
/// device sink -- tee
/// \
/// queue -- glsinkbin
///
///
static PIPELINE_NAME: Lazy<glib::GString> = Lazy::new(|| glib::GString::from("camera"));
mod screenshot {
use anyhow::Result;
use ashpd::{
desktop::screenshot::{Screenshot, ScreenshotOptions, ScreenshotProxy},
zbus, RequestProxy, Response, WindowIdentifier,
};
use gtk::{gio, prelude::*};
use image::GenericImageView;
use zbar_rust::ZBarImageScanner;
pub fn scan(screenshot: &gio::File) -> Result<String> {
let (data, _) = screenshot.load_contents(gio::NONE_CANCELLABLE)?;
let img = image::load_from_memory(&data)?;
let (width, height) = img.dimensions();
let img_data: Vec<u8> = img.to_luma8().to_vec();
let mut scanner = ZBarImageScanner::new();
let results = scanner
.scan_y800(&img_data, width, height)
.map_err(|e| anyhow::format_err!(e))?;
if let Some(ref result) = results.get(0) {
let content = String::from_utf8(result.data.clone())?;
return Ok(content);
}
anyhow::bail!("Invalid QR code")
}
pub fn capture<F: FnOnce(gio::File)>(window: gtk::Window, callback: F) -> Result<()> {
let connection = zbus::Connection::new_session()?;
let proxy = ScreenshotProxy::new(&connection)?;
let handle = proxy.screenshot(
WindowIdentifier::from(window),
ScreenshotOptions::default().interactive(true).modal(true),
)?;
let request = RequestProxy::new(&connection, &handle)?;
request.on_response(move |response: Response<Screenshot>| {
if let Ok(screenshot) = response {
callback(gio::File::new_for_uri(&screenshot.uri));
}
})?;
Ok(())
}
}
#[derive(Debug)]
pub enum CameraEvent {
CodeDetected(String),
DeviceAdded(gst::Device),
DeviceRemoved(gst::Device),
StreamStarted,
}
#[derive(Debug)]
pub enum CameraState {
Loading,
NotFound,
Ready,
Paused,
}
mod imp {
use super::*;
use glib::subclass;
use std::cell::RefCell;
#[derive(Debug, CompositeTemplate)]
pub struct Camera {
pub actions: gio::SimpleActionGroup,
pub sender: Sender<CameraEvent>,
pub receiver: RefCell<Option<Receiver<CameraEvent>>>,
pub pipeline: gst::Pipeline,
pub sink: gst::Element,
pub devices: gio::ListStore,
pub monitor: gst::DeviceMonitor,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub overlay: TemplateChild<gtk::Overlay>,
#[template_child]
pub spinner: TemplateChild<gtk::Spinner>,
}
impl ObjectSubclass for Camera {
const NAME: &'static str = "Camera";
type Type = super::Camera;
type ParentType = gtk::Widget;
type Instance = subclass::simple::InstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
glib::object_subclass!();
fn class_init(klass: &mut Self::Class) {
klass.set_template_from_resource("/com/belmoussaoui/Authenticator/camera.ui");
Self::bind_template_children(klass);
klass.set_layout_manager_type::<gtk::BinLayout>();
klass.add_signal(
"code-detected",
glib::SignalFlags::RUN_FIRST,
&[String::static_type()],
glib::Type::Unit,
);
}
fn instance_init(obj: &subclass::InitializingObject<Self::Type>) {
obj.init_template();
}
fn new() -> Self {
let pipeline = gst::Pipeline::new(Some(&*PIPELINE_NAME));
let sink = gst::ElementFactory::make("gtk4glsink", None).unwrap();
let (sender, r) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let receiver = RefCell::new(Some(r));
Self {
actions: gio::SimpleActionGroup::new(),
sink,
sender,
receiver,
pipeline,
spinner: TemplateChild::default(),
stack: TemplateChild::default(),
overlay: TemplateChild::default(),
monitor: gst::DeviceMonitor::new(),
devices: gio::ListStore::new(gst::Device::static_type()),
}
}
}
impl ObjectImpl for Camera {
fn constructed(&self, obj: &Self::Type) {
obj.init_widgets();
obj.init_monitor();
self.parent_constructed(obj);
}
fn dispose(&self, _obj: &Self::Type) {
self.monitor.stop();
self.pipeline.set_state(gst::State::Null).unwrap();
}
}
impl WidgetImpl for Camera {}
}
glib::wrapper! {
pub struct Camera(ObjectSubclass<imp::Camera>) @extends gtk::Widget;
}
impl Camera {
pub fn new() -> Self {
glib::Object::new(&[]).expect("Failed to create a Camera")
}
fn init_monitor(&self) {
let self_ = imp::Camera::from_instance(self);
let caps = gst::Caps::new_simple("video/x-raw", &[]);
self_.monitor.add_filter(Some("Video/Source"), Some(&caps));
self_.monitor.start().unwrap();
let bus = self_.monitor.get_bus();
bus.add_watch_local(clone!(@strong self_.sender as sender => move |_, msg| {
use gst::MessageView;
match msg.view() {
MessageView::DeviceAdded(event) => {
let device = event.get_device();
send!(sender, CameraEvent::DeviceAdded(device));
}
MessageView::DeviceRemoved(event) => {
let device = event.get_device();
send!(sender, CameraEvent::DeviceRemoved(device));
}
_ => (),
};
glib::Continue(true)
}))
.expect("Failed to attach a monitor");
}
fn init_pipelines(&self, source_element: gst::Element) {
let self_ = imp::Camera::from_instance(self);
let tee = gst::ElementFactory::make("tee", None).unwrap();
let queue = gst::ElementFactory::make("queue", None).unwrap();
let videoconvert = gst::ElementFactory::make("videoconvert", None).unwrap();
let zbar = gst::ElementFactory::make("zbar", None).unwrap();
let fakesink = gst::ElementFactory::make("fakesink", None).unwrap();
let queue2 = gst::ElementFactory::make("queue", None).unwrap();
let glsinkbin = gst::ElementFactory::make("glsinkbin", None).unwrap();
glsinkbin.set_property("sink", &self_.sink).unwrap();
self_
.pipeline
.add_many(&[
&source_element,
&tee,
&queue,
&videoconvert,
&zbar,
&fakesink,
&queue2,
&glsinkbin,
])
.unwrap();
gst::Element::link_many(&[
&source_element,
&tee,
&queue,
&videoconvert,
&zbar,
&fakesink,
])
.unwrap();
tee.link_pads(None, &queue2, None).unwrap();
gst::Element::link_many(&[&queue2, &glsinkbin]).unwrap();
let bus = self_.pipeline.get_bus().unwrap();
bus.add_watch_local(clone!(@strong self_.sender as sender => move |_, msg| {
use gst::MessageView;
match msg.view() {
MessageView::StateChanged(state) => {
if Some(&*PIPELINE_NAME) == state.get_src().map(|s| s.get_name()).as_ref() {
let structure = state.get_structure().unwrap();
let new_state = structure.get::<gst::State>("new-state")
.unwrap().unwrap();
if new_state == gst::State::Playing {
send!(sender, CameraEvent::StreamStarted);
}
}
}
MessageView::Element(e) => {
if let Some(s) = e.get_structure() {
if let Ok(Some(symbol)) = s.get::<String>("symbol") {
send!(sender, CameraEvent::CodeDetected(symbol));
}
}
}
MessageView::Error(err) => {
error!(
"Error from {:?}: {} ({:?})",
err.get_src().map(|s| s.get_path_string()),
err.get_error(),
err.get_debug()
);
}
_ => (),
};
glib::Continue(true)
}))
.expect("Failed to add bus watch");
}
fn set_state(&self, state: CameraState) {
let self_ = imp::Camera::from_instance(self);
info!("The camera state changed to {:#?}", state);
match state {
CameraState::NotFound => {
self_.stack.get().set_visible_child_name("not-found");
}
CameraState::Ready => {
self_.stack.get().set_visible_child_name("stream");
self_.spinner.get().stop();
}
CameraState::Loading => {
self_.stack.get().set_visible_child_name("loading");
self_.spinner.get().start();
}
CameraState::Paused => {}
}
}
fn do_event(&self, event: CameraEvent) -> glib::Continue {
let self_ = imp::Camera::from_instance(self);
match event {
CameraEvent::CodeDetected(code) => {
self.emit("code-detected", &[&code]).unwrap();
}
CameraEvent::DeviceAdded(device) => {
// TODO: allow selecting a device and update the sink on the pipeline
info!("Camera source added: {}", device.get_display_name());
self.set_state(CameraState::Loading);
let element = device.create_element(None).unwrap();
self.init_pipelines(element);
self_.devices.append(&device);
}
CameraEvent::DeviceRemoved(device) => {
info!("Camera source removed: {}", device.get_display_name());
self_.devices.append(&device);
}
CameraEvent::StreamStarted => {
self.set_state(CameraState::Ready);
}
}
glib::Continue(true)
}
pub fn start(&self) {
let self_ = imp::Camera::from_instance(self);
self_.pipeline.set_state(gst::State::Playing).unwrap();
}
pub fn stop(&self) {
let self_ = imp::Camera::from_instance(self);
self.set_state(CameraState::Paused);
self_.pipeline.set_state(gst::State::Null).unwrap();
}
pub fn from_screenshot(&self) {
let self_ = imp::Camera::from_instance(self);
let window = self.get_root().unwrap().downcast::<gtk::Window>().unwrap();
screenshot::capture(
window,
clone!(@strong self_.sender as sender => move |file| {
if let Ok(code) = screenshot::scan(&file) {
send!(sender, CameraEvent::CodeDetected(code));
}
}),
)
.ok();
}
fn init_widgets(&self) {
let self_ = imp::Camera::from_instance(self);
self.set_state(CameraState::NotFound);
let receiver = self_.receiver.borrow_mut().take().unwrap();
receiver.attach(
None,
clone!(@weak self as camera => move |action| camera.do_event(action)),
);
let widget = self_
.sink
.get_property("widget")
.unwrap()
.get::<gtk::Widget>()
.unwrap()
.unwrap();
widget.set_property("force-aspect-ratio", &false).unwrap();
self_.overlay.get().set_child(Some(&widget));
}
}

View file

@ -1,4 +1,5 @@
mod accounts; mod accounts;
mod camera;
mod preferences; mod preferences;
mod providers; mod providers;
mod url_row; mod url_row;
@ -6,6 +7,7 @@ mod window;
pub use self::{ pub use self::{
accounts::{AccountAddDialog, QRCodeData}, accounts::{AccountAddDialog, QRCodeData},
camera::Camera,
preferences::PreferencesWindow, preferences::PreferencesWindow,
providers::{ProviderImage, ProvidersDialog, ProvidersList}, providers::{ProviderImage, ProvidersDialog, ProvidersList},
url_row::UrlRow, url_row::UrlRow,