Browse Source
This also moves the error handling to the `IdentityVerification`merge-requests/1327/merge
15 changed files with 2213 additions and 147 deletions
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<interface> |
||||
<template class="QrCodeScanner" parent="AdwBin"> |
||||
<property name="child"> |
||||
<object class="GtkPicture" id="picture"> |
||||
<property name="hexpand">True</property> |
||||
<property name="vexpand">True</property> |
||||
<property name="keep-aspect-ratio">False</property> |
||||
<property name="height-request">400</property> |
||||
</object> |
||||
</property> |
||||
</template> |
||||
</interface> |
||||
@ -1,3 +1,5 @@
|
||||
mod qr_code; |
||||
mod qr_code_scanner; |
||||
|
||||
pub use self::qr_code::{QRCode, QRCodeExt}; |
||||
pub use self::qr_code_scanner::{screenshot, QrCodeScanner}; |
||||
|
||||
@ -0,0 +1,461 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Fancy Camera with QR code detection
|
||||
//
|
||||
// Pipeline:
|
||||
// queue -- videoconvert -- QrCodeDetector sink
|
||||
// /
|
||||
// pipewiresrc -- tee
|
||||
// \
|
||||
// queue -- videoconvert -- our fancy sink
|
||||
|
||||
use glib::{clone, Receiver, Sender}; |
||||
use gst::prelude::*; |
||||
use gtk::glib; |
||||
use gtk::prelude::*; |
||||
use gtk::subclass::prelude::*; |
||||
use once_cell::sync::Lazy; |
||||
|
||||
use std::os::unix::io::AsRawFd; |
||||
use std::sync::{Arc, Mutex}; |
||||
|
||||
use crate::contrib::qr_code_scanner::qr_code_detector::QrCodeDetector; |
||||
use crate::contrib::qr_code_scanner::QrVerificationDataBoxed; |
||||
use gtk::{gdk, graphene}; |
||||
use matrix_sdk::encryption::verification::QrVerificationData; |
||||
|
||||
pub enum Action { |
||||
FrameChanged, |
||||
QrCodeDetected(QrVerificationData), |
||||
} |
||||
|
||||
mod camera_sink { |
||||
use std::convert::AsRef; |
||||
|
||||
#[derive(Debug)] |
||||
pub struct Frame(pub gst_video::VideoFrame<gst_video::video_frame::Readable>); |
||||
|
||||
impl AsRef<[u8]> for Frame { |
||||
fn as_ref(&self) -> &[u8] { |
||||
self.0.plane_data(0).unwrap() |
||||
} |
||||
} |
||||
|
||||
impl From<Frame> for gdk::Paintable { |
||||
fn from(f: Frame) -> gdk::Paintable { |
||||
let format = match f.0.format() { |
||||
gst_video::VideoFormat::Bgra => gdk::MemoryFormat::B8g8r8a8, |
||||
gst_video::VideoFormat::Argb => gdk::MemoryFormat::A8r8g8b8, |
||||
gst_video::VideoFormat::Rgba => gdk::MemoryFormat::R8g8b8a8, |
||||
gst_video::VideoFormat::Abgr => gdk::MemoryFormat::A8b8g8r8, |
||||
gst_video::VideoFormat::Rgb => gdk::MemoryFormat::R8g8b8, |
||||
gst_video::VideoFormat::Bgr => gdk::MemoryFormat::B8g8r8, |
||||
_ => unreachable!(), |
||||
}; |
||||
let width = f.0.width() as i32; |
||||
let height = f.0.height() as i32; |
||||
let rowstride = f.0.plane_stride()[0] as usize; |
||||
|
||||
gdk::MemoryTexture::new( |
||||
width, |
||||
height, |
||||
format, |
||||
&glib::Bytes::from_owned(f), |
||||
rowstride, |
||||
) |
||||
.upcast() |
||||
} |
||||
} |
||||
|
||||
impl Frame { |
||||
pub fn new(buffer: &gst::Buffer, info: &gst_video::VideoInfo) -> Self { |
||||
let video_frame = |
||||
gst_video::VideoFrame::from_buffer_readable(buffer.clone(), &info).unwrap(); |
||||
Self(video_frame) |
||||
} |
||||
|
||||
pub fn width(&self) -> u32 { |
||||
self.0.width() |
||||
} |
||||
|
||||
pub fn height(&self) -> u32 { |
||||
self.0.height() |
||||
} |
||||
} |
||||
|
||||
use super::*; |
||||
|
||||
mod imp { |
||||
use std::sync::Mutex; |
||||
|
||||
use gst::subclass::prelude::*; |
||||
use gst_base::subclass::prelude::*; |
||||
use gst_video::subclass::prelude::*; |
||||
use once_cell::sync::Lazy; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Default)] |
||||
pub struct CameraSink { |
||||
pub info: Mutex<Option<gst_video::VideoInfo>>, |
||||
pub sender: Mutex<Option<Sender<Action>>>, |
||||
pub pending_frame: Mutex<Option<Frame>>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for CameraSink { |
||||
const NAME: &'static str = "CameraSink"; |
||||
type Type = super::CameraSink; |
||||
type ParentType = gst_video::VideoSink; |
||||
} |
||||
|
||||
impl ObjectImpl for CameraSink {} |
||||
impl ElementImpl for CameraSink { |
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { |
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| { |
||||
gst::subclass::ElementMetadata::new( |
||||
"GTK Camera Sink", |
||||
"Sink/Camera/Video", |
||||
"A GTK Camera sink", |
||||
"Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>", |
||||
) |
||||
}); |
||||
|
||||
Some(&*ELEMENT_METADATA) |
||||
} |
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] { |
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| { |
||||
let caps = gst_video::video_make_raw_caps(&[ |
||||
gst_video::VideoFormat::Bgra, |
||||
gst_video::VideoFormat::Argb, |
||||
gst_video::VideoFormat::Rgba, |
||||
gst_video::VideoFormat::Abgr, |
||||
gst_video::VideoFormat::Rgb, |
||||
gst_video::VideoFormat::Bgr, |
||||
]) |
||||
.any_features() |
||||
.build(); |
||||
|
||||
vec![gst::PadTemplate::new( |
||||
"sink", |
||||
gst::PadDirection::Sink, |
||||
gst::PadPresence::Always, |
||||
&caps, |
||||
) |
||||
.unwrap()] |
||||
}); |
||||
|
||||
PAD_TEMPLATES.as_ref() |
||||
} |
||||
} |
||||
impl BaseSinkImpl for CameraSink { |
||||
fn set_caps( |
||||
&self, |
||||
_element: &Self::Type, |
||||
caps: &gst::Caps, |
||||
) -> Result<(), gst::LoggableError> { |
||||
let video_info = gst_video::VideoInfo::from_caps(caps).unwrap(); |
||||
let mut info = self.info.lock().unwrap(); |
||||
info.replace(video_info); |
||||
|
||||
Ok(()) |
||||
} |
||||
} |
||||
impl VideoSinkImpl for CameraSink { |
||||
fn show_frame( |
||||
&self, |
||||
_element: &Self::Type, |
||||
buffer: &gst::Buffer, |
||||
) -> Result<gst::FlowSuccess, gst::FlowError> { |
||||
if let Some(info) = &*self.info.lock().unwrap() { |
||||
let frame = Frame::new(buffer, info); |
||||
let mut last_frame = self.pending_frame.lock().unwrap(); |
||||
|
||||
last_frame.replace(frame); |
||||
let sender = self.sender.lock().unwrap(); |
||||
|
||||
sender.as_ref().unwrap().send(Action::FrameChanged).unwrap(); |
||||
} |
||||
Ok(gst::FlowSuccess::Ok) |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
pub struct CameraSink(ObjectSubclass<imp::CameraSink>) @extends gst_video::VideoSink, gst_base::BaseSink, gst::Element, gst::Object; |
||||
} |
||||
unsafe impl Send for CameraSink {} |
||||
unsafe impl Sync for CameraSink {} |
||||
|
||||
impl CameraSink { |
||||
pub fn new(sender: Sender<Action>) -> Self { |
||||
let sink = glib::Object::new(&[]).expect("Failed to create a CameraSink"); |
||||
let priv_ = imp::CameraSink::from_instance(&sink); |
||||
priv_.sender.lock().unwrap().replace(sender); |
||||
sink |
||||
} |
||||
|
||||
pub fn pending_frame(&self) -> Option<Frame> { |
||||
let self_ = imp::CameraSink::from_instance(self); |
||||
self_.pending_frame.lock().unwrap().take() |
||||
} |
||||
} |
||||
} |
||||
|
||||
mod imp { |
||||
use glib::subclass; |
||||
use std::cell::RefCell; |
||||
|
||||
use super::*; |
||||
|
||||
pub struct CameraPaintable { |
||||
pub sink: camera_sink::CameraSink, |
||||
pub detector: QrCodeDetector, |
||||
pub pipeline: RefCell<Option<gst::Pipeline>>, |
||||
pub sender: Sender<Action>, |
||||
pub image: RefCell<Option<gdk::Paintable>>, |
||||
pub size: RefCell<Option<(u32, u32)>>, |
||||
pub receiver: RefCell<Option<Receiver<Action>>>, |
||||
} |
||||
|
||||
impl Default for CameraPaintable { |
||||
fn default() -> Self { |
||||
let (sender, r) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); |
||||
let receiver = RefCell::new(Some(r)); |
||||
|
||||
Self { |
||||
pipeline: RefCell::default(), |
||||
sink: camera_sink::CameraSink::new(sender.clone()), |
||||
detector: QrCodeDetector::new(sender.clone()), |
||||
image: RefCell::new(None), |
||||
sender, |
||||
receiver, |
||||
size: RefCell::new(None), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for CameraPaintable { |
||||
const NAME: &'static str = "CameraPaintable"; |
||||
type Type = super::CameraPaintable; |
||||
type ParentType = glib::Object; |
||||
type Interfaces = (gdk::Paintable,); |
||||
} |
||||
|
||||
impl ObjectImpl for CameraPaintable { |
||||
fn constructed(&self, obj: &Self::Type) { |
||||
obj.init_widgets(); |
||||
self.parent_constructed(obj); |
||||
} |
||||
fn dispose(&self, paintable: &Self::Type) { |
||||
paintable.close_pipeline(); |
||||
} |
||||
|
||||
fn signals() -> &'static [subclass::Signal] { |
||||
static SIGNALS: Lazy<Vec<subclass::Signal>> = Lazy::new(|| { |
||||
vec![subclass::Signal::builder( |
||||
"code-detected", |
||||
&[QrVerificationDataBoxed::static_type().into()], |
||||
glib::Type::UNIT.into(), |
||||
) |
||||
.flags(glib::SignalFlags::RUN_FIRST) |
||||
.build()] |
||||
}); |
||||
SIGNALS.as_ref() |
||||
} |
||||
} |
||||
|
||||
impl PaintableImpl for CameraPaintable { |
||||
fn intrinsic_height(&self, _paintable: &Self::Type) -> i32 { |
||||
if let Some((_, height)) = *self.size.borrow() { |
||||
height as i32 |
||||
} else { |
||||
0 |
||||
} |
||||
} |
||||
fn intrinsic_width(&self, _paintable: &Self::Type) -> i32 { |
||||
if let Some((width, _)) = *self.size.borrow() { |
||||
width as i32 |
||||
} else { |
||||
0 |
||||
} |
||||
} |
||||
|
||||
fn snapshot( |
||||
&self, |
||||
_paintable: &Self::Type, |
||||
snapshot: &gdk::Snapshot, |
||||
width: f64, |
||||
height: f64, |
||||
) { |
||||
let snapshot = snapshot.downcast_ref::<gtk::Snapshot>().unwrap(); |
||||
|
||||
if let Some(ref image) = *self.image.borrow() { |
||||
// Transformation to avoid stretching the camera. We translate and scale the image
|
||||
// under a clip.
|
||||
let clip = graphene::Rect::new(0.0, 0.0, width as f32, height as f32); |
||||
|
||||
let aspect = width / height.max(std::f64::EPSILON); // Do not divide by zero.
|
||||
let image_aspect = image.intrinsic_aspect_ratio(); |
||||
|
||||
snapshot.push_clip(&clip); |
||||
|
||||
if image_aspect == 0.0 { |
||||
image.snapshot(snapshot.upcast_ref(), 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.upcast_ref(), new_width, new_height); |
||||
|
||||
snapshot.pop(); |
||||
} 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 Default for CameraPaintable { |
||||
fn default() -> Self { |
||||
glib::Object::new(&[]).expect("Failed to create a CameraPaintable") |
||||
} |
||||
} |
||||
|
||||
impl CameraPaintable { |
||||
pub fn set_pipewire_fd<F: AsRawFd>(&self, fd: F, node_id: u32) { |
||||
let pipewire_element = gst::ElementFactory::make("pipewiresrc", None).unwrap(); |
||||
pipewire_element |
||||
.set_property("fd", &fd.as_raw_fd()) |
||||
.unwrap(); |
||||
pipewire_element |
||||
.set_property("path", &node_id.to_string()) |
||||
.unwrap(); |
||||
self.init_pipeline(pipewire_element); |
||||
} |
||||
|
||||
fn init_pipeline(&self, pipewire_src: gst::Element) { |
||||
let self_ = imp::CameraPaintable::from_instance(self); |
||||
let pipeline = gst::Pipeline::new(None); |
||||
|
||||
let tee = gst::ElementFactory::make("tee", None).unwrap(); |
||||
let queue = gst::ElementFactory::make("queue", None).unwrap(); |
||||
let videoconvert1 = gst::ElementFactory::make("videoconvert", None).unwrap(); |
||||
let videoconvert2 = gst::ElementFactory::make("videoconvert", None).unwrap(); |
||||
let src_pad = queue.static_pad("src").unwrap(); |
||||
|
||||
// Reduce the number of frames we use to get the qrcode from
|
||||
let start = Arc::new(Mutex::new(std::time::Instant::now())); |
||||
src_pad.add_probe(gst::PadProbeType::BUFFER, move |_, _| { |
||||
let mut start = start.lock().unwrap(); |
||||
if start.elapsed() < std::time::Duration::from_millis(500) { |
||||
gst::PadProbeReturn::Drop |
||||
} else { |
||||
*start = std::time::Instant::now(); |
||||
gst::PadProbeReturn::Ok |
||||
} |
||||
}); |
||||
|
||||
let queue2 = gst::ElementFactory::make("queue", None).unwrap(); |
||||
|
||||
pipeline |
||||
.add_many(&[ |
||||
&pipewire_src, |
||||
&tee, |
||||
&queue, |
||||
&videoconvert1, |
||||
self_.detector.upcast_ref(), |
||||
&queue2, |
||||
&videoconvert2, |
||||
self_.sink.upcast_ref(), |
||||
]) |
||||
.unwrap(); |
||||
|
||||
gst::Element::link_many(&[ |
||||
&pipewire_src, |
||||
&tee, |
||||
&queue, |
||||
&videoconvert1, |
||||
self_.detector.upcast_ref(), |
||||
]) |
||||
.unwrap(); |
||||
|
||||
tee.link_pads(None, &queue2, None).unwrap(); |
||||
gst::Element::link_many(&[&queue2, &videoconvert2, self_.sink.upcast_ref()]).unwrap(); |
||||
let bus = pipeline.bus().unwrap(); |
||||
bus.add_watch_local( |
||||
clone!(@weak self as paintable => @default-return glib::Continue(false), move |_, msg| { |
||||
match msg.view() { |
||||
gst::MessageView::Error(err) => { |
||||
log::error!( |
||||
"Error from {:?}: {} ({:?})", |
||||
err.src().map(|s| s.path_string()), |
||||
err.error(), |
||||
err.debug() |
||||
); |
||||
}, |
||||
_ => (), |
||||
} |
||||
glib::Continue(true) |
||||
}), |
||||
) |
||||
.expect("Failed to add bus watch"); |
||||
pipeline.set_state(gst::State::Playing).ok(); |
||||
self_.pipeline.replace(Some(pipeline)); |
||||
} |
||||
|
||||
pub fn close_pipeline(&self) { |
||||
let self_ = imp::CameraPaintable::from_instance(self); |
||||
if let Some(pipeline) = self_.pipeline.borrow_mut().take() { |
||||
pipeline.set_state(gst::State::Null).unwrap(); |
||||
} |
||||
} |
||||
|
||||
pub fn init_widgets(&self) { |
||||
let self_ = imp::CameraPaintable::from_instance(self); |
||||
|
||||
let receiver = self_.receiver.borrow_mut().take().unwrap(); |
||||
receiver.attach( |
||||
None, |
||||
glib::clone!(@weak self as paintable => @default-return glib::Continue(false), move |action| paintable.do_action(action)), |
||||
); |
||||
} |
||||
|
||||
fn do_action(&self, action: Action) -> glib::Continue { |
||||
let self_ = imp::CameraPaintable::from_instance(self); |
||||
match action { |
||||
Action::FrameChanged => { |
||||
if let Some(frame) = self_.sink.pending_frame() { |
||||
let (width, height) = (frame.width(), frame.height()); |
||||
self_.size.replace(Some((width, height))); |
||||
self_.image.replace(Some(frame.into())); |
||||
self.invalidate_contents(); |
||||
} |
||||
} |
||||
Action::QrCodeDetected(code) => { |
||||
self.emit_by_name("code-detected", &[&QrVerificationDataBoxed(code)]) |
||||
.unwrap(); |
||||
} |
||||
} |
||||
glib::Continue(true) |
||||
} |
||||
} |
||||
@ -0,0 +1,199 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use crate::spawn; |
||||
use ashpd::{desktop::camera, zbus}; |
||||
use glib::clone; |
||||
use glib::subclass; |
||||
use gtk::glib; |
||||
use gtk::prelude::*; |
||||
use gtk::subclass::prelude::*; |
||||
use matrix_sdk::encryption::verification::QrVerificationData; |
||||
use std::os::unix::prelude::RawFd; |
||||
|
||||
mod camera_paintable; |
||||
mod qr_code_detector; |
||||
pub mod screenshot; |
||||
|
||||
use camera_paintable::CameraPaintable; |
||||
|
||||
mod imp { |
||||
use adw::subclass::prelude::*; |
||||
use gtk::CompositeTemplate; |
||||
use once_cell::sync::Lazy; |
||||
use std::cell::Cell; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, CompositeTemplate, Default)] |
||||
#[template(resource = "/org/gnome/FractalNext/qr-code-scanner.ui")] |
||||
pub struct QrCodeScanner { |
||||
pub paintable: CameraPaintable, |
||||
#[template_child] |
||||
pub picture: TemplateChild<gtk::Picture>, |
||||
pub has_camera: Cell<bool>, |
||||
pub is_started: Cell<bool>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for QrCodeScanner { |
||||
const NAME: &'static str = "QrCodeScanner"; |
||||
type Type = super::QrCodeScanner; |
||||
type ParentType = adw::Bin; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
Self::bind_template(klass); |
||||
} |
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
impl ObjectImpl for QrCodeScanner { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![glib::ParamSpec::new_boolean( |
||||
"has-camera", |
||||
"Has Camera", |
||||
"Whether we have a working camera", |
||||
false, |
||||
glib::ParamFlags::READABLE, |
||||
)] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
match pspec.name() { |
||||
"has-camera" => obj.has_camera().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn constructed(&self, obj: &Self::Type) { |
||||
self.picture.set_paintable(Some(&self.paintable)); |
||||
|
||||
let callback = glib::clone!(@weak obj => @default-return None, move |args: &[glib::Value]| { |
||||
let code = args.get(1).unwrap().get::<QrVerificationDataBoxed>().unwrap(); |
||||
obj.emit_by_name("code-detected", &[&code]).unwrap(); |
||||
|
||||
None |
||||
}); |
||||
self.paintable |
||||
.connect_local("code-detected", false, callback) |
||||
.unwrap(); |
||||
obj.init_has_camera(); |
||||
} |
||||
|
||||
fn signals() -> &'static [subclass::Signal] { |
||||
static SIGNALS: Lazy<Vec<subclass::Signal>> = Lazy::new(|| { |
||||
vec![subclass::Signal::builder( |
||||
"code-detected", |
||||
&[QrVerificationDataBoxed::static_type().into()], |
||||
glib::Type::UNIT.into(), |
||||
) |
||||
.flags(glib::SignalFlags::RUN_FIRST) |
||||
.build()] |
||||
}); |
||||
SIGNALS.as_ref() |
||||
} |
||||
} |
||||
impl WidgetImpl for QrCodeScanner { |
||||
fn unmap(&self, widget: &Self::Type) { |
||||
self.parent_unmap(widget); |
||||
widget.stop(); |
||||
} |
||||
} |
||||
impl BinImpl for QrCodeScanner {} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
pub struct QrCodeScanner(ObjectSubclass<imp::QrCodeScanner>) @extends gtk::Widget, adw::Bin; |
||||
} |
||||
|
||||
impl QrCodeScanner { |
||||
#[allow(clippy::new_without_default)] |
||||
pub fn new() -> Self { |
||||
glib::Object::new(&[]).expect("Failed to create a QrCodeScanner") |
||||
} |
||||
|
||||
pub fn stop(&self) { |
||||
let self_ = imp::QrCodeScanner::from_instance(self); |
||||
|
||||
self_.paintable.close_pipeline(); |
||||
} |
||||
|
||||
async fn start_internal(&self) -> bool { |
||||
let self_ = imp::QrCodeScanner::from_instance(self); |
||||
if let Ok(Some(stream_fd)) = stream().await { |
||||
if let Ok(node_id) = camera::pipewire_node_id(stream_fd).await { |
||||
self_.paintable.set_pipewire_fd(stream_fd, node_id); |
||||
self_.has_camera.set(true); |
||||
self.notify("has-camera"); |
||||
return true; |
||||
} |
||||
} |
||||
self_.has_camera.set(false); |
||||
self.notify("has-camera"); |
||||
false |
||||
} |
||||
|
||||
pub async fn start(&self) -> bool { |
||||
let priv_ = imp::QrCodeScanner::from_instance(self); |
||||
let is_started = self.start_internal().await; |
||||
priv_.is_started.set(is_started); |
||||
is_started |
||||
} |
||||
|
||||
fn init_has_camera(&self) { |
||||
spawn!(clone!(@weak self as obj => async move { |
||||
let priv_ = imp::QrCodeScanner::from_instance(&obj); |
||||
let has_camera = if obj.start_internal().await { |
||||
if !priv_.is_started.get() { |
||||
obj.stop(); |
||||
} |
||||
true |
||||
} else { |
||||
false |
||||
}; |
||||
priv_.has_camera.set(has_camera); |
||||
obj.notify("has-camera"); |
||||
})); |
||||
} |
||||
|
||||
pub fn has_camera(&self) -> bool { |
||||
let priv_ = imp::QrCodeScanner::from_instance(self); |
||||
priv_.has_camera.get() |
||||
} |
||||
|
||||
/// Connects the prepared signals to the function f given in input
|
||||
pub fn connect_code_detected<F: Fn(&Self, QrVerificationData) + 'static>( |
||||
&self, |
||||
f: F, |
||||
) -> glib::SignalHandlerId { |
||||
self.connect_local("code-detected", true, move |values| { |
||||
let obj = values[0].get::<Self>().unwrap(); |
||||
let data = values[1].get::<QrVerificationDataBoxed>().unwrap(); |
||||
|
||||
f(&obj, data.0); |
||||
|
||||
None |
||||
}) |
||||
.unwrap() |
||||
} |
||||
} |
||||
|
||||
async fn stream() -> Result<Option<RawFd>, ashpd::Error> { |
||||
let connection = zbus::Connection::session().await?; |
||||
let proxy = camera::CameraProxy::new(&connection).await?; |
||||
|
||||
if proxy.is_camera_present().await? { |
||||
proxy.access_camera().await?; |
||||
Ok(Some(proxy.open_pipe_wire_remote().await?)) |
||||
} else { |
||||
Ok(None) |
||||
} |
||||
} |
||||
|
||||
#[derive(Clone, Debug, PartialEq, glib::GBoxed)] |
||||
#[gboxed(type_name = "QrVerificationDataBoxed")] |
||||
struct QrVerificationDataBoxed(QrVerificationData); |
||||
@ -0,0 +1,140 @@
|
||||
use crate::contrib::qr_code_scanner::camera_paintable::Action; |
||||
use matrix_sdk::encryption::verification::QrVerificationData; |
||||
|
||||
use glib::Sender; |
||||
use gst_video::{video_frame::VideoFrameRef, VideoInfo}; |
||||
use log::debug; |
||||
use std::convert::AsRef; |
||||
|
||||
use super::*; |
||||
|
||||
mod imp { |
||||
use std::sync::Mutex; |
||||
|
||||
use gst::subclass::prelude::*; |
||||
use gst_base::subclass::prelude::*; |
||||
use gst_video::subclass::prelude::*; |
||||
use once_cell::sync::Lazy; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Default)] |
||||
pub struct QrCodeDetector { |
||||
pub info: Mutex<Option<VideoInfo>>, |
||||
pub sender: Mutex<Option<Sender<Action>>>, |
||||
pub code: Mutex<Option<QrVerificationData>>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for QrCodeDetector { |
||||
const NAME: &'static str = "QrCodeDetector"; |
||||
type Type = super::QrCodeDetector; |
||||
type ParentType = gst_video::VideoSink; |
||||
} |
||||
|
||||
impl ObjectImpl for QrCodeDetector {} |
||||
impl ElementImpl for QrCodeDetector { |
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { |
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| { |
||||
gst::subclass::ElementMetadata::new( |
||||
"Matrix Qr Code detector Sink", |
||||
"Sink/Video/QrCode/Matrix", |
||||
"A Qr code detector for Matrix", |
||||
"Julian Sparber <julian@sparber.net>", |
||||
) |
||||
}); |
||||
|
||||
Some(&*ELEMENT_METADATA) |
||||
} |
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] { |
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| { |
||||
let caps = gst_video::video_make_raw_caps(&[gst_video::VideoFormat::Gray8]) |
||||
.any_features() |
||||
.build(); |
||||
|
||||
vec![gst::PadTemplate::new( |
||||
"sink", |
||||
gst::PadDirection::Sink, |
||||
gst::PadPresence::Always, |
||||
&caps, |
||||
) |
||||
.unwrap()] |
||||
}); |
||||
|
||||
PAD_TEMPLATES.as_ref() |
||||
} |
||||
} |
||||
impl BaseSinkImpl for QrCodeDetector { |
||||
fn set_caps( |
||||
&self, |
||||
_element: &Self::Type, |
||||
caps: &gst::Caps, |
||||
) -> Result<(), gst::LoggableError> { |
||||
let video_info = gst_video::VideoInfo::from_caps(caps).unwrap(); |
||||
let mut info = self.info.lock().unwrap(); |
||||
info.replace(video_info); |
||||
|
||||
Ok(()) |
||||
} |
||||
} |
||||
impl VideoSinkImpl for QrCodeDetector { |
||||
fn show_frame( |
||||
&self, |
||||
_element: &Self::Type, |
||||
buffer: &gst::Buffer, |
||||
) -> Result<gst::FlowSuccess, gst::FlowError> { |
||||
let now = std::time::Instant::now(); |
||||
|
||||
if let Some(info) = &*self.info.lock().unwrap() { |
||||
let frame = VideoFrameRef::from_buffer_ref_readable(buffer, info).unwrap(); |
||||
|
||||
let mut samples = image::FlatSamples::<Vec<u8>> { |
||||
samples: frame.plane_data(0).unwrap().to_vec(), |
||||
layout: image::flat::SampleLayout { |
||||
channels: 1, |
||||
channel_stride: 1, |
||||
width: frame.width(), |
||||
width_stride: 1, |
||||
height: frame.height(), |
||||
height_stride: frame.plane_stride()[0] as usize, |
||||
}, |
||||
color_hint: Some(image::ColorType::L8), |
||||
}; |
||||
|
||||
let image = samples.as_view_mut::<image::Luma<u8>>().unwrap(); |
||||
|
||||
if let Ok(code) = QrVerificationData::from_luma(image) { |
||||
let mut previous_code = self.code.lock().unwrap(); |
||||
if previous_code.as_ref() != Some(&code) { |
||||
previous_code.replace(code.clone()); |
||||
let sender = self.sender.lock().unwrap(); |
||||
sender |
||||
.as_ref() |
||||
.unwrap() |
||||
.send(Action::QrCodeDetected(code)) |
||||
.unwrap(); |
||||
} |
||||
} |
||||
} |
||||
debug!("Spend {}ms to detect qr code", now.elapsed().as_millis()); |
||||
|
||||
Ok(gst::FlowSuccess::Ok) |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
pub struct QrCodeDetector(ObjectSubclass<imp::QrCodeDetector>) @extends gst_video::VideoSink, gst_base::BaseSink, gst::Element, gst::Object; |
||||
} |
||||
unsafe impl Send for QrCodeDetector {} |
||||
unsafe impl Sync for QrCodeDetector {} |
||||
|
||||
impl QrCodeDetector { |
||||
pub fn new(sender: Sender<Action>) -> Self { |
||||
let sink = glib::Object::new(&[]).expect("Failed to create a QrCodeDetector"); |
||||
let priv_ = imp::QrCodeDetector::from_instance(&sink); |
||||
priv_.sender.lock().unwrap().replace(sender); |
||||
sink |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@
|
||||
use ashpd::desktop::screenshot; |
||||
use gtk::gio; |
||||
use gtk::prelude::*; |
||||
use matrix_sdk::encryption::verification::QrVerificationData; |
||||
|
||||
pub async fn capture(root: >k::Root) -> Option<QrVerificationData> { |
||||
let identifier = ashpd::WindowIdentifier::from_native(root).await; |
||||
let uri = screenshot::take(&identifier, true, true).await.ok()?; |
||||
let screenshot = gio::File::for_uri(&uri); |
||||
let (data, _) = screenshot.load_contents(gio::NONE_CANCELLABLE).ok()?; |
||||
let image = image::load_from_memory(&data).ok()?; |
||||
|
||||
QrVerificationData::from_image(image).ok() |
||||
} |
||||
Loading…
Reference in new issue