mirror of
https://gitlab.gnome.org/World/Authenticator.git
synced 2025-03-04 00:34:40 +01:00
Use aperture
This commit is contained in:
parent
ff82f1a938
commit
04a1c45fd5
13 changed files with 475 additions and 890 deletions
765
Cargo.lock
generated
765
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,8 @@ rust-version = "1.70"
|
|||
adw = {package = "libadwaita", version = "0.5", features = ["v1_4"]}
|
||||
aes-gcm = "0.10"
|
||||
anyhow = "1.0"
|
||||
ashpd = {version = "0.6", default-features = false, features = ["pipewire", "gtk4", "tokio", "tracing"]}
|
||||
aperture = "0.3.2"
|
||||
ashpd = {version = "0.6", default-features = false, features = ["gtk4", "tokio", "tracing"]}
|
||||
data-encoding = "2.3"
|
||||
diesel = {version = "2.0", features = ["sqlite", "r2d2"]}
|
||||
diesel_migrations = {version = "2.0", features = ["sqlite"]}
|
||||
|
@ -17,9 +18,6 @@ futures-channel = "0.3"
|
|||
futures-executor = "0.3"
|
||||
futures-util = "0.3"
|
||||
gettext-rs = {version = "0.7", features = ["gettext-system"]}
|
||||
gst = {package = "gstreamer", version = "0.21"}
|
||||
gst_video = { package = "gstreamer-video", version = "0.21"}
|
||||
gst4gtk = {package = "gst-plugin-gtk4", version = "0.11.0-alpha.1", features = ["wayland", "x11egl", "x11glx"]}
|
||||
gtk = {package = "gtk4", version = "0.7", features = ["v4_10"]}
|
||||
hex = {version = "0.4.3", features = ["serde"]}
|
||||
image = {version = "0.24", default-features = false, features = ["png"]}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"--socket=wayland",
|
||||
"--device=dri",
|
||||
"--talk-name=org.freedesktop.secrets",
|
||||
"--env=RUST_LOG=authenticator=debug,ashpd=debug,oo7=debug",
|
||||
"--env=RUST_LOG=authenticator=debug,ashpd=debug,oo7=debug,aperture=debug",
|
||||
"--env=G_MESSAGES_DEBUG=none",
|
||||
"--talk-name=org.gtk.vfs.*",
|
||||
"--filesystem=xdg-run/gvfs",
|
||||
|
|
|
@ -233,7 +233,6 @@
|
|||
<object class="AdwNavigationPage">
|
||||
<property name="title" translatable="yes">Camera</property>
|
||||
<property name="tag">camera</property>
|
||||
<signal name="hidden" handler="camera_page_hidden" swapped="true" />
|
||||
<property name="child">
|
||||
<object class="Camera" id="camera">
|
||||
<signal name="close" handler="camera_closed" swapped="true" />
|
||||
|
|
|
@ -57,8 +57,8 @@
|
|||
<property name="child">
|
||||
<object class="GtkOverlay">
|
||||
<child>
|
||||
<object class="GtkPicture" id="picture">
|
||||
<property name="keep-aspect-ratio">False</property>
|
||||
<object class="ApertureViewfinder" id="viewfinder">
|
||||
<property name="detect-codes">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
|
|
|
@ -27,7 +27,6 @@ src/models/algorithm.rs
|
|||
src/widgets/accounts/add.rs
|
||||
src/widgets/accounts/details.rs
|
||||
src/widgets/accounts/row.rs
|
||||
src/widgets/camera.rs
|
||||
src/widgets/preferences/password_page.rs
|
||||
src/widgets/preferences/window.rs
|
||||
src/widgets/providers/dialog.rs
|
||||
|
|
|
@ -27,8 +27,7 @@ fn init_i18n() -> anyhow::Result<()> {
|
|||
fn main() -> glib::ExitCode {
|
||||
tracing_subscriber::fmt::init();
|
||||
gtk::init().expect("failed to init gtk");
|
||||
gst::init().expect("failed to init gstreamer");
|
||||
gst4gtk::plugin_register_static().expect("Failed to register gstgtk4 plugin");
|
||||
aperture::init(config::APP_ID);
|
||||
|
||||
if let Err(err) = init_i18n() {
|
||||
tracing::error!("Failed to initialize i18n {}", err);
|
||||
|
|
|
@ -198,11 +198,6 @@ impl AccountAddDialog {
|
|||
name_entry.set_position(entry.cursor_position());
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn camera_page_hidden(&self, _page: &adw::NavigationPage) {
|
||||
self.imp().camera.stop();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn camera_closed(&self, _camera: Camera) {
|
||||
self.activate_action("add.previous", None).unwrap();
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
use std::cell::RefCell;
|
||||
use std::cell::OnceCell;
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::Once;
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use anyhow::Result;
|
||||
use ashpd::desktop::screenshot::ScreenshotRequest;
|
||||
use gettextrs::gettext;
|
||||
use glib::once_cell::sync::Lazy;
|
||||
use gst::prelude::*;
|
||||
use gtk::{
|
||||
gio,
|
||||
glib::{self, clone, Receiver},
|
||||
glib::{self, clone},
|
||||
prelude::*,
|
||||
};
|
||||
use image::GenericImageView;
|
||||
|
||||
use super::{CameraItem, CameraRow};
|
||||
use crate::{utils::spawn_tokio, widgets::CameraPaintable};
|
||||
|
||||
static CAMERA_LOCATION: &str = "api.libcamera.location";
|
||||
use super::CameraRow;
|
||||
use crate::utils::spawn_tokio;
|
||||
|
||||
pub mod screenshot {
|
||||
use super::*;
|
||||
|
@ -62,30 +60,18 @@ pub mod screenshot {
|
|||
}
|
||||
}
|
||||
|
||||
pub enum CameraEvent {
|
||||
CodeDetected(String),
|
||||
StreamStarted,
|
||||
}
|
||||
|
||||
pub enum CameraState {
|
||||
NotFound,
|
||||
Ready,
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use glib::subclass::{InitializingObject, Signal};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(gtk::CompositeTemplate)]
|
||||
#[derive(gtk::CompositeTemplate, Default)]
|
||||
#[template(resource = "/com/belmoussaoui/Authenticator/camera.ui")]
|
||||
pub struct Camera {
|
||||
pub paintable: CameraPaintable,
|
||||
pub receiver: RefCell<Option<Receiver<CameraEvent>>>,
|
||||
#[template_child]
|
||||
pub stack: TemplateChild<gtk::Stack>,
|
||||
#[template_child]
|
||||
pub picture: TemplateChild<gtk::Picture>,
|
||||
pub viewfinder: TemplateChild<aperture::Viewfinder>,
|
||||
#[template_child]
|
||||
pub spinner: TemplateChild<gtk::Spinner>,
|
||||
#[template_child]
|
||||
|
@ -94,8 +80,8 @@ mod imp {
|
|||
pub camera_selection_button: TemplateChild<gtk::MenuButton>,
|
||||
#[template_child]
|
||||
pub toolbar_view: TemplateChild<adw::ToolbarView>,
|
||||
pub stream_list: gio::ListStore,
|
||||
pub selection: gtk::SingleSelection,
|
||||
pub provider: OnceCell<aperture::DeviceProvider>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
@ -113,24 +99,6 @@ mod imp {
|
|||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
|
||||
fn new() -> Self {
|
||||
let (sender, r) = glib::MainContext::channel(glib::Priority::default());
|
||||
let receiver = RefCell::new(Some(r));
|
||||
|
||||
Self {
|
||||
paintable: CameraPaintable::new(sender),
|
||||
receiver,
|
||||
camera_selection_button: TemplateChild::default(),
|
||||
spinner: TemplateChild::default(),
|
||||
stack: TemplateChild::default(),
|
||||
picture: TemplateChild::default(),
|
||||
screenshot: TemplateChild::default(),
|
||||
toolbar_view: TemplateChild::default(),
|
||||
stream_list: gio::ListStore::new::<glib::BoxedAnyObject>(),
|
||||
selection: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for Camera {
|
||||
|
@ -150,14 +118,67 @@ mod imp {
|
|||
fn constructed(&self) {
|
||||
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));
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.paintable.close_pipeline();
|
||||
let provider = aperture::DeviceProvider::instance();
|
||||
self.provider.set(provider.clone()).unwrap();
|
||||
|
||||
self.viewfinder
|
||||
.connect_state_notify(glib::clone!(@weak obj => move |_| {
|
||||
obj.update_viewfinder_state();
|
||||
}));
|
||||
obj.update_viewfinder_state();
|
||||
|
||||
self.viewfinder.connect_code_detected(
|
||||
glib::clone!(@weak obj => move|_, code_type, code| {
|
||||
if matches!(code_type, aperture::CodeType::Qr) {
|
||||
obj.emit_by_name::<()>("code-detected", &[&code]);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let popover = gtk::Popover::new();
|
||||
popover.add_css_class("menu");
|
||||
|
||||
self.selection.set_model(Some(provider));
|
||||
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 = &self.selection;
|
||||
factory.connect_bind(glib::clone!(@weak selection => move |_, item| {
|
||||
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
|
||||
let child = item.child().unwrap();
|
||||
let row = child.downcast_ref::<CameraRow>().unwrap();
|
||||
|
||||
let item = item.item().and_downcast::<aperture::Camera>().unwrap();
|
||||
row.set_label(&item.display_name());
|
||||
|
||||
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(self.selection.clone()), Some(factory));
|
||||
popover.set_child(Some(&list_view));
|
||||
|
||||
self.selection.connect_selected_item_notify(
|
||||
glib::clone!(@weak obj, @weak popover => move |selection| {
|
||||
if let Some(selected_item) = selection.selected_item() {
|
||||
let camera = selected_item.downcast_ref::<aperture::Camera>();
|
||||
obj.imp().viewfinder.set_camera(camera);
|
||||
}
|
||||
popover.popdown();
|
||||
}),
|
||||
);
|
||||
|
||||
self.camera_selection_button.set_popover(Some(&popover));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,19 +193,6 @@ glib::wrapper! {
|
|||
|
||||
#[gtk::template_callbacks]
|
||||
impl Camera {
|
||||
pub fn start(&self) {
|
||||
let imp = self.imp();
|
||||
imp.paintable.start();
|
||||
self.set_state(CameraState::Ready);
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
let imp = self.imp();
|
||||
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
|
||||
where
|
||||
F: Fn(&Self) + 'static,
|
||||
|
@ -214,46 +222,27 @@ impl Camera {
|
|||
)
|
||||
}
|
||||
|
||||
fn set_streams(&self, streams: Vec<ashpd::desktop::camera::Stream>) {
|
||||
let imp = self.imp();
|
||||
let mut selected_stream = 0;
|
||||
for (id, stream) in streams.into_iter().enumerate() {
|
||||
let default = gettext("Unknown Device");
|
||||
let nick = stream
|
||||
.properties()
|
||||
.get("node.nick")
|
||||
.unwrap_or(&default)
|
||||
.to_string();
|
||||
pub async fn scan_from_camera(&self) {
|
||||
static INIT: Once = Once::new();
|
||||
if INIT.is_completed() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(location) = stream.properties().get(CAMERA_LOCATION) {
|
||||
if location == "front" {
|
||||
selected_stream = id;
|
||||
let provider = self.imp().provider.get().unwrap();
|
||||
match spawn_tokio(stream()).await {
|
||||
Ok(fd) => {
|
||||
if let Err(err) = provider.set_fd(fd) {
|
||||
tracing::error!("Could not use the camera portal: {err}");
|
||||
} else {
|
||||
if let Err(err) = provider.start() {
|
||||
tracing::error!("Could not start the device provider: {err}");
|
||||
} else {
|
||||
tracing::debug!("Device provider started");
|
||||
INIT.call_once(|| ());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let item = CameraItem {
|
||||
nick,
|
||||
node_id: stream.node_id(),
|
||||
};
|
||||
imp.stream_list.append(&glib::BoxedAnyObject::new(item));
|
||||
}
|
||||
imp.selection.set_selected(selected_stream as u32);
|
||||
}
|
||||
|
||||
pub async fn scan_from_camera(&self) {
|
||||
match spawn_tokio(ashpd::desktop::camera::request()).await {
|
||||
Ok(Some((stream_fd, nodes_id))) => {
|
||||
match self.imp().paintable.set_pipewire_fd(stream_fd) {
|
||||
Ok(_) => {
|
||||
self.set_streams(nodes_id);
|
||||
}
|
||||
Err(err) => tracing::error!("Failed to start the camera stream {err}"),
|
||||
};
|
||||
}
|
||||
Ok(None) => {
|
||||
self.set_state(CameraState::NotFound);
|
||||
}
|
||||
Err(e) => tracing::error!("Failed to stream {}", e),
|
||||
Err(err) => tracing::error!("Failed to start the camera portal: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,93 +262,38 @@ impl Camera {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn set_state(&self, state: CameraState) {
|
||||
fn update_viewfinder_state(&self) {
|
||||
let imp = self.imp();
|
||||
let state = imp.viewfinder.state();
|
||||
match state {
|
||||
CameraState::NotFound => {
|
||||
tracing::info!("The camera state changed: Not Found");
|
||||
imp.stack.set_visible_child_name("not-found");
|
||||
imp.toolbar_view.set_extend_content_to_top_edge(false);
|
||||
imp.toolbar_view.remove_css_class("extended");
|
||||
aperture::ViewfinderState::Loading => {
|
||||
imp.stack.set_visible_child_name("loading");
|
||||
}
|
||||
CameraState::Ready => {
|
||||
tracing::info!("The camera state changed: Ready");
|
||||
aperture::ViewfinderState::Error => {
|
||||
imp.stack.set_visible_child_name("not-found");
|
||||
}
|
||||
aperture::ViewfinderState::NoCameras => {
|
||||
imp.stack.set_visible_child_name("not-found");
|
||||
}
|
||||
aperture::ViewfinderState::Ready => {
|
||||
imp.stack.set_visible_child_name("stream");
|
||||
imp.toolbar_view.set_extend_content_to_top_edge(true);
|
||||
imp.toolbar_view.add_css_class("extended");
|
||||
imp.spinner.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::info!("The camera state changed: {state:?}");
|
||||
|
||||
fn setup_receiver(&self) {
|
||||
self.imp().receiver.borrow_mut().take().unwrap().attach(
|
||||
None,
|
||||
glib::clone!(@weak self as camera => @default-return glib::ControlFlow::Break, move |event| {
|
||||
match event {
|
||||
CameraEvent::CodeDetected(code) => {
|
||||
camera.emit_by_name::<()>("code-detected", &[&code]);
|
||||
}
|
||||
CameraEvent::StreamStarted => {
|
||||
camera.set_state(CameraState::Ready);
|
||||
}
|
||||
}
|
||||
glib::ControlFlow::Continue
|
||||
}),
|
||||
);
|
||||
}
|
||||
let is_ready = matches!(state, aperture::ViewfinderState::Ready);
|
||||
imp.toolbar_view.set_extend_content_to_top_edge(is_ready);
|
||||
if is_ready {
|
||||
imp.toolbar_view.add_css_class("extended");
|
||||
} else {
|
||||
imp.toolbar_view.remove_css_class("extended");
|
||||
}
|
||||
|
||||
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.downcast_ref::<gtk::ListItem>().unwrap();
|
||||
let child = item.child().unwrap();
|
||||
let row = child.downcast_ref::<CameraRow>().unwrap();
|
||||
|
||||
let item = item.item().and_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.clone()), 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));
|
||||
if matches!(state, aperture::ViewfinderState::Loading) {
|
||||
imp.spinner.start();
|
||||
} else {
|
||||
imp.spinner.stop();
|
||||
}
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
|
@ -375,3 +309,10 @@ impl Default for Camera {
|
|||
glib::Object::new()
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream() -> ashpd::Result<RawFd> {
|
||||
let proxy = ashpd::desktop::camera::Camera::new().await?;
|
||||
proxy.request_access().await?;
|
||||
|
||||
proxy.open_pipe_wire_remote().await
|
||||
}
|
||||
|
|
|
@ -1,265 +0,0 @@
|
|||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
use glib::once_cell::sync::Lazy;
|
||||
use gst::prelude::*;
|
||||
use gtk::{
|
||||
gdk,
|
||||
glib::{self, clone, Sender},
|
||||
graphene,
|
||||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
};
|
||||
|
||||
use crate::widgets::camera::CameraEvent;
|
||||
static PIPELINE_NAME: Lazy<glib::GString> = Lazy::new(|| glib::GString::from("camera"));
|
||||
/// Fancy Camera with QR code detection using ZBar
|
||||
///
|
||||
/// Pipeline:
|
||||
/// queue -- videoconvert -- zbar -- fakesink
|
||||
/// /
|
||||
/// pipewiresrc -- tee
|
||||
/// \
|
||||
/// queue -- videoflip - glsinkbin
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
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>>,
|
||||
pub guard: RefCell<Option<gst::bus::BusWatchGuard>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for CameraPaintable {
|
||||
const NAME: &'static str = "CameraPaintable";
|
||||
type Type = super::CameraPaintable;
|
||||
type Interfaces = (gdk::Paintable,);
|
||||
}
|
||||
|
||||
impl ObjectImpl for CameraPaintable {
|
||||
fn dispose(&self) {
|
||||
self.obj().close_pipeline();
|
||||
}
|
||||
}
|
||||
|
||||
impl PaintableImpl for CameraPaintable {
|
||||
fn intrinsic_height(&self) -> i32 {
|
||||
if let Some(ref paintable) = *self.sink_paintable.borrow() {
|
||||
paintable.intrinsic_height()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn intrinsic_width(&self) -> i32 {
|
||||
if let Some(ref paintable) = *self.sink_paintable.borrow() {
|
||||
paintable.intrinsic_width()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(&self, snapshot: &gdk::Snapshot, width: f64, height: f64) {
|
||||
if let Some(ref image) = *self.sink_paintable.borrow() {
|
||||
// Transformation to avoid stretching the camera. We translate and scale the
|
||||
// image.
|
||||
let aspect = width / height.max(std::f64::EPSILON); // Do not divide by zero.
|
||||
let image_aspect = image.intrinsic_aspect_ratio();
|
||||
|
||||
if image_aspect == 0.0 {
|
||||
image.snapshot(snapshot, width, height);
|
||||
return;
|
||||
};
|
||||
|
||||
let (new_width, new_height) = match aspect <= image_aspect {
|
||||
true => (height * image_aspect, height), // Mobile view
|
||||
false => (width, width / image_aspect), // Landscape
|
||||
};
|
||||
|
||||
let p = graphene::Point::new(
|
||||
((width - new_width) / 2.0) as f32,
|
||||
((height - new_height) / 2.0) as f32,
|
||||
);
|
||||
snapshot.translate(&p);
|
||||
|
||||
image.snapshot(snapshot, new_width, new_height);
|
||||
} else {
|
||||
snapshot.append_color(
|
||||
&gdk::RGBA::BLACK,
|
||||
&graphene::Rect::new(0f32, 0f32, width as f32, height as f32),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct CameraPaintable(ObjectSubclass<imp::CameraPaintable>)
|
||||
@implements gdk::Paintable;
|
||||
}
|
||||
|
||||
impl CameraPaintable {
|
||||
pub fn new(sender: Sender<CameraEvent>) -> Self {
|
||||
let paintable = glib::Object::new::<Self>();
|
||||
paintable.imp().sender.replace(Some(sender));
|
||||
paintable
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
let sink = gst::ElementFactory::make_with_name("gtk4paintablesink", None)?;
|
||||
let paintable = sink.property::<gdk::Paintable>("paintable");
|
||||
|
||||
paintable.connect_invalidate_contents(clone!(@weak self as pt => move |_| {
|
||||
pt.invalidate_contents();
|
||||
}));
|
||||
|
||||
paintable.connect_invalidate_size(clone!(@weak self as pt => move |_| {
|
||||
pt.invalidate_size();
|
||||
}));
|
||||
let tee = gst::ElementFactory::make_with_name("tee", None)?;
|
||||
let videoconvert = gst::ElementFactory::make_with_name("videoconvert", None)?;
|
||||
let queue1 = gst::ElementFactory::make_with_name("queue", None)?;
|
||||
let queue2 = gst::ElementFactory::make_with_name("queue", None)?;
|
||||
let zbar = gst::ElementFactory::make_with_name("zbar", None)?;
|
||||
let fakesink = gst::ElementFactory::make_with_name("fakesink", None)?;
|
||||
|
||||
let videoflip = gst::ElementFactory::make("videoflip")
|
||||
.property("video-direction", gst_video::VideoOrientationMethod::Auto)
|
||||
.build()?;
|
||||
|
||||
let sink = if paintable
|
||||
.property::<Option<gdk::GLContext>>("gl-context")
|
||||
.is_some()
|
||||
{
|
||||
gst::ElementFactory::make("glsinkbin")
|
||||
.property("sink", &sink)
|
||||
.build()?
|
||||
} else {
|
||||
let bin = gst::Bin::default();
|
||||
let convert = gst::ElementFactory::make_with_name("videoconvert", None)?;
|
||||
|
||||
bin.add(&convert)?;
|
||||
bin.add(&sink)?;
|
||||
convert.link(&sink)?;
|
||||
|
||||
bin.add_pad(
|
||||
&gst::GhostPad::builder_with_target(&convert.static_pad("sink").unwrap())?
|
||||
.name("sink")
|
||||
.build(),
|
||||
)?;
|
||||
|
||||
bin.upcast()
|
||||
};
|
||||
imp.sink_paintable.replace(Some(paintable));
|
||||
|
||||
pipeline.add_many([
|
||||
pipewire_src,
|
||||
&tee,
|
||||
&queue1,
|
||||
&videoconvert,
|
||||
&zbar,
|
||||
&fakesink,
|
||||
&queue2,
|
||||
&videoflip,
|
||||
&sink,
|
||||
])?;
|
||||
|
||||
gst::Element::link_many([pipewire_src, &tee, &queue1, &videoconvert, &zbar, &fakesink])?;
|
||||
tee.link_pads(None, &queue2, None)?;
|
||||
gst::Element::link_many([&queue2, &videoflip, &sink])?;
|
||||
|
||||
let bus = pipeline.bus().unwrap();
|
||||
let guard = bus.add_watch_local(
|
||||
clone!(@weak self as paintable => @default-return glib::ControlFlow::Break, move |_, msg| {
|
||||
use gst::MessageView;
|
||||
let sender = paintable.imp().sender.borrow().as_ref().unwrap().clone();
|
||||
match msg.view() {
|
||||
MessageView::Error(err) => {
|
||||
tracing::error!(
|
||||
"Error from {:?}: {} ({:?})",
|
||||
err.src().map(|s| s.path_string()),
|
||||
err.error(),
|
||||
err.debug()
|
||||
);
|
||||
}
|
||||
MessageView::StateChanged(state) => {
|
||||
if Some(&*PIPELINE_NAME) == state.src().map(|s| s.name()).as_ref() {
|
||||
let structure = state.structure().unwrap();
|
||||
let new_state = structure.get::<gst::State>("new-state")
|
||||
.unwrap();
|
||||
if new_state == gst::State::Playing {
|
||||
sender.send(CameraEvent::StreamStarted).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
MessageView::Element(e) => {
|
||||
if let Some(s) = e.structure() {
|
||||
if let Ok(symbol) = s.get::<String>("symbol") {
|
||||
sender.send(CameraEvent::CodeDetected(symbol)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
glib::ControlFlow::Continue
|
||||
|
||||
}),
|
||||
)?;
|
||||
imp.guard.replace(Some(guard));
|
||||
imp.pipeline.replace(Some(pipeline));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn close_pipeline(&self) {
|
||||
tracing::debug!("Closing pipeline");
|
||||
if let Some(pipeline) = self.imp().pipeline.take() {
|
||||
if let Err(err) = pipeline.set_state(gst::State::Null) {
|
||||
tracing::error!("Failed to close the pipeline: {err}");
|
||||
}
|
||||
}
|
||||
let _ = self.imp().guard.take();
|
||||
}
|
||||
|
||||
pub fn start(&self) {
|
||||
if let Some(pipeline) = &*self.imp().pipeline.borrow() {
|
||||
if let Err(err) = pipeline.set_state(gst::State::Playing) {
|
||||
tracing::error!("Failed to start the camera stream: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
if let Some(pipeline) = &*self.imp().pipeline.borrow() {
|
||||
if let Err(err) = pipeline.set_state(gst::State::Null) {
|
||||
tracing::error!("Failed to stop the camera stream: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,5 @@
|
|||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||
|
||||
#[derive(Clone, glib::Boxed)]
|
||||
#[boxed_type(name = "CameraItem")]
|
||||
pub struct CameraItem {
|
||||
pub nick: String,
|
||||
pub node_id: u32,
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
|
@ -59,8 +52,4 @@ impl CameraRow {
|
|||
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,5 @@
|
|||
mod accounts;
|
||||
mod camera;
|
||||
mod camera_paintable;
|
||||
mod camera_row;
|
||||
mod error_revealer;
|
||||
mod keyring_error_dialog;
|
||||
|
@ -12,9 +11,8 @@ mod window;
|
|||
|
||||
pub use self::{
|
||||
accounts::{AccountAddDialog, QRCodeData},
|
||||
camera::{screenshot, Camera, CameraEvent},
|
||||
camera_paintable::CameraPaintable,
|
||||
camera_row::{CameraItem, CameraRow},
|
||||
camera::{screenshot, Camera},
|
||||
camera_row::CameraRow,
|
||||
error_revealer::ErrorRevealer,
|
||||
keyring_error_dialog::KeyringErrorDialog,
|
||||
preferences::PreferencesWindow,
|
||||
|
|
|
@ -531,7 +531,6 @@ impl PreferencesWindow {
|
|||
let close_page = gio::ActionEntry::builder("close_page")
|
||||
.activate(clone!(@weak self as win => move |_, _, _| {
|
||||
win.pop_subpage();
|
||||
win.imp().camera_page.imp().camera.stop();
|
||||
}))
|
||||
.build();
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue