fractal/contrib/qr_code_scanner/camera/camera_paintable/
linux.rsuse std::{
cell::Cell,
os::unix::io::AsRawFd,
sync::{Arc, Mutex},
};
use ashpd::desktop::camera;
use futures_channel::mpsc;
use futures_util::StreamExt;
use gst::{bus::BusWatchGuard, prelude::*};
use gtk::{
gdk, glib,
glib::{clone, subclass::prelude::*},
graphene,
prelude::*,
subclass::prelude::*,
};
use tracing::{debug, error};
use super::{Action, CameraPaintable, CameraPaintableImpl};
use crate::{
contrib::qr_code_scanner::{qr_code_detector::QrCodeDetector, QrVerificationDataBoxed},
spawn,
};
mod imp {
use std::cell::RefCell;
use super::*;
#[derive(Debug, Default)]
pub struct LinuxCameraPaintable {
pub pipeline: RefCell<Option<(gst::Pipeline, BusWatchGuard)>>,
pub sink_paintable: RefCell<Option<gdk::Paintable>>,
}
#[glib::object_subclass]
impl ObjectSubclass for LinuxCameraPaintable {
const NAME: &'static str = "LinuxCameraPaintable";
type Type = super::LinuxCameraPaintable;
type ParentType = CameraPaintable;
type Interfaces = (gdk::Paintable,);
}
impl ObjectImpl for LinuxCameraPaintable {
fn dispose(&self) {
self.obj().set_pipeline(None);
}
}
impl CameraPaintableImpl for LinuxCameraPaintable {}
impl PaintableImpl for LinuxCameraPaintable {
fn intrinsic_height(&self) -> i32 {
if let Some(paintable) = self.sink_paintable.borrow().as_ref() {
paintable.intrinsic_height()
} else {
0
}
}
fn intrinsic_width(&self) -> i32 {
if let Some(paintable) = self.sink_paintable.borrow().as_ref() {
paintable.intrinsic_width()
} else {
0
}
}
fn snapshot(&self, snapshot: &gdk::Snapshot, width: f64, height: f64) {
let snapshot = snapshot.downcast_ref::<gtk::Snapshot>().unwrap();
let paintable = self.sink_paintable.borrow();
let Some(image) = paintable.as_ref() else {
return;
};
let aspect = width / height.max(f64::EPSILON); 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), false => (width, width / image_aspect), };
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);
}
}
}
glib::wrapper! {
pub struct LinuxCameraPaintable(ObjectSubclass<imp::LinuxCameraPaintable>)
@extends CameraPaintable, @implements gdk::Paintable;
}
impl LinuxCameraPaintable {
pub async fn new<F: AsRawFd>(fd: F, streams: Vec<camera::Stream>) -> Self {
let self_: Self = glib::Object::new();
self_.set_pipewire_fd(fd, streams).await;
self_
}
async fn set_pipewire_fd<F: AsRawFd>(&self, fd: F, streams: Vec<camera::Stream>) {
self.set_pipeline(None);
let mut src_builder =
gst::ElementFactory::make("pipewiresrc").property("fd", fd.as_raw_fd());
if let Some(node_id) = streams.first().map(|s| s.node_id()) {
src_builder = src_builder.property("path", node_id.to_string());
}
let pipewire_src = src_builder.build().unwrap();
let pipeline = gst::Pipeline::new();
let detector = QrCodeDetector::new(self.create_sender()).upcast();
let tee = gst::ElementFactory::make("tee").build().unwrap();
let queue = gst::ElementFactory::make("queue").build().unwrap();
let videoconvert1 = gst::ElementFactory::make("videoconvert").build().unwrap();
let videoconvert2 = gst::ElementFactory::make("videoconvert").build().unwrap();
let src_pad = queue.static_pad("src").unwrap();
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").build().unwrap();
let sink = gst::ElementFactory::make("gtk4paintablesink")
.build()
.unwrap();
pipeline
.add_many([
&pipewire_src,
&tee,
&queue,
&videoconvert1,
&detector,
&queue2,
&videoconvert2,
&sink,
])
.unwrap();
gst::Element::link_many([&pipewire_src, &tee, &queue, &videoconvert1, &detector]).unwrap();
tee.link_pads(None, &queue2, None).unwrap();
gst::Element::link_many([&queue2, &videoconvert2, &sink]).unwrap();
let bus = pipeline.bus().unwrap();
let bus_guard = bus
.add_watch_local(move |_, msg| {
if let gst::MessageView::Error(err) = msg.view() {
error!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
}
glib::ControlFlow::Continue
})
.expect("Could not add bus watch");
let paintable = sink.property::<gdk::Paintable>("paintable");
let (sender, receiver) = futures_channel::oneshot::channel();
let sender = Cell::new(Some(sender));
paintable.connect_invalidate_contents(move |_| {
if let Some(sender) = sender.take() {
if sender.send(()).is_err() {
error!("Could not send camera paintable `invalidate-contents` signal");
}
}
});
self.set_sink_paintable(paintable);
pipeline.set_state(gst::State::Playing).unwrap();
self.set_pipeline(Some((pipeline, bus_guard)));
if receiver.await.is_err() {
debug!("Camera paintable `invalidate-contents` signal sender was dropped");
}
}
fn set_sink_paintable(&self, paintable: gdk::Paintable) {
let imp = self.imp();
paintable.connect_invalidate_contents(clone!(
#[weak(rename_to = obj)]
self,
move |_| {
obj.invalidate_contents();
}
));
paintable.connect_invalidate_size(clone!(
#[weak(rename_to = obj)]
self,
move |_| {
obj.invalidate_size();
}
));
imp.sink_paintable.replace(Some(paintable));
self.invalidate_contents();
self.invalidate_size();
}
fn set_pipeline(&self, pipeline: Option<(gst::Pipeline, BusWatchGuard)>) {
let imp = self.imp();
if let Some((pipeline, _)) = imp.pipeline.take() {
pipeline.set_state(gst::State::Null).unwrap();
}
if pipeline.is_none() {
return;
}
imp.pipeline.replace(pipeline);
}
fn create_sender(&self) -> mpsc::Sender<Action> {
let (sender, receiver) = mpsc::channel::<Action>(8);
let obj_weak = glib::SendWeakRef::from(self.downgrade());
spawn!(async move {
receiver
.for_each(move |action| {
let obj_weak = obj_weak.clone();
async move {
let ctx = glib::MainContext::default();
ctx.spawn(async move {
spawn!(async move {
if let Some(obj) = obj_weak.upgrade() {
match action {
Action::QrCodeDetected(code) => {
obj.emit_by_name::<()>(
"code-detected",
&[&QrVerificationDataBoxed(code)],
);
}
}
}
});
});
}
})
.await;
});
sender
}
}