Modify architecture to run pipewire loop in second thread.

The pipewire loop now runs without interruption in a second thread and communicates with
the GTK thread via a channel in each direction, instead of checking for events once a second and using callbacks.

This allows changes to appear instantly in the view, instead of having to wait.
This commit is contained in:
Tom A. Wagner
2021-05-05 21:43:52 +02:00
parent 75aa0a30d0
commit 076fec7eb4
9 changed files with 292 additions and 346 deletions

64
Cargo.lock generated
View File

@@ -49,9 +49,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.57.0" version = "0.58.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd4865004a46a0aafb2a0a5eb19d3c9fc46ee5f063a6cfc605c69ac9ecf5263d" checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cexpr", "cexpr",
@@ -108,7 +108,7 @@ source = "git+https://github.com/gtk-rs/gtk-rs#51fea9aa4aff93a0322e87ea27c090465
dependencies = [ dependencies = [
"glib-sys", "glib-sys",
"libc", "libc",
"system-deps 3.1.0", "system-deps",
] ]
[[package]] [[package]]
@@ -342,7 +342,7 @@ dependencies = [
"glib-sys", "glib-sys",
"gobject-sys", "gobject-sys",
"libc", "libc",
"system-deps 3.1.0", "system-deps",
] ]
[[package]] [[package]]
@@ -372,7 +372,7 @@ dependencies = [
"gobject-sys", "gobject-sys",
"libc", "libc",
"pango-sys", "pango-sys",
"system-deps 3.1.0", "system-deps",
] ]
[[package]] [[package]]
@@ -399,7 +399,7 @@ dependencies = [
"glib-sys", "glib-sys",
"gobject-sys", "gobject-sys",
"libc", "libc",
"system-deps 3.1.0", "system-deps",
"winapi", "winapi",
] ]
@@ -441,7 +441,7 @@ version = "0.13.0"
source = "git+https://github.com/gtk-rs/gtk-rs#51fea9aa4aff93a0322e87ea27c090465f43f1f4" source = "git+https://github.com/gtk-rs/gtk-rs#51fea9aa4aff93a0322e87ea27c090465f43f1f4"
dependencies = [ dependencies = [
"libc", "libc",
"system-deps 3.1.0", "system-deps",
] ]
[[package]] [[package]]
@@ -457,7 +457,7 @@ source = "git+https://github.com/gtk-rs/gtk-rs#51fea9aa4aff93a0322e87ea27c090465
dependencies = [ dependencies = [
"glib-sys", "glib-sys",
"libc", "libc",
"system-deps 3.1.0", "system-deps",
] ]
[[package]] [[package]]
@@ -478,7 +478,7 @@ dependencies = [
"glib-sys", "glib-sys",
"libc", "libc",
"pkg-config", "pkg-config",
"system-deps 3.1.0", "system-deps",
] ]
[[package]] [[package]]
@@ -508,7 +508,7 @@ dependencies = [
"graphene-sys", "graphene-sys",
"libc", "libc",
"pango-sys", "pango-sys",
"system-deps 3.1.0", "system-deps",
] ]
[[package]] [[package]]
@@ -563,7 +563,7 @@ dependencies = [
"gsk4-sys", "gsk4-sys",
"libc", "libc",
"pango-sys", "pango-sys",
"system-deps 3.1.0", "system-deps",
] ]
[[package]] [[package]]
@@ -581,7 +581,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"env_logger", "env_logger",
"gtk4", "gtk4",
"libspa",
"log", "log",
"once_cell", "once_cell",
"pipewire", "pipewire",
@@ -664,8 +663,7 @@ dependencies = [
[[package]] [[package]]
name = "libspa" name = "libspa"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs?branch=main#f8dc21b0f85f391201e7c6346b121fbd21c02836"
checksum = "011074b4d7771195ec969f3053c5745d8cd22f4415e8348d3c574944b108b895"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cc", "cc",
@@ -674,17 +672,16 @@ dependencies = [
"libc", "libc",
"libspa-sys", "libspa-sys",
"nom 6.1.2", "nom 6.1.2",
"system-deps 2.0.3", "system-deps",
] ]
[[package]] [[package]]
name = "libspa-sys" name = "libspa-sys"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs?branch=main#f8dc21b0f85f391201e7c6346b121fbd21c02836"
checksum = "19351566b3a5adc05e491dfaef725869a0d5e3f206662a29f6ba64381d674769"
dependencies = [ dependencies = [
"bindgen", "bindgen",
"system-deps 3.1.0", "system-deps",
] ]
[[package]] [[package]]
@@ -773,7 +770,7 @@ dependencies = [
"glib-sys", "glib-sys",
"gobject-sys", "gobject-sys",
"libc", "libc",
"system-deps 3.1.0", "system-deps",
] ]
[[package]] [[package]]
@@ -806,14 +803,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pipewire" name = "pipewire"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs?branch=main#f8dc21b0f85f391201e7c6346b121fbd21c02836"
checksum = "e678fc8a43e7e4d5a211c4ce16517cf0a4bca520d96d9f4df15f26193ef120aa"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags", "bitflags",
"errno",
"libc", "libc",
"libspa", "libspa",
"libspa-sys", "libspa-sys",
"once_cell",
"pipewire-sys", "pipewire-sys",
"signal", "signal",
"thiserror", "thiserror",
@@ -822,12 +820,11 @@ dependencies = [
[[package]] [[package]]
name = "pipewire-sys" name = "pipewire-sys"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs?branch=main#f8dc21b0f85f391201e7c6346b121fbd21c02836"
checksum = "3afc6fc22dcdb1e80c445725e7f95b610640a38f591783b38c3d58133d5e2135"
dependencies = [ dependencies = [
"bindgen", "bindgen",
"libspa-sys", "libspa-sys",
"system-deps 3.1.0", "system-deps",
] ]
[[package]] [[package]]
@@ -957,9 +954,9 @@ checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "0.1.1" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d"
[[package]] [[package]]
name = "signal" name = "signal"
@@ -1024,21 +1021,6 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "system-deps"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b59b8aafd652f3c1469f16e6c223121e8a8dbe40c71475209c1401cff3a67ef"
dependencies = [
"heck",
"pkg-config",
"strum",
"strum_macros",
"thiserror",
"toml",
"version-compare",
]
[[package]] [[package]]
name = "system-deps" name = "system-deps"
version = "3.1.0" version = "3.1.0"

View File

@@ -14,9 +14,7 @@ categories = ["gui", "multimedia"]
[dependencies] [dependencies]
gtk = { git = "https://github.com/gtk-rs/gtk4-rs/", package = "gtk4" } gtk = { git = "https://github.com/gtk-rs/gtk4-rs/", package = "gtk4" }
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs", branch = "main" }
pipewire = "0.3.0"
libspa = "0.3.0"
log = "0.4.11" log = "0.4.11"
env_logger = "0.8.2" env_logger = "0.8.2"

View File

@@ -15,45 +15,41 @@ Helvum uses an architecture with the components laid out like this:
Λ ┆ Λ ┆
│<───── updates view │<───── updates view
│ ┆ │ ┆
│ ┆<─────────────── notifies of user input (using callbacks) │ ┆<─ notifies of user input
│ ┆ │ ┆ (using signals)
│ ┆ │ ┆
│ ┆ │ ┆
│ V notifies of remote changes │ V notifies of remote changes
┌┴───────────┐ (using callbacks) ┌─────────────────────┐ ┌┴───────────┐ via messages ┌─────────────────────┐
│ │<╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ │<╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ Seperate
│ Controller │ │ Pipewire Connection │ Controller │ │ Pipewire
│ ├──────────────────────────────>│ │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌>│ Thread
└┬───────────┘ Request changes to remote └─────────────────────┘ └┬───────────┘ Request changes to remote └─────────────────────┘
Λ via messages Λ
│ ║ │ ║
│<─── updates/reads state Communicates ───> │<─── updates/reads state
│ ║ │ ║
V ║ V ║
┌───────┐ V ┌───────┐ V
│ State │ [ Remote Pipewire Server ] │ State │ [ Remote Pipewire Server ]
└───────┘ └───────┘
``` ```
The program is split between two threads, with most stuff happening inside the GTK thread.
The GTK thread will sit in a GTK event processing loop, while the pipewire thread will sit in a
pipewire event processing loop.
The `Controller` struct is the centerpiece of this architecture. The `Controller` struct inside the GTK thread is the centerpiece of this architecture.
It registers callbacks with the `PipewireConnection` struct to get notified of any changes It communicates with the pipewire thread using two channels,
on the remote. where each message sent by one thread will trigger the loop of the other thread to invoke a callback
with the received message.
For each change it is notified of, it updates the view to reflect those changes, and additionally memorizes anything it might need later in the state. For each change on the remote pipewire server, the GTK thread is notified by the pipewire thread
and updates the view to reflect those changes, and additionally memorizes anything it might need later in the state.
Additionally, a user may also make changes using the view. Additionally, a user may also make changes using the view.
For each change, the view notifies the controller by invoking callbacks registered on it. For each change, the view notifies the controller by emitting a matching signal.
The controller will then request the pipewire connection to make those changes on the remote. \ The controller will then request the pipewire connection to make those changes on the remote. \
These changes will then be applied to the view like any other remote changes as explained above. These changes will then be applied to the view like any other remote changes as explained above.
## Control flow
Most of the time, the program will sit idle in a gtk event processing loop.
For any changes made using the view, gtk will emit an event on some widget, which will result in a closure on that widget being called, and in turn the controller being updated.
On the other hand, we may receive updates from the remote pipewire server at any moment. \
To process these changes, the gtk event loop is set up to trigger a roundtrip on the pipewire
connection on an interval. During this roundtrip, we process all events sent to us by the pipewire server and notify the controller of them.
# View Architecture # View Architecture
TODO TODO

View File

@@ -1,13 +1,12 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc}; use std::{cell::RefCell, collections::HashMap, rc::Rc};
use gtk::glib::{self, clone}; use gtk::glib::{self, clone, Continue, Receiver};
use libspa::{ForeignDict, ReadableDict};
use log::{info, warn}; use log::{info, warn};
use pipewire::{port::Direction, registry::GlobalObject, types::ObjectType}; use pipewire::spa::Direction;
use crate::{pipewire_connection::PipewireConnection, view}; use crate::{view, PipewireLink, PipewireMessage};
#[derive(Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum MediaType { pub enum MediaType {
Audio, Audio,
Video, Video,
@@ -41,7 +40,6 @@ enum Item {
/// ///
/// It also keeps and manages a state object that contains the current state of objects present on the remote. /// It also keeps and manages a state object that contains the current state of objects present on the remote.
pub struct Controller { pub struct Controller {
con: Rc<RefCell<PipewireConnection>>,
state: HashMap<u32, Item>, state: HashMap<u32, Item>,
view: Rc<view::View>, view: Rc<view::View>,
} }
@@ -56,95 +54,52 @@ impl Controller {
/// will also drop the controller, unless the `Rc` is cloned outside of this function. /// will also drop the controller, unless the `Rc` is cloned outside of this function.
pub(super) fn new( pub(super) fn new(
view: Rc<view::View>, view: Rc<view::View>,
con: Rc<RefCell<PipewireConnection>>, gtk_receiver: Receiver<PipewireMessage>,
) -> Rc<RefCell<Controller>> { ) -> Rc<RefCell<Controller>> {
let result = Rc::new(RefCell::new(Controller { let result = Rc::new(RefCell::new(Controller {
con,
view, view,
state: HashMap::new(), state: HashMap::new(),
})); }));
result // React to messages received from the pipewire thread.
.borrow() gtk_receiver.attach(
.con None,
clone!(
@weak result as controller => @default-return Continue(true),
move |msg| {
match msg {
PipewireMessage::NodeAdded {
id,
name,
media_type,
} => controller.borrow_mut().add_node(id, name, media_type),
PipewireMessage::PortAdded {
id,
node_id,
name,
direction,
} => controller
.borrow_mut() .borrow_mut()
.on_global_add(Some(Box::new( .add_port(id, node_id, name, direction),
clone!(@weak result as this => move |global| { PipewireMessage::LinkAdded { id, link } => controller.borrow_mut().add_link(id, link),
this.borrow_mut().global_add(global); PipewireMessage::ObjectRemoved { id } => controller.borrow_mut().remove_global(id),
}), };
))); Continue(true)
result }
.borrow() )
.con );
.borrow_mut()
.on_global_remove(Some(Box::new(clone!(@weak result as this => move |id| {
this.borrow_mut().global_remove(id);
}))));
result result
} }
/// Handle a new global object being added.
/// Relevant objects are displayed to the user and/or stored to the state.
///
/// It is called from the `PipewireConnection` via callback.
fn global_add(&mut self, global: &GlobalObject<ForeignDict>) {
match global.type_ {
ObjectType::Node => {
self.add_node(global);
}
ObjectType::Port => {
self.add_port(global);
}
ObjectType::Link => {
self.add_link(global);
}
_ => {}
}
}
/// Handle a node object being added. /// Handle a node object being added.
fn add_node(&mut self, node: &GlobalObject<ForeignDict>) { pub(super) fn add_node(&mut self, id: u32, name: String, media_type: Option<MediaType>) {
info!("Adding node to graph: id {}", node.id); info!("Adding node to graph: id {}", id);
// Get the nicest possible name for the node, using a fallback chain of possible name attributes. self.view.add_node(id, name.as_str());
let node_name = &node
.props
.as_ref()
.map(|dict| {
String::from(
dict.get("node.nick")
.or_else(|| dict.get("node.description"))
.or_else(|| dict.get("node.name"))
.unwrap_or_default(),
)
})
.unwrap_or_default();
// FIXME: This relies on the node being passed to us by the pipwire server before its port.
let media_type = node
.props
.as_ref()
.map(|props| {
props.get("media.class").map(|class| {
if class.contains("Audio") {
Some(MediaType::Audio)
} else if class.contains("Video") {
Some(MediaType::Video)
} else if class.contains("Midi") {
Some(MediaType::Midi)
} else {
None
}
})
})
.flatten()
.flatten();
self.view.add_node(node.id, node_name);
self.state.insert( self.state.insert(
node.id, id,
Item::Node { Item::Node {
// widget: node_widget, // widget: node_widget,
media_type, media_type,
@@ -153,94 +108,43 @@ impl Controller {
} }
/// Handle a port object being added. /// Handle a port object being added.
fn add_port(&mut self, port: &GlobalObject<ForeignDict>) { pub(super) fn add_port(&mut self, id: u32, node_id: u32, name: String, direction: Direction) {
info!("Adding port to graph: id {}", port.id); info!("Adding port to graph: id {}", id);
// Update graph to contain the new port. // Update graph to contain the new port.
let props = port
.props
.as_ref()
.expect("Port object is missing properties");
let port_label = props.get("port.name").unwrap_or_default().to_string();
let node_id: u32 = props
.get("node.id")
.expect("Port has no node.id property!")
.parse()
.expect("Could not parse node.id property");
// Find out the nodes media type so that the port can be colored. // Find out the nodes media type so that the port can be colored.
let media_type = if let Some(Item::Node { media_type, .. }) = self.state.get(&node_id) { let media_type = if let Some(Item::Node { media_type, .. }) = self.state.get(&node_id) {
media_type.to_owned() media_type.to_owned()
} else { } else {
warn!("Node not found for Port {}", port.id); warn!("Node not found for Port {}", id);
None None
}; };
self.view.add_port( self.view
node_id, .add_port(node_id, id, &name, direction, media_type);
port.id,
&port_label,
if matches!(props.get("port.direction"), Some("in")) {
Direction::Input
} else {
Direction::Output
},
media_type,
);
// Save node_id so we can delete this port easily. // Save node_id so we can delete this port easily.
self.state.insert(port.id, Item::Port { node_id }); self.state.insert(id, Item::Port { node_id });
} }
/// Handle a link object being added. /// Handle a link object being added.
fn add_link(&mut self, link: &GlobalObject<ForeignDict>) { pub(super) fn add_link(&mut self, id: u32, link: PipewireLink) {
info!("Adding link to graph: id {}", link.id); info!("Adding link to graph: id {}", id);
// FIXME: Links should be colored depending on the data they carry (video, audio, midi) like ports are. // FIXME: Links should be colored depending on the data they carry (video, audio, midi) like ports are.
self.state.insert(link.id, Item::Link); self.state.insert(id, Item::Link);
// Update graph to contain the new link. // Update graph to contain the new link.
let props = link self.view.add_link(id, link);
.props
.as_ref()
.expect("Link object is missing properties");
let input_node: u32 = props
.get("link.input.node")
.expect("Link has no link.input.node property")
.parse()
.expect("Could not parse link.input.node property");
let input_port: u32 = props
.get("link.input.port")
.expect("Link has no link.input.port property")
.parse()
.expect("Could not parse link.input.port property");
let output_node: u32 = props
.get("link.output.node")
.expect("Link has no link.input.node property")
.parse()
.expect("Could not parse link.input.node property");
let output_port: u32 = props
.get("link.output.port")
.expect("Link has no link.output.port property")
.parse()
.expect("Could not parse link.output.port property");
self.view.add_link(
link.id,
crate::PipewireLink {
node_from: output_node,
port_from: output_port,
node_to: input_node,
port_to: input_port,
},
);
} }
/// Handle a globalobject being removed. /// Handle a globalobject being removed.
/// Relevant objects are removed from the view and/or the state. /// Relevant objects are removed from the view and/or the state.
/// ///
/// This is called from the `PipewireConnection` via callback. /// This is called from the `PipewireConnection` via callback.
fn global_remove(&mut self, id: u32) { pub(super) fn remove_global(&mut self, id: u32) {
if let Some(item) = self.state.remove(&id) { if let Some(item) = self.state.remove(&id) {
match item { match item {
Item::Node { .. } => { Item::Node { .. } => {

View File

@@ -4,7 +4,38 @@ mod view;
use std::rc::Rc; use std::rc::Rc;
use gtk::{glib, prelude::*}; use controller::MediaType;
use gtk::glib::{self, PRIORITY_DEFAULT};
use pipewire::spa::Direction;
/// Messages used GTK thread to command the pipewire thread.
#[derive(Debug)]
enum GtkMessage {
/// Quit the event loop and let the thread finish.
Terminate,
}
/// Messages used pipewire thread to notify the GTK thread.
#[derive(Debug)]
enum PipewireMessage {
/// A new node has appeared.
NodeAdded {
id: u32,
name: String,
media_type: Option<MediaType>,
},
/// A new port has appeared.
PortAdded {
id: u32,
node_id: u32,
name: String,
direction: Direction,
},
/// A new link has appeared.
LinkAdded { id: u32, link: PipewireLink },
/// An object was removed
ObjectRemoved { id: u32 },
}
#[derive(Debug)] #[derive(Debug)]
pub struct PipewireLink { pub struct PipewireLink {
@@ -18,20 +49,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init(); env_logger::init();
gtk::init()?; gtk::init()?;
let view = Rc::new(view::View::new()); // Start the pipewire thread with channels in both directions.
let pw_con = pipewire_connection::PipewireConnection::new()?; let (gtk_sender, gtk_receiver) = glib::MainContext::channel(PRIORITY_DEFAULT);
let _controller = controller::Controller::new(view.clone(), pw_con.clone()); let (pw_sender, pw_receiver) = pipewire::channel::channel();
let pw_thread =
std::thread::spawn(move || pipewire_connection::thread_main(gtk_sender, pw_receiver));
// Do an initial roundtrip before showing the view, let view = Rc::new(view::View::new());
// so that the graph is already populated when the window opens. let _controller = controller::Controller::new(view.clone(), gtk_receiver);
pw_con.borrow().roundtrip();
// From now on, call roundtrip() every second.
glib::timeout_add_seconds_local(1, move || {
pw_con.borrow().roundtrip();
Continue(true)
});
view.run(); view.run();
pw_sender
.send(GtkMessage::Terminate)
.expect("Failed to send message");
pw_thread.join().expect("Pipewire thread panicked");
Ok(()) Ok(())
} }

View File

@@ -1,123 +1,159 @@
use gtk::glib::{self, clone}; use gtk::glib;
use libspa::ForeignDict; use pipewire::{
use log::trace; prelude::*,
use once_cell::unsync::OnceCell; registry::GlobalObject,
use pipewire as pw; spa::{Direction, ForeignDict},
use pw::registry::GlobalObject; types::ObjectType,
Context, MainLoop,
use std::{
cell::{Cell, RefCell},
rc::Rc,
}; };
/// This struct is responsible for communication with the pipewire server. use crate::{controller::MediaType, GtkMessage, PipewireMessage};
/// The owner of this struct can subscribe to notifications for globals added or removed.
/// /// The "main" function of the pipewire thread.
/// It's `roundtrip` function must be called regularly to receive updates. pub(super) fn thread_main(
pub struct PipewireConnection { gtk_sender: glib::Sender<PipewireMessage>,
mainloop: pw::MainLoop, pw_receiver: pipewire::channel::Receiver<GtkMessage>,
_context: pw::Context<pw::MainLoop>, ) {
core: pw::Core, let mainloop = MainLoop::new().expect("Failed to create mainloop");
registry: pw::registry::Registry, let context = Context::new(&mainloop).expect("Failed to create context");
listeners: OnceCell<pw::registry::Listener>, let core = context.connect(None).expect("Failed to connect to remote");
on_global_add: Option<Box<dyn Fn(&GlobalObject<ForeignDict>)>>, let registry = core.get_registry().expect("Failed to get registry");
on_global_remove: Option<Box<dyn Fn(u32)>>,
let _receiver = pw_receiver.attach(&mainloop, {
let mainloop = mainloop.clone();
move |msg| match msg {
GtkMessage::Terminate => mainloop.quit(),
} }
});
impl PipewireConnection { let _listener = registry
/// Create a new Pipewire Connection.
///
/// This returns an `Rc`, because weak references to the result are needed inside closures set up during creation.
pub fn new() -> Result<Rc<RefCell<Self>>, pw::Error> {
// Initialize pipewire lib and obtain needed pipewire objects.
pw::init();
let mainloop = pw::MainLoop::new()?;
let context = pw::Context::new(&mainloop)?;
let core = context.connect(None)?;
let registry = core.get_registry()?;
let result = Rc::new(RefCell::new(Self {
mainloop,
_context: context,
core,
registry,
listeners: OnceCell::new(),
on_global_add: None,
on_global_remove: None,
}));
// Notify state on globals added / removed
let listeners = result
.borrow()
.registry
.add_listener_local() .add_listener_local()
.global(clone!(@weak result as this => move |global| { .global({
trace!("Global is added: {}", global.id); let sender = gtk_sender.clone();
let con = this.borrow(); move |global| match global.type_ {
if let Some(callback) = con.on_global_add.as_ref() { ObjectType::Node => handle_node(global, &sender),
callback(global) ObjectType::Port => handle_port(global, &sender),
} else { ObjectType::Link => handle_link(global, &sender),
trace!("No on_global_add callback registered"); _ => {
// Other objects are not interesting to us
} }
}))
.global_remove(clone!(@weak result as this => move |id| {
trace!("Global is removed: {}", id);
let con = this.borrow();
if let Some(callback) = con.on_global_remove.as_ref() {
callback(id)
} else {
trace!("No on_global_remove callback registered");
}
}))
.register();
// Makeshift `expect()`: listeners does not implement `Debug`, so we can not use `expect`.
assert!(
result.borrow_mut().listeners.set(listeners).is_ok(),
"PipewireConnection.listeners field already set"
);
Ok(result)
}
/// Receive all events from the pipewire server, sending them to the `pipewire_state` struct for processing.
pub fn roundtrip(&self) {
trace!("Starting roundtrip");
let done = Rc::new(Cell::new(false));
let pending = self
.core
.sync(0)
.expect("Failed to trigger core sync event");
let done_clone = done.clone();
let loop_clone = self.mainloop.clone();
let _listener = self
.core
.add_listener_local()
.done(move |id, seq| {
if id == pw::PW_ID_CORE && seq == pending {
done_clone.set(true);
loop_clone.quit();
} }
}) })
.global_remove(move |id| {
gtk_sender
.send(PipewireMessage::ObjectRemoved { id })
.expect("Failed to send message")
})
.register(); .register();
while !done.get() { mainloop.run();
self.mainloop.run();
} }
trace!("Roundtrip finished"); /// Handle a new node being added
fn handle_node(node: &GlobalObject<ForeignDict>, sender: &glib::Sender<PipewireMessage>) {
let props = node
.props
.as_ref()
.expect("Node object is missing properties");
// Get the nicest possible name for the node, using a fallback chain of possible name attributes.
let name = String::from(
props
.get("node.nick")
.or_else(|| props.get("node.description"))
.or_else(|| props.get("node.name"))
.unwrap_or_default(),
);
// FIXME: This relies on the node being passed to us by the pipwire server before its port.
let media_type = props
.get("media.class")
.map(|class| {
if class.contains("Audio") {
Some(MediaType::Audio)
} else if class.contains("Video") {
Some(MediaType::Video)
} else if class.contains("Midi") {
Some(MediaType::Midi)
} else {
None
}
})
.flatten();
sender
.send(PipewireMessage::NodeAdded {
id: node.id,
name,
media_type,
})
.expect("Failed to send message");
} }
/// Set or unset a callback that gets called when a new global is added. /// Handle a new port being added
pub fn on_global_add(&mut self, callback: Option<Box<dyn Fn(&GlobalObject<ForeignDict>)>>) { fn handle_port(port: &GlobalObject<ForeignDict>, sender: &glib::Sender<PipewireMessage>) {
self.on_global_add = callback; let props = port
.props
.as_ref()
.expect("Port object is missing properties");
let name = props.get("port.name").unwrap_or_default().to_string();
let node_id: u32 = props
.get("node.id")
.expect("Port has no node.id property!")
.parse()
.expect("Could not parse node.id property");
let direction = if matches!(props.get("port.direction"), Some("in")) {
Direction::Input
} else {
Direction::Output
};
sender
.send(PipewireMessage::PortAdded {
id: port.id,
node_id,
name,
direction,
})
.expect("Failed to send message");
} }
/// Set or unset a callback that gets called when a global is removed. /// Handle a new link being added
pub fn on_global_remove(&mut self, callback: Option<Box<dyn Fn(u32)>>) { fn handle_link(link: &GlobalObject<ForeignDict>, sender: &glib::Sender<PipewireMessage>) {
self.on_global_remove = callback; let props = link
} .props
.as_ref()
.expect("Link object is missing properties");
let node_from: u32 = props
.get("link.output.node")
.expect("Link has no link.input.node property")
.parse()
.expect("Could not parse link.input.node property");
let port_from: u32 = props
.get("link.output.port")
.expect("Link has no link.output.port property")
.parse()
.expect("Could not parse link.output.port property");
let node_to: u32 = props
.get("link.input.node")
.expect("Link has no link.input.node property")
.parse()
.expect("Could not parse link.input.node property");
let port_to: u32 = props
.get("link.input.port")
.expect("Link has no link.input.port property")
.parse()
.expect("Could not parse link.input.port property");
sender
.send(PipewireMessage::LinkAdded {
id: link.id,
link: crate::PipewireLink {
node_from,
port_from,
node_to,
port_to,
},
})
.expect("Failed to send message");
} }

View File

@@ -13,7 +13,7 @@ use gtk::{
glib::{self, clone}, glib::{self, clone},
prelude::*, prelude::*,
}; };
use pipewire::port::Direction; use pipewire::spa::Direction;
use crate::controller::MediaType; use crate::controller::MediaType;

View File

@@ -1,7 +1,7 @@
use super::graph_view::GraphView; use super::graph_view::GraphView;
use gtk::{glib, prelude::*, subclass::prelude::*, WidgetExt}; use gtk::{glib, prelude::*, subclass::prelude::*, WidgetExt};
use pipewire::port::Direction; use pipewire::spa::Direction;
use std::{collections::HashMap, rc::Rc}; use std::{collections::HashMap, rc::Rc};

View File

@@ -1,9 +1,11 @@
use gtk::{glib, prelude::*, subclass::prelude::*}; use gtk::{glib, prelude::*, subclass::prelude::*};
use pipewire::spa::Direction;
use crate::controller::MediaType; use crate::controller::MediaType;
mod imp { mod imp {
use once_cell::unsync::OnceCell; use once_cell::unsync::OnceCell;
use pipewire::spa::Direction;
use super::*; use super::*;
@@ -11,7 +13,7 @@ mod imp {
#[derive(Default)] #[derive(Default)]
pub struct Port { pub struct Port {
pub(super) id: OnceCell<u32>, pub(super) id: OnceCell<u32>,
pub(super) direction: OnceCell<pipewire::port::Direction>, pub(super) direction: OnceCell<Direction>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@@ -32,12 +34,7 @@ glib::wrapper! {
} }
impl Port { impl Port {
pub fn new( pub fn new(id: u32, name: &str, direction: Direction, media_type: Option<MediaType>) -> Self {
id: u32,
name: &str,
direction: pipewire::port::Direction,
media_type: Option<MediaType>,
) -> Self {
// Create the widget and initialize needed fields // Create the widget and initialize needed fields
let res: Self = glib::Object::new(&[]).expect("Failed to create Port"); let res: Self = glib::Object::new(&[]).expect("Failed to create Port");
let private = imp::Port::from_instance(&res); let private = imp::Port::from_instance(&res);
@@ -60,7 +57,7 @@ impl Port {
res res
} }
pub fn direction(&self) -> &pipewire::port::Direction { pub fn direction(&self) -> &Direction {
let private = imp::Port::from_instance(self); let private = imp::Port::from_instance(self);
private.direction.get().expect("Port direction is not set") private.direction.get().expect("Port direction is not set")
} }