From bee0c4a4f54e4024499973bef2834967b235cca8 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Wed, 15 Jan 2025 13:56:52 +0000 Subject: [PATCH 01/17] PipeWire implementation --- .gitignore | 4 +- Cargo.toml | 3 + examples/beep.rs | 42 +- examples/synth_tones.rs | 45 + pipewire-client/Cargo.toml | 19 + .../src/client/connection_string.rs | 36 + pipewire-client/src/client/handlers/event.rs | 263 ++++++ pipewire-client/src/client/handlers/mod.rs | 5 + .../src/client/handlers/registry.rs | 272 ++++++ .../src/client/handlers/request.rs | 560 +++++++++++++ pipewire-client/src/client/handlers/thread.rs | 134 +++ pipewire-client/src/client/implementation.rs | 430 ++++++++++ .../src/client/implementation_test.rs | 162 ++++ pipewire-client/src/client/mod.rs | 8 + pipewire-client/src/constants.rs | 32 + pipewire-client/src/error.rs | 15 + pipewire-client/src/info.rs | 53 ++ pipewire-client/src/lib.rs | 19 + pipewire-client/src/listeners.rs | 49 ++ pipewire-client/src/messages.rs | 118 +++ pipewire-client/src/states.rs | 777 ++++++++++++++++++ pipewire-client/src/utils.rs | 139 ++++ pipewire-spa-utils/Cargo.toml | 31 + pipewire-spa-utils/build.rs | 18 + .../build_modules/format/mod.rs | 445 ++++++++++ pipewire-spa-utils/build_modules/mod.rs | 3 + .../syntax/generators/enumerator.rs | 145 ++++ .../build_modules/syntax/generators/mod.rs | 1 + .../build_modules/syntax/mod.rs | 3 + .../build_modules/syntax/parsers/mod.rs | 59 ++ .../build_modules/syntax/utils.rs | 67 ++ pipewire-spa-utils/build_modules/utils/mod.rs | 104 +++ pipewire-spa-utils/src/audio/mod.rs | 57 ++ pipewire-spa-utils/src/audio/raw.rs | 63 ++ pipewire-spa-utils/src/format/mod.rs | 12 + pipewire-spa-utils/src/lib.rs | 5 + pipewire-spa-utils/src/macros/mod.rs | 115 +++ pipewire-spa-utils/src/utils/mod.rs | 255 ++++++ src/host/mod.rs | 10 + src/host/pipewire/device.rs | 208 +++++ src/host/pipewire/host.rs | 79 ++ src/host/pipewire/mod.rs | 11 + src/host/pipewire/stream.rs | 36 + src/host/pipewire/utils.rs | 126 +++ src/platform/mod.rs | 12 +- 45 files changed, 5046 insertions(+), 4 deletions(-) create mode 100644 pipewire-client/Cargo.toml create mode 100644 pipewire-client/src/client/connection_string.rs create mode 100644 pipewire-client/src/client/handlers/event.rs create mode 100644 pipewire-client/src/client/handlers/mod.rs create mode 100644 pipewire-client/src/client/handlers/registry.rs create mode 100644 pipewire-client/src/client/handlers/request.rs create mode 100644 pipewire-client/src/client/handlers/thread.rs create mode 100644 pipewire-client/src/client/implementation.rs create mode 100644 pipewire-client/src/client/implementation_test.rs create mode 100644 pipewire-client/src/client/mod.rs create mode 100644 pipewire-client/src/constants.rs create mode 100644 pipewire-client/src/error.rs create mode 100644 pipewire-client/src/info.rs create mode 100644 pipewire-client/src/lib.rs create mode 100644 pipewire-client/src/listeners.rs create mode 100644 pipewire-client/src/messages.rs create mode 100644 pipewire-client/src/states.rs create mode 100644 pipewire-client/src/utils.rs create mode 100644 pipewire-spa-utils/Cargo.toml create mode 100644 pipewire-spa-utils/build.rs create mode 100644 pipewire-spa-utils/build_modules/format/mod.rs create mode 100644 pipewire-spa-utils/build_modules/mod.rs create mode 100644 pipewire-spa-utils/build_modules/syntax/generators/enumerator.rs create mode 100644 pipewire-spa-utils/build_modules/syntax/generators/mod.rs create mode 100644 pipewire-spa-utils/build_modules/syntax/mod.rs create mode 100644 pipewire-spa-utils/build_modules/syntax/parsers/mod.rs create mode 100644 pipewire-spa-utils/build_modules/syntax/utils.rs create mode 100644 pipewire-spa-utils/build_modules/utils/mod.rs create mode 100644 pipewire-spa-utils/src/audio/mod.rs create mode 100644 pipewire-spa-utils/src/audio/raw.rs create mode 100644 pipewire-spa-utils/src/format/mod.rs create mode 100644 pipewire-spa-utils/src/lib.rs create mode 100644 pipewire-spa-utils/src/macros/mod.rs create mode 100644 pipewire-spa-utils/src/utils/mod.rs create mode 100644 src/host/pipewire/device.rs create mode 100644 src/host/pipewire/host.rs create mode 100644 src/host/pipewire/mod.rs create mode 100644 src/host/pipewire/stream.rs create mode 100644 src/host/pipewire/utils.rs diff --git a/.gitignore b/.gitignore index 0289afe13..5689be829 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -/target -/Cargo.lock +target/ +Cargo.lock .cargo/ .DS_Store recorded.wav diff --git a/Cargo.toml b/Cargo.toml index ecf4cf002..4609e6b2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ rust-version = "1.70" [features] asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions. oboe-shared-stdcxx = ["oboe/shared-stdcxx"] # Only available on Android. See README for what it does. +jack = ["dep:jack"] +pipewire = ["dep:pipewire-client"] [dependencies] dasp_sample = "0.11" @@ -46,6 +48,7 @@ num-traits = { version = "0.2.6", optional = true } alsa = "0.9" libc = "0.2" jack = { version = "0.13.0", optional = true } +pipewire-client = { version = "0.1", path = "pipewire-client", optional = true } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] core-foundation-sys = "0.8.2" # For linking to CoreFoundation.framework and handling device name `CFString`s. diff --git a/examples/beep.rs b/examples/beep.rs index 7d3b23d88..921180e00 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -24,6 +24,20 @@ struct Opt { #[arg(short, long)] #[allow(dead_code)] jack: bool, + + /// Use the PipeWire host + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "pipewire" + ))] + #[arg(short, long)] + #[allow(dead_code)] + pipewire: bool, } fn main() -> anyhow::Result<()> { @@ -52,6 +66,29 @@ fn main() -> anyhow::Result<()> { cpal::default_host() }; + // Conditionally compile with pipewire if the feature is specified. + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "pipewire" + ))] + // Manually check for flags. Can be passed through cargo with -- e.g. + // cargo run --release --example beep --features pipewire -- --pipewire + let host = if opt.pipewire { + cpal::host_from_id(cpal::available_hosts() + .into_iter() + .find(|id| *id == cpal::HostId::PipeWire) + .expect( + "make sure --features pipewire is specified. only works on OSes where pipewire is available", + )).expect("pipewire host unavailable") + } else { + cpal::default_host() + }; + #[cfg(any( not(any( target_os = "linux", @@ -59,7 +96,10 @@ fn main() -> anyhow::Result<()> { target_os = "freebsd", target_os = "netbsd" )), - not(feature = "jack") + not(any( + feature = "jack", + feature = "pipewire", + )) ))] let host = cpal::default_host(); diff --git a/examples/synth_tones.rs b/examples/synth_tones.rs index c83f816d9..e4ee87559 100644 --- a/examples/synth_tones.rs +++ b/examples/synth_tones.rs @@ -114,6 +114,51 @@ where pub fn host_device_setup( ) -> Result<(cpal::Host, cpal::Device, cpal::SupportedStreamConfig), anyhow::Error> { + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "jack" + ))] + // Manually check for flags. Can be passed through cargo with -- e.g. + // cargo run --release --example beep --features jack -- --jack + let host = cpal::host_from_id(cpal::available_hosts() + .into_iter() + .find(|id| *id == cpal::HostId::Jack) + .expect( + "make sure --features jack is specified. only works on OSes where jack is available", + )).expect("jack host unavailable"); + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "pipewire" + ))] + let host = cpal::host_from_id(cpal::available_hosts() + .into_iter() + .find(|id| *id == cpal::HostId::PipeWire) + .expect( + "make sure --features pipewire is specified. only works on OSes where pipewire is available", + )).expect("pipewire host unavailable"); + + #[cfg(any( + not(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + )), + not(any( + feature = "jack", + feature = "pipewire", + )) + ))] let host = cpal::default_host(); let device = host diff --git a/pipewire-client/Cargo.toml b/pipewire-client/Cargo.toml new file mode 100644 index 000000000..fd0e7cfe2 --- /dev/null +++ b/pipewire-client/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pipewire-client" +version = "0.1.0" +edition = "2021" +authors = ["Alexis Bekhdadi "] +description = "PipeWire Client" +repository = "https://github.com/RustAudio/cpal/" +#documentation = "" +license = "Apache-2.0" +keywords = ["pipewire", "client"] + +[dependencies] +pipewire = { version = "0.8" } +pipewire-spa-utils = { version = "0.1", path = "../pipewire-spa-utils"} +serde_json = "1.0" + +[dev-dependencies] +rstest = "0.24" +serial_test = "3.2" \ No newline at end of file diff --git a/pipewire-client/src/client/connection_string.rs b/pipewire-client/src/client/connection_string.rs new file mode 100644 index 000000000..de0d46ada --- /dev/null +++ b/pipewire-client/src/client/connection_string.rs @@ -0,0 +1,36 @@ +use std::path::PathBuf; +use crate::constants::*; + +pub(super) struct PipewireClientConnectionString; + +impl PipewireClientConnectionString { + pub(super) fn from_env() -> String { + let pipewire_runtime_dir = std::env::var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY); + let xdg_runtime_dir = std::env::var(XDG_RUNTIME_DIR_ENVIRONMENT_KEY); + + let socket_directory = match (xdg_runtime_dir, pipewire_runtime_dir) { + (Ok(value), Ok(_)) => value, + (Ok(value), Err(_)) => value, + (Err(_), Ok(value)) => value, + (Err(_), Err(_)) => panic!( + "${} or ${} should be set. See https://docs.pipewire.org/page_man_pipewire_1.html", + PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, XDG_RUNTIME_DIR_ENVIRONMENT_KEY + ), + }; + + let pipewire_remote = match std::env::var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY) { + Ok(value) => value, + Err(_) => panic!( + "${PIPEWIRE_REMOTE_ENVIRONMENT_KEY} should be set. See https://docs.pipewire.org/page_man_pipewire_1.html", + ) + }; + + let socket_path = PathBuf::from(socket_directory).join(pipewire_remote); + socket_path.to_str().unwrap().to_string() + } +} + +pub(super) struct PipewireClientInfo { + pub name: String, + pub connection_string: String, +} \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/event.rs b/pipewire-client/src/client/handlers/event.rs new file mode 100644 index 000000000..b7ee2e95d --- /dev/null +++ b/pipewire-client/src/client/handlers/event.rs @@ -0,0 +1,263 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::mpsc; +use pipewire_spa_utils::audio::raw::AudioInfoRaw; +use crate::constants::{METADATA_NAME_PROPERTY_VALUE_DEFAULT, METADATA_NAME_PROPERTY_VALUE_SETTINGS}; +use crate::error::Error; +use crate::listeners::ListenerTriggerPolicy; +use crate::messages::{EventMessage, MessageResponse}; +use crate::states::{DefaultAudioNodesState, GlobalId, GlobalState, SettingsState}; + +pub(super) fn event_handler( + state: Rc>, + main_sender: mpsc::Sender, + event_sender: pipewire::channel::Sender, +) -> impl Fn(EventMessage) + 'static +{ + move |event_message: EventMessage| match event_message { + EventMessage::SetMetadataListeners { id } => handle_set_metadata_listeners( + id, + state.clone(), + main_sender.clone(), + ), + EventMessage::RemoveNode { id } => handle_remove_node( + id, + state.clone(), + main_sender.clone() + ), + EventMessage::SetNodePropertiesListener { id } => handle_set_node_properties_listener( + id, + state.clone(), + main_sender.clone(), + event_sender.clone() + ), + EventMessage::SetNodeFormatListener { id } => handle_set_node_format_listener( + id, + state.clone(), + main_sender.clone(), + event_sender.clone() + ), + EventMessage::SetNodeProperties { + id, + properties + } => handle_set_node_properties( + id, + properties, + state.clone(), + main_sender.clone() + ), + EventMessage::SetNodeFormat { id, format } => handle_set_node_format( + id, + format, + state.clone(), + main_sender.clone() + ), + } +} + +fn handle_set_metadata_listeners( + id: GlobalId, + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let listener_state = state.clone(); + let mut state = state.borrow_mut(); + let metadata = match state.get_metadata_mut(&id) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let main_sender = main_sender.clone(); + match metadata.name.as_str() { + METADATA_NAME_PROPERTY_VALUE_SETTINGS => { + metadata.add_property_listener( + ListenerTriggerPolicy::Keep, + SettingsState::listener( + listener_state, + move |settings: &SettingsState| { + main_sender + .send(MessageResponse::SettingsState(settings.state.clone())) + .unwrap(); + } + ) + ) + }, + METADATA_NAME_PROPERTY_VALUE_DEFAULT => { + metadata.add_property_listener( + ListenerTriggerPolicy::Keep, + DefaultAudioNodesState::listener( + listener_state, + move |default_audio_devices: &DefaultAudioNodesState| { + main_sender + .send(MessageResponse::DefaultAudioNodesState(default_audio_devices.state.clone())) + .unwrap(); + } + ) + ) + }, + _ => { + main_sender + .send(MessageResponse::Error(Error { + description: format!("Unexpected metadata with name: {}", metadata.name) + })) + .unwrap(); + } + }; +} +fn handle_remove_node( + id: GlobalId, + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let mut state = state.borrow_mut(); + let _ = match state.get_node(&id) { + Ok(_) => {}, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + state.remove(&id); +} +fn handle_set_node_properties_listener( + id: GlobalId, + state: Rc>, + main_sender: mpsc::Sender, + event_sender: pipewire::channel::Sender, +) +{ + let mut state = state.borrow_mut(); + let node = match state.get_node_mut(&id) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let event_sender = event_sender.clone(); + node.add_properties_listener( + ListenerTriggerPolicy::Keep, + move |properties| { + // "object.register" property when set to "false", indicate we should not + // register this object + // Some bluez nodes don't have sample rate information in their + // EnumFormat object. We delete those nodes since parsing node audio format + // imply to retrieve: + // - Media type + // - Media subtype + // - Sample format + // - Sample rate + // - Channels + // - Channels position + // Lets see in the future if node with no "object.register: false" property + // and with incorrect EnumFormat object occur. + if properties.get("object.register").is_some_and(move |value| value == "false") { + event_sender + .send(EventMessage::RemoveNode { + id: id.clone(), + }) + .unwrap(); + } + else { + event_sender + .send(EventMessage::SetNodeProperties { + id: id.clone(), + properties, + }) + .unwrap(); + event_sender + .send(EventMessage::SetNodeFormatListener { + id: id.clone(), + }) + .unwrap(); + } + } + ); +} +fn handle_set_node_format_listener( + id: GlobalId, + state: Rc>, + main_sender: mpsc::Sender, + event_sender: pipewire::channel::Sender, +) +{ + let mut state = state.borrow_mut(); + let node = match state.get_node_mut(&id) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let main_sender = main_sender.clone(); + let event_sender = event_sender.clone(); + node.add_format_listener( + ListenerTriggerPolicy::Keep, + move |format| { + match format { + Ok(value) => { + event_sender + .send(EventMessage::SetNodeFormat { + id, + format: value, + }) + .unwrap(); + } + Err(value) => { + main_sender.send(MessageResponse::Error(value)).unwrap(); + } + } + } + ) +} +fn handle_set_node_properties( + id: GlobalId, + properties: HashMap, + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let mut state = state.borrow_mut(); + let node = match state.get_node_mut(&id) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + node.set_properties(properties); +} +fn handle_set_node_format( + id: GlobalId, + format: AudioInfoRaw, + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let mut state = state.borrow_mut(); + let node = match state.get_node_mut(&id) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + node.set_format(format) +} \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/mod.rs b/pipewire-client/src/client/handlers/mod.rs new file mode 100644 index 000000000..caeefb05e --- /dev/null +++ b/pipewire-client/src/client/handlers/mod.rs @@ -0,0 +1,5 @@ +mod event; +mod registry; +mod request; +mod thread; +pub use thread::pw_thread as thread; diff --git a/pipewire-client/src/client/handlers/registry.rs b/pipewire-client/src/client/handlers/registry.rs new file mode 100644 index 000000000..d0be01cca --- /dev/null +++ b/pipewire-client/src/client/handlers/registry.rs @@ -0,0 +1,272 @@ +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::mpsc; +use pipewire::registry::GlobalObject; +use pipewire::spa; +use crate::constants::{APPLICATION_NAME_PROPERTY_KEY, APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION, APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER, MEDIA_CLASS_PROPERTY_KEY, MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK, MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE, METADATA_NAME_PROPERTY_KEY, METADATA_NAME_PROPERTY_VALUE_DEFAULT, METADATA_NAME_PROPERTY_VALUE_SETTINGS}; +use crate::messages::{EventMessage, MessageResponse}; +use crate::states::{ClientState, GlobalId, GlobalObjectState, GlobalState, MetadataState, NodeState}; +use crate::utils::debug_dict_ref; + +pub(super) fn registry_global_handler( + state: Rc>, + registry: Rc, + main_sender: mpsc::Sender, + event_sender: pipewire::channel::Sender, +) -> impl Fn(&GlobalObject<&spa::utils::dict::DictRef>) + 'static +{ + move |global: &GlobalObject<&spa::utils::dict::DictRef>| match global.type_ { + pipewire::types::ObjectType::Client => handle_client( + global, + state.clone(), + main_sender.clone() + ), + pipewire::types::ObjectType::Metadata => handle_metadata( + global, + state.clone(), + registry.clone(), + main_sender.clone(), + event_sender.clone() + ), + pipewire::types::ObjectType::Node => handle_node( + global, + state.clone(), + registry.clone(), + main_sender.clone(), + event_sender.clone() + ), + pipewire::types::ObjectType::Port => handle_port( + global, + state.clone(), + registry.clone(), + main_sender.clone(), + event_sender.clone() + ), + pipewire::types::ObjectType::Link => handle_link( + global, + state.clone(), + registry.clone(), + main_sender.clone(), + event_sender.clone() + ), + _ => {} + } +} + +fn handle_client( + global: &GlobalObject<&spa::utils::dict::DictRef>, + state: Rc>, + main_sender: mpsc::Sender, +) +{ + if global.props.is_none() { + return; + } + let properties = global.props.unwrap(); + let client = + match properties.get(APPLICATION_NAME_PROPERTY_KEY) { + Some(APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER) => { + ClientState::new( + APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER.to_string() + ) + } + Some(APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION) => { + ClientState::new( + APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION.to_string() + ) + } + _ => return, + }; + let mut state = state.borrow_mut(); + if let Err(value) = state.insert_client(global.id.into(), client) { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + }; +} + +fn handle_metadata( + global: &GlobalObject<&spa::utils::dict::DictRef>, + state: Rc>, + registry: Rc, + main_sender: mpsc::Sender, + event_sender: pipewire::channel::Sender, +) +{ + if global.props.is_none() { + return; + } + let properties = global.props.unwrap(); + let metadata = + match properties.get(METADATA_NAME_PROPERTY_KEY) { + Some(METADATA_NAME_PROPERTY_VALUE_SETTINGS) + | Some(METADATA_NAME_PROPERTY_VALUE_DEFAULT) => { + let metadata = registry.bind(global).unwrap(); + MetadataState::new( + metadata, + properties.get(METADATA_NAME_PROPERTY_KEY).unwrap().to_string(), + ) + } + _ => return, + }; + let mut state = state.borrow_mut(); + if let Err(value) = state.insert_metadata(global.id.into(), metadata) { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + }; + let metadata = state.get_metadata(&global.id.into()).unwrap(); + add_metadata_listeners( + global.id.into(), + &metadata, + &event_sender + ); +} + +fn handle_node( + global: &GlobalObject<&spa::utils::dict::DictRef>, + state: Rc>, + registry: Rc, + main_sender: mpsc::Sender, + event_sender: pipewire::channel::Sender, +) +{ + if global.props.is_none() { + + } + let properties = global.props.unwrap(); + let node = match properties.get(MEDIA_CLASS_PROPERTY_KEY) { + Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE) + | Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK) => { + let node: pipewire::node::Node = registry.bind(global).unwrap(); + NodeState::new(node) + } + _ => return, + }; + let mut state = state.borrow_mut(); + if let Err(value) = state.insert_node(global.id.into(), node) { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + }; + let node = state.get_node(&global.id.into()).unwrap(); + add_node_listeners( + global.id.into(), + &node, + &event_sender + ); +} + +fn handle_port( + global: &GlobalObject<&spa::utils::dict::DictRef>, + state: Rc>, + registry: Rc, + main_sender: mpsc::Sender, + event_sender: pipewire::channel::Sender, +) +{ + if global.props.is_none() { + + } + let properties = global.props.unwrap(); + debug_dict_ref(properties); + + let port: pipewire::port::Port = registry.bind(global).unwrap(); + + // let node = match properties.get(MEDIA_CLASS_PROPERTY_KEY) { + // Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE) + // | Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK) => { + // let node: pipewire::node::Node = registry.bind(global).unwrap(); + // NodeState::new(node) + // } + // _ => return, + // }; + // let mut state = state.borrow_mut(); + // if let Err(value) = state.insert_node(global.id.into(), node) { + // main_sender + // .send(MessageResponse::Error(value)) + // .unwrap(); + // return; + // }; + // let node = state.get_node(&global.id.into()).unwrap(); + // add_node_listeners( + // global.id.into(), + // &node, + // &event_sender + // ); +} + +fn handle_link( + global: &GlobalObject<&spa::utils::dict::DictRef>, + state: Rc>, + registry: Rc, + main_sender: mpsc::Sender, + event_sender: pipewire::channel::Sender, +) +{ + if global.props.is_none() { + + } + let properties = global.props.unwrap(); + debug_dict_ref(properties); + + let link: pipewire::link::Link = registry.bind(global).unwrap(); + // link. + + // let node = match properties.get(MEDIA_CLASS_PROPERTY_KEY) { + // Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE) + // | Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK) => { + // let node: pipewire::node::Node = registry.bind(global).unwrap(); + // NodeState::new(node) + // } + // _ => return, + // }; + // let mut state = state.borrow_mut(); + // if let Err(value) = state.insert_node(global.id.into(), node) { + // main_sender + // .send(MessageResponse::Error(value)) + // .unwrap(); + // return; + // }; + // let node = state.get_node(&global.id.into()).unwrap(); + // add_node_listeners( + // global.id.into(), + // &node, + // &event_sender + // ); +} + +fn add_metadata_listeners( + id: GlobalId, + metadata: &MetadataState, + event_sender: &pipewire::channel::Sender +) { + if *metadata.state.borrow() != GlobalObjectState::Pending { + return; + } + let id = id.clone(); + event_sender + .send(EventMessage::SetMetadataListeners { + id, + }) + .unwrap() +} + +fn add_node_listeners( + id: GlobalId, + node: &NodeState, + event_sender: &pipewire::channel::Sender +) { + if node.state() != GlobalObjectState::Pending { + return; + } + let id = id.clone(); + event_sender + .send(EventMessage::SetNodePropertiesListener { + id, + }) + .unwrap() +} \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/request.rs b/pipewire-client/src/client/handlers/request.rs new file mode 100644 index 000000000..dcfa485f7 --- /dev/null +++ b/pipewire-client/src/client/handlers/request.rs @@ -0,0 +1,560 @@ +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::mpsc; +use pipewire::proxy::ProxyT; +use crate::constants::*; +use crate::{AudioStreamInfo, Direction, NodeInfo}; +use crate::error::Error; +use crate::listeners::ListenerTriggerPolicy; +use crate::messages::{MessageRequest, MessageResponse, StreamCallback}; +use crate::states::{GlobalId, GlobalObjectState, GlobalState, OrphanState, StreamState}; +use crate::utils::PipewireCoreSync; + +pub(super) fn request_handler( + core: Rc, + core_sync: Rc, + main_loop: pipewire::main_loop::MainLoop, + state: Rc>, + main_sender: mpsc::Sender, +) -> impl Fn(MessageRequest) + 'static +{ + move |message_request: MessageRequest| match message_request { + MessageRequest::Quit => main_loop.quit(), + MessageRequest::Settings => { + handle_settings( + state.clone(), + main_sender.clone(), + ) + } + MessageRequest::DefaultAudioNodes => { + handle_default_audio_nodes( + state.clone(), + main_sender.clone() + ) + }, + MessageRequest::CreateNode { + name, + description, + nickname, + direction, + channels, + } => { + handle_create_node( + name, + description, + nickname, + direction, + channels, + core.clone(), + core_sync.clone(), + state.clone(), + main_sender.clone(), + ) + } + MessageRequest::EnumerateNodes(direction) => { + handle_enumerate_node( + direction, + state.clone(), + main_sender.clone(), + ) + }, + MessageRequest::CreateStream { + node_id, + direction, + format, + callback + } => { + handle_create_stream( + node_id, + direction, + format, + callback, + core.clone(), + state.clone(), + main_sender.clone(), + ) + } + MessageRequest::DeleteStream { name } => { + handle_delete_stream( + name, + state.clone(), + main_sender.clone() + ) + } + MessageRequest::ConnectStream { name } => { + handle_connect_stream( + name, + state.clone(), + main_sender.clone() + ) + } + MessageRequest::DisconnectStream { name } => { + handle_disconnect_stream( + name, + state.clone(), + main_sender.clone() + ) + } + // Internal requests + MessageRequest::CheckSessionManagerRegistered => { + handle_check_session_manager_registered( + state.clone(), + main_sender.clone() + ) + } + MessageRequest::NodeState(id) => { + handle_node_state( + id, + state.clone(), + main_sender.clone() + ) + } + MessageRequest::NodeStates => { + handle_node_states( + state.clone(), + main_sender.clone() + ) + } + } +} + +fn handle_settings( + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let state = state.borrow(); + let settings = state.get_settings(); + main_sender.send(MessageResponse::Settings(settings)).unwrap(); +} +fn handle_default_audio_nodes( + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let state = state.borrow(); + let default_audio_devices = state.get_default_audio_nodes(); + main_sender.send(MessageResponse::DefaultAudioNodes(default_audio_devices)).unwrap(); +} +fn handle_create_node( + name: String, + description: String, + nickname: String, + direction: Direction, + channels: u16, + core: Rc, + core_sync: Rc, + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let default_audio_position = format!( + "[ {} ]", + (1..=channels + 1) + .map(|n| n.to_string()) + .collect::>() + .join(" ") + ); + let properties = &pipewire::properties::properties! { + *pipewire::keys::FACTORY_NAME => "support.null-audio-sink", + *pipewire::keys::NODE_NAME => name.clone(), + *pipewire::keys::NODE_DESCRIPTION => description.clone(), + *pipewire::keys::NODE_NICK => nickname.clone(), + *pipewire::keys::MEDIA_CLASS => match direction { + Direction::Input => MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE, + Direction::Output => MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK, + }, + *pipewire::keys::OBJECT_LINGER => "false", + *pipewire::keys::AUDIO_CHANNELS => channels.to_string(), + MONITOR_CHANNEL_VOLUMES_PROPERTY_KEY => "true", + MONITOR_PASSTHROUGH_PROPERTY_KEY => "true", + AUDIO_POSITION_PROPERTY_KEY => match channels { + 1 => "[ MONO ]", + 2 => "[ FL FR ]", // 2.0 + 3 => "[ FL FR LFE ]", // 2.1 + 4 => "[ FL FR RL RR ]", // 4.0 + 5 => "[ FL FR FC RL RR ]", // 5.0 + 6 => "[ FL FR FC RL RR LFE ]", // 5.1 + 7 => "[ FL FR FC RL RR SL SR ]", // 7.0 + 8 => "[ FL FR FC RL RR SL SR LFE ]", // 7.1 + _ => default_audio_position.as_str(), + } + }; + let node: pipewire::node::Node = match core + .create_object("adapter", properties) + .map_err(move |error| { + Error { + description: error.to_string(), + } + }) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let core_sync = core_sync.clone(); + let listener_main_sender = main_sender.clone(); + let listener_state = state.clone(); + core_sync.register( + false, + PIPEWIRE_CORE_SYNC_CREATE_DEVICE_SEQ, + move || { + let state = listener_state.borrow(); + let nodes = match state.get_nodes() { + Ok(value) => value, + Err(value) => { + listener_main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let node = nodes.iter() + .find(move |(_, node)| { + node.state() == GlobalObjectState::Pending + }); + if let None = node { + listener_main_sender + .send(MessageResponse::Error(Error { + description: "Created node not found".to_string(), + })) + .unwrap(); + return; + }; + let node_id = node.unwrap().0; + listener_main_sender + .send(MessageResponse::CreateNode { + id: (*node_id).clone(), + }) + .unwrap(); + } + ); + let mut state = state.borrow_mut(); + // We need to store created node object as orphan since it had not been + // registered by server at this point (does not have an id yet). + // + // When a proxy object is dropped its send a server request to remove it on server + // side, then the server ask clients to remove proxy object on their side. + // + // The server will send a global object (through registry global object event + // listener) later, represented by a new proxy object instance that we can store + // as a NodeState. + // OrphanState object define "removed" listener from Proxy to ensure our orphan + // proxy object is removed when proper NodeState object is retrieved from server + let orphan = OrphanState::new(node.upcast()); + state.insert_orphan(orphan); +} +fn handle_enumerate_node( + direction: Direction, + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let state = state.borrow(); + let default_audio_nodes = state.get_default_audio_nodes(); + let default_audio_node = match direction { + Direction::Input => default_audio_nodes.source.clone(), + Direction::Output => default_audio_nodes.sink.clone() + }; + let filter_value = match direction { + Direction::Input => MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE, + Direction::Output => MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK, + }; + let nodes = match state.get_nodes() { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let nodes: Vec = nodes + .iter() + .filter_map(|(id, node)| { + let properties = node.properties(); + let format = node.format().unwrap(); + if properties.iter().any(|(_, v)| v == filter_value) { + Some((id, properties, format)) + } else { + None + } + }) + .map(|(id, properties, format)| { + let name = properties.get(*pipewire::keys::NODE_NAME).unwrap().clone(); + let description = properties + .get(*pipewire::keys::NODE_DESCRIPTION) + .unwrap() + .clone(); + let nickname = match properties.contains_key(*pipewire::keys::NODE_NICK) { + true => properties.get(*pipewire::keys::NODE_NICK).unwrap().clone(), + false => name.clone(), + }; + let is_default = name == default_audio_node; + NodeInfo { + id: (*id).clone().into(), + name, + description, + nickname, + direction: direction.clone(), + is_default, + format: format.clone() + } + }) + .collect(); + main_sender.send(MessageResponse::EnumerateNodes(nodes)).unwrap(); +} +fn handle_create_stream( + node_id: GlobalId, + direction: Direction, + format: AudioStreamInfo, + callback: StreamCallback, + core: Rc, + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let mut state = state.borrow_mut(); + let node_name = match state.get_node(&node_id) { + Ok(value) => value.name(), + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let stream_name = match direction { + Direction::Input => { + format!("{}.stream_input", node_name) + } + Direction::Output => { + format!("{}.stream_output", node_name) + } + }; + let properties = pipewire::properties::properties! { + *pipewire::keys::MEDIA_TYPE => MEDIA_TYPE_PROPERTY_VALUE_AUDIO, + *pipewire::keys::MEDIA_CLASS => match direction { + Direction::Input => MEDIA_CLASS_PROPERTY_VALUE_STREAM_INPUT_AUDIO, + Direction::Output => MEDIA_CLASS_PROPERTY_VALUE_STREAM_OUTPUT_AUDIO, + }, + }; + let stream = match pipewire::stream::Stream::new( + &core, + stream_name.clone().as_str(), + properties, + ) + .map_err(move |error| { + Error { + description: error.to_string(), + } + }) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let mut stream = StreamState::new( + stream_name.clone(), + format.into(), + direction.into(), + stream + ); + stream.add_process_listener( + ListenerTriggerPolicy::Keep, + callback + ); + if let Err(value) = state.insert_stream(stream_name.clone(), stream) { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + }; + main_sender + .send(MessageResponse::CreateStream { + name: stream_name.clone(), + }) + .unwrap(); +} +fn handle_delete_stream( + name: String, + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let mut state = state.borrow_mut(); + let stream = match state.get_stream_mut(&name) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + if let Err(value) = stream.disconnect() { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + }; + if let Err(value) = state.remove_stream(&name) { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + }; + main_sender.send(MessageResponse::DeleteStream).unwrap(); +} +fn handle_connect_stream( + name: String, + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let mut state = state.borrow_mut(); + let stream = match state.get_stream_mut(&name) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + if let Err(value) = stream.connect() { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + }; + main_sender.send(MessageResponse::ConnectStream).unwrap(); +} +fn handle_disconnect_stream( + name: String, + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let mut state = state.borrow_mut(); + let stream = match state.get_stream_mut(&name) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + if let Err(value) = stream.disconnect() { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + }; + main_sender.send(MessageResponse::DisconnectStream).unwrap(); +} +fn handle_check_session_manager_registered( + state: Rc>, + main_sender: mpsc::Sender, +) +{ + // Checking if session manager is registered because we need "default" metadata + // object to determine default audio nodes (sink and source). + fn generate_error_message(session_managers: &Vec<&str>) -> String { + let session_managers = session_managers.iter() + .map(move |session_manager| { + let session_manager = match *session_manager { + APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER => "WirePlumber", + APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION => "PipeWire Media Session", + _ => panic!("Cannot determine session manager name") + }; + format!(" - {}", session_manager) + }) + .collect::>() + .join("\n"); + let message = format!( + "No session manager registered. Install and run one of the following:\n{}", + session_managers + ); + message + } + let session_managers = vec![ + APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER, + APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION + ]; + let state = state.borrow_mut(); + let clients = state.get_clients().map_err(|_| { + Error { + description: generate_error_message(&session_managers), + } + }); + let clients = match clients { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let session_manager_registered = clients.iter() + .any(|(_, client)| { + session_managers.contains(&client.name.as_str()) + }); + if session_manager_registered { + return; + } + let description = generate_error_message(&session_managers); + main_sender + .send(MessageResponse::Error(Error { + description, + })) + .unwrap(); +} +fn handle_node_state( + id: GlobalId, + state: Rc>, + main_sender: mpsc::Sender, +) { + let state = state.borrow(); + let node = match state.get_node(&id) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let state = node.state(); + main_sender.send(MessageResponse::NodeState(state)).unwrap(); +} +fn handle_node_states( + state: Rc>, + main_sender: mpsc::Sender, +) +{ + let state = state.borrow_mut(); + let nodes = match state.get_nodes() { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let states = nodes.iter() + .map(move |(_, node)| { + node.state() + }) + .collect::>(); + main_sender.send(MessageResponse::NodeStates(states)).unwrap(); +} \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/thread.rs b/pipewire-client/src/client/handlers/thread.rs new file mode 100644 index 000000000..f3b1b481a --- /dev/null +++ b/pipewire-client/src/client/handlers/thread.rs @@ -0,0 +1,134 @@ +use crate::client::connection_string::PipewireClientInfo; +use crate::client::handlers::event::event_handler; +use crate::client::handlers::registry::registry_global_handler; +use crate::client::handlers::request::request_handler; +use crate::constants::PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ; +use crate::error::Error; +use crate::messages::{EventMessage, MessageRequest, MessageResponse}; +use crate::states::GlobalState; +use crate::utils::PipewireCoreSync; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::mpsc; + +pub fn pw_thread( + client_info: PipewireClientInfo, + main_sender: mpsc::Sender, + pw_receiver: pipewire::channel::Receiver, + event_sender: pipewire::channel::Sender, + event_receiver: pipewire::channel::Receiver, +) { + let connection_properties = Some(pipewire::properties::properties! { + *pipewire::keys::REMOTE_NAME => client_info.connection_string, + *pipewire::keys::APP_NAME => client_info.name, + }); + + let main_loop = match pipewire::main_loop::MainLoop::new(None) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(Error { + description: format!("Failed to create PipeWire main loop: {}", value), + })) + .unwrap(); + return; + } + }; + + let context = match pipewire::context::Context::new(&main_loop) { + Ok(value) => Rc::new(value), + Err(value) => { + main_sender + .send(MessageResponse::Error(Error { + description: format!("Failed to create PipeWire context: {}", value), + })) + .unwrap(); + return; + } + }; + + let core = match context.connect(connection_properties) { + Ok(value) => value, + Err(value) => { + main_sender + .send(MessageResponse::Error(Error { + description: format!("Failed to connect PipeWire server: {}", value), + })) + .unwrap(); + return; + } + }; + + let listener_main_sender = main_sender.clone(); + let _core_listener = core + .add_listener_local() + .error(move |_, _, _, message| { + listener_main_sender + .send(MessageResponse::Error(Error { + description: format!("Server error: {}", message), + })) + .unwrap(); + }) + .register(); + + let registry = match core.get_registry() { + Ok(value) => Rc::new(value), + Err(value) => { + unsafe { + pipewire::deinit(); + } + panic!("Failed to get Pipewire registry: {}", value); + } + }; + + let core_sync = Rc::new(PipewireCoreSync::new(Rc::new(RefCell::new(core.clone())))); + let core = Rc::new(core); + let state = Rc::new(RefCell::new(GlobalState::default())); + + let listener_main_sender = main_sender.clone(); + core_sync.register( + false, + PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ, + move || { + listener_main_sender + .send(MessageResponse::Initialized) + .unwrap(); + } + ); + + let _attached_event_receiver = event_receiver.attach( + main_loop.loop_(), + event_handler( + state.clone(), + main_sender.clone(), + event_sender.clone() + ) + ); + + let _attached_pw_receiver = pw_receiver.attach( + main_loop.loop_(), + request_handler( + core.clone(), + core_sync.clone(), + main_loop.clone(), + state.clone(), + main_sender.clone() + ) + ); + + let _registry_listener = registry + .add_listener_local() + .global(registry_global_handler( + state.clone(), + registry.clone(), + main_sender.clone(), + event_sender.clone(), + )) + .global_remove(move |global_id| { + let mut state = state.borrow_mut(); + state.remove(&global_id.into()) + }) + .register(); + + main_loop.run(); +} \ No newline at end of file diff --git a/pipewire-client/src/client/implementation.rs b/pipewire-client/src/client/implementation.rs new file mode 100644 index 000000000..067ae3ae0 --- /dev/null +++ b/pipewire-client/src/client/implementation.rs @@ -0,0 +1,430 @@ +extern crate pipewire; + +use crate::client::connection_string::{PipewireClientConnectionString, PipewireClientInfo}; +use crate::client::handlers::thread; +use crate::error::Error; +use crate::info::{AudioStreamInfo, NodeInfo}; +use crate::messages::{EventMessage, MessageRequest, MessageResponse, StreamCallback}; +use crate::states::{DefaultAudioNodesState, GlobalId, GlobalObjectState, SettingsState}; +use crate::utils::{Direction, Backoff}; +use std::fmt::{Debug, Formatter}; +use std::string::ToString; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::mpsc; +use std::thread; +use std::thread::JoinHandle; + +pub(super) static CLIENT_NAME_PREFIX: &str = "cpal-client"; +pub(super) static CLIENT_INDEX: AtomicU32 = AtomicU32::new(0); + +pub struct PipewireClient { + pub(crate) name: String, + connection_string: String, + sender: pipewire::channel::Sender, + receiver: mpsc::Receiver, + thread_handle: Option>, +} + +impl PipewireClient { + pub fn new() -> Result { + let name = format!("{}-{}", CLIENT_NAME_PREFIX, CLIENT_INDEX.load(Ordering::SeqCst)); + CLIENT_INDEX.fetch_add(1, Ordering::SeqCst); + + let connection_string = PipewireClientConnectionString::from_env(); + + let client_info = PipewireClientInfo { + name: name.clone(), + connection_string: connection_string.clone(), + }; + + let (main_sender, main_receiver) = mpsc::channel(); + let (pw_sender, pw_receiver) = pipewire::channel::channel(); + let (event_sender, event_receiver) = pipewire::channel::channel::(); + + let pw_thread = thread::spawn(move || thread( + client_info, + main_sender, + pw_receiver, + event_sender, + event_receiver + )); + + let client = Self { + name, + connection_string, + sender: pw_sender, + receiver: main_receiver, + thread_handle: Some(pw_thread), + }; + + match client.wait_initialization() { + Ok(_) => {} + Err(value) => return Err(value) + }; + match client.wait_post_initialization() { + Ok(_) => {} + Err(value) => return Err(value), + }; + Ok(client) + } + + fn wait_initialization(&self) -> Result<(), Error> { + let response = self.receiver.recv(); + let response = match response { + Ok(value) => value, + Err(value) => { + return Err(Error { + description: format!( + "Failed during pipewire initialization: {:?}", + value + ), + }) + } + }; + match response { + MessageResponse::Initialized => Ok(()), + _ => Err(Error { + description: format!("Received unexpected response: {:?}", response), + }), + } + } + + fn wait_post_initialization(&self) -> Result<(), Error> { + let mut settings_initialized = false; + let mut default_audio_devices_initialized = false; + let mut nodes_initialized = false; + #[cfg(debug_assertions)] + let timeout_duration = std::time::Duration::from_secs(u64::MAX); + #[cfg(not(debug_assertions))] + let timeout_duration = std::time::Duration::from_millis(500); + self.check_session_manager_registered()?; + self.node_states()?; + let operation = move || { + let response = self.receiver.recv_timeout(timeout_duration); + match response { + Ok(value) => match value { + MessageResponse::SettingsState(state) => { + match state { + GlobalObjectState::Initialized => { + settings_initialized = true; + } + _ => return Err(Error { + description: "Settings not yet initialized".to_string(), + }) + }; + }, + MessageResponse::DefaultAudioNodesState(state) => { + match state { + GlobalObjectState::Initialized => { + default_audio_devices_initialized = true; + } + _ => return Err(Error { + description: "Default audio nodes not yet initialized".to_string(), + }) + } + }, + MessageResponse::NodeStates(states) => { + let condition = states.iter() + .all(|state| *state == GlobalObjectState::Initialized); + match condition { + true => { + nodes_initialized = true; + }, + false => { + self.node_states()?; + return Err(Error { + description: "All nodes should be initialized at this point".to_string(), + }) + } + }; + } + MessageResponse::Error(value) => return Err(value), + _ => return Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + Err(value) => return Err(Error { + description: format!("Failed during post initialization: {:?}", value), + }) + }; + if settings_initialized == false || default_audio_devices_initialized == false || nodes_initialized == false { + return Err(Error { + description: "Post initialization not yet finalized".to_string(), + }) + + } + return Ok(()); + }; + let mut backoff = Backoff::new( + 30, + std::time::Duration::from_millis(10), + std::time::Duration::from_millis(100), + ); + backoff.retry(operation) + } + + fn send_request(&self, request: &MessageRequest) -> Result { + let response = self.sender.send(request.clone()); + let response = match response { + Ok(_) => self.receiver.recv(), + Err(_) => return Err(Error { + description: format!("Failed to send request: {:?}", request), + }), + }; + match response { + Ok(value) => { + match value { + MessageResponse::Error(value) => Err(value), + _ => Ok(value), + } + }, + Err(value) => Err(Error { + description: format!( + "Failed to execute request ({:?}): {:?}", + request, value + ), + }), + } + } + + fn send_request_without_response(&self, request: &MessageRequest) -> Result<(), Error> { + let response = self.sender.send(request.clone()); + match response { + Ok(_) => Ok(()), + Err(value) => Err(Error { + description: format!( + "Failed to execute request ({:?}): {:?}", + request, value + ), + }), + } + } + + pub fn quit(&self) { + let request = MessageRequest::Quit; + self.send_request_without_response(&request).unwrap(); + } + + pub fn settings(&self) -> Result { + let request = MessageRequest::Settings; + let response = self.send_request(&request); + match response { + Ok(MessageResponse::Settings(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + + pub fn default_audio_nodes(&self) -> Result { + let request = MessageRequest::DefaultAudioNodes; + let response = self.send_request(&request); + match response { + Ok(MessageResponse::DefaultAudioNodes(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + + pub fn create_node( + &self, + name: String, + description: String, + nickname: String, + direction: Direction, + channels: u16, + ) -> Result<(), Error> { + let request = MessageRequest::CreateNode { + name, + description, + nickname, + direction, + channels, + }; + let response = self.send_request(&request); + match response { + Ok(MessageResponse::CreateNode { + id + }) => { + #[cfg(debug_assertions)] + let timeout_duration = std::time::Duration::from_secs(u64::MAX); + #[cfg(not(debug_assertions))] + let timeout_duration = std::time::Duration::from_millis(500); + self.node_state(&id)?; + let operation = move || { + let response = self.receiver.recv_timeout(timeout_duration); + return match response { + Ok(value) => match value { + MessageResponse::NodeState(state) => { + match state == GlobalObjectState::Initialized { + true => { + Ok(()) + }, + false => { + self.node_state(&id)?; + Err(Error { + description: "Created node should be initialized at this point".to_string(), + }) + } + } + } + _ => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + }, + Err(value) => Err(Error { + description: format!("Failed during post initialization: {:?}", value), + }) + }; + }; + let mut backoff = Backoff::new( + 10, + std::time::Duration::from_millis(10), + std::time::Duration::from_millis(100), + ); + backoff.retry(operation) + }, + Ok(MessageResponse::Error(value)) => Err(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + + pub fn enumerate_nodes( + &self, + direction: Direction, + ) -> Result, Error> { + let request = MessageRequest::EnumerateNodes(direction); + let response = self.send_request(&request); + match response { + Ok(MessageResponse::EnumerateNodes(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + + pub fn create_stream( + &self, + node_id: u32, + direction: Direction, + format: AudioStreamInfo, + callback: F, + ) -> Result + where + F: FnMut(pipewire::buffer::Buffer) + Send + 'static + { + let request = MessageRequest::CreateStream { + node_id: GlobalId::from(node_id), + direction, + format, + callback: StreamCallback::from(callback), + }; + let response = self.send_request(&request); + match response { + Ok(MessageResponse::CreateStream{name}) => Ok(name), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + + pub fn delete_stream( + &self, + name: String + ) -> Result<(), Error> { + let request = MessageRequest::DeleteStream { + name, + }; + let response = self.send_request(&request); + match response { + Ok(MessageResponse::DeleteStream) => Ok(()), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value) + }), + } + } + + pub fn connect_stream( + &self, + name: String + ) -> Result<(), Error> { + let request = MessageRequest::ConnectStream { + name, + }; + let response = self.send_request(&request); + match response { + Ok(MessageResponse::ConnectStream) => Ok(()), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + + pub fn disconnect_stream( + &self, + name: String + ) -> Result<(), Error> { + let request = MessageRequest::DisconnectStream { + name, + }; + let response = self.send_request(&request); + match response { + Ok(MessageResponse::DisconnectStream) => Ok(()), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + + // Internal requests + pub(super) fn check_session_manager_registered(&self) -> Result<(), Error> { + let request = MessageRequest::CheckSessionManagerRegistered; + self.send_request_without_response(&request) + } + + pub(super) fn node_state( + &self, + id: &GlobalId, + ) -> Result<(), Error> { + let request = MessageRequest::NodeState(id.clone()); + self.send_request_without_response(&request) + } + + pub(super) fn node_states( + &self, + ) -> Result<(), Error> { + let request = MessageRequest::NodeStates; + self.send_request_without_response(&request) + } +} + +impl Debug for PipewireClient { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "PipewireClient: {}", self.connection_string) + } +} + +impl Drop for PipewireClient { + fn drop(&mut self) { + if self.sender.send(MessageRequest::Quit).is_ok() { + if let Some(thread_handle) = self.thread_handle.take() { + if let Err(err) = thread_handle.join() { + panic!("Failed to join PipeWire thread: {:?}", err); + } + } + } else { + panic!("Failed to send Quit message to PipeWire thread."); + } + } +} \ No newline at end of file diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs new file mode 100644 index 000000000..c844b69bf --- /dev/null +++ b/pipewire-client/src/client/implementation_test.rs @@ -0,0 +1,162 @@ +use std::sync::atomic::Ordering; +use rstest::rstest; +use serial_test::serial; +use crate::client::implementation::CLIENT_INDEX; +use crate::{Direction, PipewireClient}; + +#[rstest] +#[serial] +pub fn all() { + for _ in 0..100 { + name(); + quit(); + settings(); + default_audio_nodes(); + create_node(); + create_node_then_enumerate_nodes(); + create_stream(); + enumerate_nodes(); + } +} + +#[rstest] +#[serial] +pub fn name() { + let client_1 = PipewireClient::new().unwrap(); + assert_eq!(format!("cpal-client-{}", CLIENT_INDEX.load(Ordering::SeqCst) - 1), client_1.name); + let client_2 = PipewireClient::new().unwrap(); + assert_eq!(format!("cpal-client-{}", CLIENT_INDEX.load(Ordering::SeqCst) - 1), client_2.name); +} + +#[rstest] +#[serial] +fn quit() { + let client = PipewireClient::new().unwrap(); + client.quit(); +} + +#[rstest] +#[serial] +fn settings() { + let client = PipewireClient::new().unwrap(); + let response = client.settings(); + assert!( + response.is_ok(), + "Should send settings message without errors" + ); + let settings = response.unwrap(); + assert_eq!(true, settings.sample_rate > u32::default()); + assert_eq!(true, settings.default_buffer_size > u32::default()); + assert_eq!(true, settings.min_buffer_size > u32::default()); + assert_eq!(true, settings.max_buffer_size > u32::default()); + assert_eq!(true, settings.allowed_sample_rates[0] > u32::default()); +} + +#[rstest] +#[serial] +fn default_audio_nodes() { + let client = PipewireClient::new().unwrap(); + let response = client.default_audio_nodes(); + assert!( + response.is_ok(), + "Should send default audio nodes message without errors" + ); + let default_audio_nodes = response.unwrap(); + assert_eq!(false, default_audio_nodes.sink.is_empty()); + assert_eq!(false, default_audio_nodes.source.is_empty()); +} + +#[rstest] +#[serial] +fn create_node() { + let client = PipewireClient::new().unwrap(); + let response = client.create_node( + "test".to_string(), + "test".to_string(), + "test".to_string(), + Direction::Output, + 2 + ); + assert!( + response.is_ok(), + "Should send create node message without errors" + ); +} + +#[rstest] +#[serial] +fn create_node_then_enumerate_nodes() { + let client = PipewireClient::new().unwrap(); + let response = client.create_node( + "test".to_string(), + "test".to_string(), + "test".to_string(), + Direction::Output, + 2 + ); + assert!( + response.is_ok(), + "Should send create node message without errors" + ); + let response = client.enumerate_nodes(Direction::Output); + assert!( + response.is_ok(), + "Should send enumerate devices message without errors" + ); + let nodes = response.unwrap(); + assert_eq!(false, nodes.is_empty()); + let default_node = nodes.iter() + .filter(|node| node.is_default) + .last(); + assert_eq!(true, default_node.is_some()); +} + +#[rstest] +#[serial] +fn create_stream() { + let client = PipewireClient::new().unwrap(); + let response = client.enumerate_nodes(Direction::Output).unwrap(); + let default_node = response.iter() + .filter(|node| node.is_default) + .last() + .unwrap(); + let response = client.create_stream( + default_node.id, + Direction::Output, + default_node.format.clone().into(), + move |mut buffer| { + let data = buffer.datas_mut(); + let data = &mut data[0]; + let data = data.data().unwrap(); + assert_eq!(true, data.len() > 0); + } + ); + assert!( + response.is_ok(), + "Should send create stream message without errors" + ); + let stream_name = response.ok().unwrap(); + let response = client.connect_stream(stream_name); + std::thread::sleep(std::time::Duration::from_millis(1 * 1000)); + assert!( + response.is_ok(), + "Should send connect stream message without errors" + ); +} + +#[rstest] +#[serial] +fn enumerate_nodes() { + let client = PipewireClient::new().unwrap(); + let response = client.enumerate_nodes(Direction::Output); + assert!( + response.is_ok(), + "Should send enumerate devices message without errors" + ); + let nodes = response.unwrap(); + assert_eq!(false, nodes.is_empty()); + let default_node = nodes.iter() + .filter(|node| node.is_default) + .last(); + assert_eq!(true, default_node.is_some()); +} \ No newline at end of file diff --git a/pipewire-client/src/client/mod.rs b/pipewire-client/src/client/mod.rs new file mode 100644 index 000000000..7b16bb32a --- /dev/null +++ b/pipewire-client/src/client/mod.rs @@ -0,0 +1,8 @@ +mod implementation; +pub use implementation::PipewireClient; +mod connection_string; +mod handlers; + +#[cfg(test)] +#[path = "implementation_test.rs"] +mod implementation_test; \ No newline at end of file diff --git a/pipewire-client/src/constants.rs b/pipewire-client/src/constants.rs new file mode 100644 index 000000000..0bb88192e --- /dev/null +++ b/pipewire-client/src/constants.rs @@ -0,0 +1,32 @@ +pub(super) const PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY: &str = "PIPEWIRE_RUNTIME_DIR"; +pub(super) const PIPEWIRE_REMOTE_ENVIRONMENT_KEY: &str = "PIPEWIRE_REMOTE"; +pub(super) const XDG_RUNTIME_DIR_ENVIRONMENT_KEY: &str = "XDG_RUNTIME_DIR"; +pub(super) const PIPEWIRE_REMOTE_ENVIRONMENT_DEFAULT: &str = "pipewire-0"; + +pub(super) const PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ :u32 = 0; +pub(super) const PIPEWIRE_CORE_SYNC_CREATE_DEVICE_SEQ :u32 = 1; + +pub(super) const MEDIA_TYPE_PROPERTY_VALUE_AUDIO: &str = "Audio"; +pub(super) const MEDIA_CLASS_PROPERTY_KEY: &str = "media.class"; +pub(super) const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE: &str = "Audio/Source"; +pub(super) const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK: &str = "Audio/Sink"; +pub(super) const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_DUPLEX: &str = "Audio/Duplex"; +pub(super) const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_DEVICE: &str = "Audio/Device"; +pub(super) const MEDIA_CLASS_PROPERTY_VALUE_STREAM_OUTPUT_AUDIO: &str = "Stream/Output/Audio"; +pub(super) const MEDIA_CLASS_PROPERTY_VALUE_STREAM_INPUT_AUDIO: &str = "Stream/Input/Audio"; +pub(super) const METADATA_NAME_PROPERTY_KEY: &str = "metadata.name"; +pub(super) const METADATA_NAME_PROPERTY_VALUE_SETTINGS: &str = "settings"; +pub(super) const METADATA_NAME_PROPERTY_VALUE_DEFAULT: &str = "default"; +pub(super) const CLOCK_RATE_PROPERTY_KEY: &str = "clock.rate"; +pub(super) const CLOCK_QUANTUM_PROPERTY_KEY: &str = "clock.quantum"; +pub(super) const CLOCK_QUANTUM_MIN_PROPERTY_KEY: &str = "clock.min-quantum"; +pub(super) const CLOCK_QUANTUM_MAX_PROPERTY_KEY: &str = "clock.max-quantum"; +pub(super) const CLOCK_ALLOWED_RATES_PROPERTY_KEY: &str = "clock.allowed-rates"; +pub(super) const MONITOR_CHANNEL_VOLUMES_PROPERTY_KEY: &str = "monitor.channel-volumes"; +pub(super) const MONITOR_PASSTHROUGH_PROPERTY_KEY: &str = "monitor.passthrough"; +pub(super) const DEFAULT_AUDIO_SINK_PROPERTY_KEY: &str = "default.audio.sink"; +pub(super) const DEFAULT_AUDIO_SOURCE_PROPERTY_KEY: &str = "default.audio.source"; +pub(super) const AUDIO_POSITION_PROPERTY_KEY: &str = "audio.position"; +pub(super) const APPLICATION_NAME_PROPERTY_KEY: &str = "application.name"; +pub(super) const APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER: &str = "WirePlumber"; +pub(super) const APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION: &str = "pipewire-media-session"; \ No newline at end of file diff --git a/pipewire-client/src/error.rs b/pipewire-client/src/error.rs new file mode 100644 index 000000000..05e44d5d5 --- /dev/null +++ b/pipewire-client/src/error.rs @@ -0,0 +1,15 @@ +use std::error::Error as StdError; +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Clone)] +pub struct Error { + pub description: String, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.description) + } +} + +impl StdError for Error {} \ No newline at end of file diff --git a/pipewire-client/src/info.rs b/pipewire-client/src/info.rs new file mode 100644 index 000000000..9f6f07ec9 --- /dev/null +++ b/pipewire-client/src/info.rs @@ -0,0 +1,53 @@ +use pipewire_spa_utils::audio::{AudioChannelPosition}; +use pipewire_spa_utils::audio::AudioSampleFormat; +use pipewire_spa_utils::audio::raw::AudioInfoRaw; +use pipewire_spa_utils::format::{MediaSubtype, MediaType}; +use crate::utils::Direction; + +#[derive(Debug, Clone)] +pub struct NodeInfo { + pub id: u32, + pub name: String, + pub description: String, + pub nickname: String, + pub direction: Direction, + pub is_default: bool, + pub format: AudioInfoRaw +} + +#[derive(Debug, Clone)] +pub struct AudioStreamInfo { + pub media_type: MediaType, + pub media_subtype: MediaSubtype, + pub sample_format: AudioSampleFormat, + pub sample_rate: u32, + pub channels: u32, + pub position: AudioChannelPosition +} + +impl From for AudioStreamInfo { + fn from(value: AudioInfoRaw) -> Self { + Self { + media_type: MediaType::Audio, + media_subtype: MediaSubtype::Raw, + sample_format: value.sample_format.default, + sample_rate: value.sample_rate.value, + channels: *value.channels, + position: AudioChannelPosition::default(), + } + } +} + +impl From for pipewire::spa::param::audio::AudioInfoRaw { + fn from(value: AudioStreamInfo) -> Self { + let format: pipewire::spa::sys::spa_audio_format = value.sample_format as u32; + let format = pipewire::spa::param::audio::AudioFormat::from_raw(format); + let position: [u32; 64] = value.position.to_array(); + let mut info = pipewire::spa::param::audio::AudioInfoRaw::default(); + info.set_format(format); + info.set_rate(value.sample_rate); + info.set_channels(value.channels); + info.set_position(position); + info + } +} \ No newline at end of file diff --git a/pipewire-client/src/lib.rs b/pipewire-client/src/lib.rs new file mode 100644 index 000000000..d330433a3 --- /dev/null +++ b/pipewire-client/src/lib.rs @@ -0,0 +1,19 @@ +mod client; +pub use client::PipewireClient; + +mod constants; +mod listeners; +mod messages; +mod states; + +mod utils; +pub use utils::Direction; + +mod error; + +mod info; +pub use info::NodeInfo; +pub use info::AudioStreamInfo; + +pub use pipewire as pipewire; +pub use pipewire_spa_utils as spa_utils; diff --git a/pipewire-client/src/listeners.rs b/pipewire-client/src/listeners.rs new file mode 100644 index 000000000..9591a8ee7 --- /dev/null +++ b/pipewire-client/src/listeners.rs @@ -0,0 +1,49 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +#[derive(Debug, Clone, PartialEq)] +pub(super) enum ListenerTriggerPolicy { + Keep, + Remove +} + +pub(super) struct Listener { + inner: T, + trigger_policy: ListenerTriggerPolicy, +} + +impl Listener { + pub fn new(inner: T, policy: ListenerTriggerPolicy) -> Self + { + Self { + inner, + trigger_policy: policy, + } + } +} + +pub(super) struct Listeners { + listeners: Rc>>>, +} + +impl Listeners { + pub fn new() -> Self { + Self { + listeners: Rc::new(RefCell::new(HashMap::new())), + } + } + + pub fn add(&mut self, name: String, listener: Listener) { + let mut listeners = self.listeners.borrow_mut(); + listeners.insert(name, listener); + } + + pub fn triggered(&mut self, name: &String) { + let mut listeners = self.listeners.borrow_mut(); + let listener = listeners.get_mut(name).unwrap(); + if listener.trigger_policy == ListenerTriggerPolicy::Remove { + listeners.remove(name); + } + } +} \ No newline at end of file diff --git a/pipewire-client/src/messages.rs b/pipewire-client/src/messages.rs new file mode 100644 index 000000000..0d2991cf1 --- /dev/null +++ b/pipewire-client/src/messages.rs @@ -0,0 +1,118 @@ +use pipewire_spa_utils::audio::raw::AudioInfoRaw; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; +use std::sync::{Arc, Mutex}; +use crate::error::Error; +use crate::info::{AudioStreamInfo, NodeInfo}; +use crate::states::{DefaultAudioNodesState, GlobalId, GlobalObjectState, SettingsState}; +use crate::utils::Direction; + +pub(super) struct StreamCallback { + callback: Arc>> +} + +impl From for StreamCallback { + fn from(value: F) -> Self { + Self { callback: Arc::new(Mutex::new(Box::new(value))) } + } +} + +impl StreamCallback { + pub fn call(&mut self, buffer: pipewire::buffer::Buffer) { + let mut callback = self.callback.lock().unwrap(); + callback(buffer); + } +} + +impl Debug for StreamCallback { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StreamCallback").finish() + } +} + +impl Clone for StreamCallback { + fn clone(&self) -> Self { + Self { callback: self.callback.clone() } + } +} + +#[derive(Debug, Clone)] +pub(super) enum MessageRequest { + Quit, + Settings, + DefaultAudioNodes, + CreateNode { + name: String, + description: String, + nickname: String, + direction: Direction, + channels: u16, + }, + EnumerateNodes(Direction), + CreateStream { + node_id: GlobalId, + direction: Direction, + format: AudioStreamInfo, + callback: StreamCallback, + }, + DeleteStream { + name: String + }, + ConnectStream { + name: String + }, + DisconnectStream { + name: String + }, + // Internal requests + CheckSessionManagerRegistered, + NodeState(GlobalId), + NodeStates, +} + +#[derive(Debug, Clone)] +pub(super) enum MessageResponse { + Error(Error), + Initialized, + Settings(SettingsState), + DefaultAudioNodes(DefaultAudioNodesState), + CreateNode { + id: GlobalId + }, + EnumerateNodes(Vec), + CreateStream { + name: String, + }, + DeleteStream, + ConnectStream, + DisconnectStream, + // Internals responses + SettingsState(GlobalObjectState), + DefaultAudioNodesState(GlobalObjectState), + NodeState(GlobalObjectState), + NodeStates(Vec) +} + +#[derive(Debug, Clone)] +pub(super) enum EventMessage { + SetMetadataListeners { + id: GlobalId + }, + RemoveNode { + id: GlobalId + }, + SetNodePropertiesListener { + id: GlobalId + }, + SetNodeFormatListener{ + id: GlobalId + }, + SetNodeProperties { + id: GlobalId, + properties: HashMap, + }, + SetNodeFormat { + id: GlobalId, + format: AudioInfoRaw, + } +} \ No newline at end of file diff --git a/pipewire-client/src/states.rs b/pipewire-client/src/states.rs new file mode 100644 index 000000000..1bbfb5693 --- /dev/null +++ b/pipewire-client/src/states.rs @@ -0,0 +1,777 @@ +use super::constants::*; +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use std::io::Cursor; +use std::rc::Rc; +use std::str::FromStr; +use pipewire::spa::utils::dict::ParsableValue; +use pipewire_spa_utils::audio::AudioChannel; +use pipewire_spa_utils::audio::raw::AudioInfoRaw; +use pipewire_spa_utils::format::{MediaSubtype, MediaType}; +use crate::Direction; +use crate::error::Error; +use crate::listeners::{Listener, ListenerTriggerPolicy, Listeners}; +use crate::messages::StreamCallback; +use crate::utils::dict_ref_to_hashmap; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) struct GlobalId(u32); + +impl From for GlobalId { + fn from(value: String) -> Self { + u32::parse_value(value.as_str()).unwrap().into() + } +} + +impl From for GlobalId { + fn from(value: u32) -> Self { + GlobalId(value) + } +} + +impl From for GlobalId { + fn from(value: i32) -> Self { + GlobalId(value as u32) + } +} + +impl Into for GlobalId { + fn into(self) -> i32 { + self.0 as i32 + } +} + +impl From for u32 { + fn from(value: GlobalId) -> Self { + value.0 + } +} + +impl Display for GlobalId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(super) enum GlobalObjectState { + Pending, + Initialized +} + +pub(super) struct GlobalState { + orphans: Rc>>, + clients: HashMap, + metadata: HashMap, + nodes: HashMap, + streams: HashMap, + settings: SettingsState, + default_audio_nodes: DefaultAudioNodesState, +} + +impl GlobalState { + pub fn insert_orphan(&mut self, mut state: OrphanState) { + let index = std::ptr::addr_of!(state) as usize; + let listener_orphans = self.orphans.clone(); + state.add_removed_listener( + ListenerTriggerPolicy::Remove, + move || { + listener_orphans.borrow_mut().remove(&index); + } + ); + self.orphans.borrow_mut().insert(index, state); + } + + pub fn insert_client(&mut self, id: GlobalId, state: ClientState) -> Result<(), Error> { + if self.clients.contains_key(&id) { + return Err(Error { + description: format!("Client with id({}) already exists", id), + }); + } + self.clients.insert(id, state); + Ok(()) + } + + pub fn get_clients(&self) -> Result, Error> { + let clients = self.clients.iter() + .map(|(id, state)| (id, state)) + .collect::>(); + if clients.is_empty() { + return Err(Error { + description: "Zero client registered".to_string(), + }) + } + Ok(clients) + } + + pub fn insert_metadata(&mut self, id: GlobalId, state: MetadataState) -> Result<(), Error> { + if self.metadata.contains_key(&id) { + return Err(Error { + description: format!("Metadata with id({}) already exists", id), + }); + } + self.metadata.insert(id, state); + Ok(()) + } + + pub fn get_metadata(&self, id: &GlobalId) -> Result<&MetadataState, Error> { + self.metadata.get(id).ok_or(Error { + description: format!("Metadata with id({}) not found", id), + }) + } + + pub fn get_metadata_mut(&mut self, id: &GlobalId) -> Result<&mut MetadataState, Error> { + self.metadata.get_mut(id).ok_or(Error { + description: format!("Metadata with id({}) not found", id), + }) + } + + pub fn insert_node(&mut self, id: GlobalId, state: NodeState) -> Result<(), Error> { + if self.nodes.contains_key(&id) { + return Err(Error { + description: format!("Node with id({}) already exists", id), + }); + } + self.nodes.insert(id, state); + Ok(()) + } + + pub fn get_node(&self, id: &GlobalId) -> Result<&NodeState, Error> { + self.nodes.get(id).ok_or(Error { + description: format!("Node with id({}) not found", id), + }) + } + + pub fn get_node_mut(&mut self, id: &GlobalId) -> Result<&mut NodeState, Error> { + self.nodes.get_mut(id).ok_or(Error { + description: format!("Node with id({}) not found", id), + }) + } + + pub fn get_nodes(&self) -> Result, Error> { + let nodes = self.nodes.iter() + .map(|(id, state)| (id, state)) + .collect::>(); + if nodes.is_empty() { + return Err(Error { + description: "Zero node registered".to_string(), + }) + } + Ok(nodes) + } + + pub fn insert_stream(&mut self, name: String, state: StreamState) -> Result<(), Error> { + if self.streams.contains_key(&name) { + return Err(Error { + description: format!("Stream with name({}) already exists", name), + }); + } + self.streams.insert(name, state); + Ok(()) + } + + pub fn remove_stream(&mut self, name: &String) -> Result<(), Error> { + if self.streams.contains_key(name) == false { + return Err(Error { + description: format!("Stream with name({}) not found", name), + }); + } + self.streams.remove(name); + Ok(()) + } + + pub fn get_stream(&self, name: &String) -> Result<&StreamState, Error> { + self.streams.get(name).ok_or(Error { + description: format!("Stream with name({}) not found", name), + }) + } + + pub fn get_stream_mut(&mut self, name: &String) -> Result<&mut StreamState, Error> { + self.streams.get_mut(name).ok_or(Error { + description: format!("Stream with name({}) not found", name), + }) + } + + pub fn get_settings(&self) -> SettingsState { + self.settings.clone() + } + + pub fn get_default_audio_nodes(&self) -> DefaultAudioNodesState { + self.default_audio_nodes.clone() + } + + pub fn remove(&mut self, id: &GlobalId) { + self.metadata.remove(id); + self.nodes.remove(id); + } +} + +impl Default for GlobalState { + fn default() -> Self { + GlobalState { + orphans: Rc::new(RefCell::new(HashMap::new())), + clients: HashMap::new(), + metadata: HashMap::new(), + nodes: HashMap::new(), + streams: HashMap::new(), + settings: SettingsState::default(), + default_audio_nodes: DefaultAudioNodesState::default(), + } + } +} + +pub(super) struct OrphanState { + proxy: pipewire::proxy::Proxy, + listeners: Rc>> +} + +impl OrphanState { + pub fn new(proxy: pipewire::proxy::Proxy) -> Self { + Self { + proxy, + listeners: Rc::new(RefCell::new(Listeners::new())), + } + } + + pub fn add_removed_listener(&mut self, policy: ListenerTriggerPolicy, callback: F) + where + F: Fn() + 'static + { + const LISTENER_NAME: &str = "removed"; + let listeners = self.listeners.clone(); + let listener = self.proxy.add_listener_local() + .removed(move || { + callback(); + listeners.borrow_mut().triggered(&LISTENER_NAME.to_string()); + }) + .register(); + self.listeners.borrow_mut().add( + LISTENER_NAME.to_string(), + Listener::new(listener, policy) + ); + } +} + +pub(super) struct NodeState { + proxy: pipewire::node::Node, + state: Rc>, + properties: Rc>>, + format: Rc>>, + listeners: Rc>> +} + +impl NodeState { + pub fn new(proxy: pipewire::node::Node) -> Self { + Self { + proxy, + state: Rc::new(RefCell::new(GlobalObjectState::Pending)), + properties: Rc::new(RefCell::new(HashMap::new())), + format: Rc::new(RefCell::new(None)), + listeners: Rc::new(RefCell::new(Listeners::new())), + } + } + + pub fn state(&self) -> GlobalObjectState { + self.state.borrow().clone() + } + + fn set_state(&mut self) { + let properties = self.properties.borrow(); + let format = self.format.borrow(); + + let new_state = if properties.is_empty() == false && format.is_some() { + GlobalObjectState::Initialized + } else { + GlobalObjectState::Pending + }; + + let mut state = self.state.borrow_mut(); + *state = new_state; + } + + pub fn properties(&self) -> HashMap { + self.properties.borrow().clone() + } + + pub fn set_properties(&mut self, properties: HashMap) { + self.properties.borrow_mut().extend(properties); + self.set_state(); + } + + pub fn format(&self) -> Option { + self.format.borrow().clone() + } + + pub fn set_format(&mut self, format: AudioInfoRaw) { + *self.format.borrow_mut() = Some(format); + self.set_state(); + } + + pub fn name(&self) -> String { + self.properties.borrow().get(*pipewire::keys::NODE_NAME).unwrap().clone() + } + + fn add_info_listener(&mut self, name: String, policy: ListenerTriggerPolicy, listener: F) + where + F: Fn(&pipewire::node::NodeInfoRef) + 'static + { + let listeners = self.listeners.clone(); + let listener_name = name.clone(); + let listener = self.proxy.add_listener_local() + .info(move |info| { + listener(info); + listeners.borrow_mut().triggered(&listener_name); + }) + .register(); + self.listeners.borrow_mut().add(name, Listener::new(listener, policy)); + } + + pub fn add_properties_listener(&mut self, policy: ListenerTriggerPolicy, callback: F) + where + F: Fn(HashMap) + 'static, + { + self.add_info_listener( + "properties".to_string(), + policy, + move |info| { + if info.props().is_none() { + return; + } + let properties = info.props().unwrap(); + let properties = dict_ref_to_hashmap(properties); + callback(properties); + } + ); + } + + fn add_parameter_listener( + &mut self, + name: String, + expected_kind: pipewire::spa::param::ParamType, + policy: ListenerTriggerPolicy, + listener: F + ) + where + F: Fn(u32, u32, &pipewire::spa::pod::Pod) + 'static, + { + let listeners = self.listeners.clone(); + let listener_name = name.clone(); + self.proxy.subscribe_params(&[expected_kind]); + let listener = self.proxy.add_listener_local() + .param(move |_, kind, id, next_id, parameter| { + if kind != expected_kind { + return; + } + let Some(parameter) = parameter else { + return; + }; + listener(id, next_id, parameter); + listeners.borrow_mut().triggered(&listener_name); + }) + .register(); + self.listeners.borrow_mut().add(name, Listener::new(listener, policy)); + } + + pub fn add_format_listener(&mut self, policy: ListenerTriggerPolicy, callback: F) + where + F: Fn(Result) + 'static, + { + self.add_parameter_listener( + "format".to_string(), + pipewire::spa::param::ParamType::EnumFormat, + policy, + move |_, _, parameter| { + let (media_type, media_subtype): (MediaType, MediaSubtype) = + match pipewire::spa::param::format_utils::parse_format(parameter) { + Ok((media_type, media_subtype)) => (media_type.0.into(), media_subtype.0.into()), + Err(_) => return, + }; + let pod = parameter; + let data = pod.as_bytes(); + let parameter = match media_type { + MediaType::Audio => match media_subtype { + MediaSubtype::Raw => { + let result = pipewire::spa::pod::deserialize::PodDeserializer::deserialize_from(data); + let result = result + .map(move |(_, parameter)| { + parameter + }) + .map_err(move |error| { + let description = match error { + pipewire::spa::pod::deserialize::DeserializeError::Nom(_) => "Parsing error", + pipewire::spa::pod::deserialize::DeserializeError::UnsupportedType => "Unsupported type", + pipewire::spa::pod::deserialize::DeserializeError::InvalidType => "Invalid type", + pipewire::spa::pod::deserialize::DeserializeError::PropertyMissing => "Property missing", + pipewire::spa::pod::deserialize::DeserializeError::PropertyWrongKey(value) => &*format!( + "Wrong property key({})", + value + ), + pipewire::spa::pod::deserialize::DeserializeError::InvalidChoiceType => "Invalide choice type", + pipewire::spa::pod::deserialize::DeserializeError::MissingChoiceValues => "Missing choice values", + }; + Error { + description: format!( + "Failed POD deserialization for type(AudioInfoRaw): {}", + description + ), + } + }); + result + } + _ => return + }, + _ => return + }; + callback(parameter); + } + ); + } +} + +pub(super) struct ClientState { + pub(super) name: String +} + +impl ClientState { + pub fn new(name: String) -> Self { + Self { + name, + } + } +} + +pub(super) struct MetadataState { + proxy: pipewire::metadata::Metadata, + pub(super) state: Rc>, + pub(super) name: String, + listeners: Rc>>, +} + +impl MetadataState { + pub fn new(proxy: pipewire::metadata::Metadata, name: String) -> Self { + Self { + proxy, + name, + state: Rc::new(RefCell::new(GlobalObjectState::Pending)), + listeners: Rc::new(RefCell::new(Listeners::new())), + } + } + + pub fn add_property_listener(&mut self, policy: ListenerTriggerPolicy, listener: F) + where + F: Fn(u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + Sized + 'static + { + const LISTENER_NAME: &str = "property"; + let listeners = self.listeners.clone(); + let listener = self.proxy.add_listener_local() + .property(move |subject , key, kind, value| { + let result = listener(subject, key, kind, value); + listeners.borrow_mut().triggered(&LISTENER_NAME.to_string()); + result + }) + .register(); + self.listeners.borrow_mut().add( + LISTENER_NAME.to_string(), + Listener::new(listener, policy) + ); + } +} + +#[derive(Debug, Default)] +pub(super) struct StreamUserData {} + +pub(super) struct StreamState { + proxy: pipewire::stream::Stream, + pub(super) name: String, + is_connected: bool, + format: pipewire::spa::param::audio::AudioInfoRaw, + direction: pipewire::spa::utils::Direction, + listeners: Rc>>>, +} + +impl StreamState { + pub fn new( + name: String, + format: pipewire::spa::param::audio::AudioInfoRaw, + direction: pipewire::spa::utils::Direction, + proxy: pipewire::stream::Stream + ) -> Self { + Self { + name, + proxy, + is_connected: false, + format, + direction, + listeners: Rc::new(RefCell::new(Listeners::new())), + } + } + + pub fn connect(&mut self) -> Result<(), Error> { + if self.is_connected { + return Err(Error { + description: format!("Stream {} is already connected", self.name) + }); + } + + let object = pipewire::spa::pod::Value::Object(pipewire::spa::pod::Object { + type_: pipewire::spa::sys::SPA_TYPE_OBJECT_Format, + id: pipewire::spa::sys::SPA_PARAM_EnumFormat, + properties: self.format.into(), + }); + + let values: Vec = pipewire::spa::pod::serialize::PodSerializer::serialize( + Cursor::new(Vec::new()), + &object, + ) + .unwrap() + .0 + .into_inner(); + + let mut params = [pipewire::spa::pod::Pod::from_bytes(&values).unwrap()]; + let flags = pipewire::stream::StreamFlags::AUTOCONNECT | pipewire::stream::StreamFlags::MAP_BUFFERS; + + self.proxy + .connect( + self.direction, + None, + flags, + &mut params, + ) + .map_err(move |error| Error { description: error.to_string() })?; + + self.is_connected = true; + + Ok(()) + } + + pub fn disconnect(&mut self) -> Result<(), Error> { + if self.is_connected == false { + return Err(Error { + description: format!("Stream {} is not connected", self.name) + }); + } + self.proxy + .disconnect() + .map_err(move |error| Error { description: error.to_string() })?; + + self.is_connected = false; + + Ok(()) + } + + pub fn add_process_listener( + &mut self, + policy: ListenerTriggerPolicy, + mut callback: StreamCallback + ) + { + const LISTENER_NAME: &str = "process"; + let listeners = self.listeners.clone(); + let listener = self.proxy.add_local_listener() + .process(move |stream, _| { + let buffer = stream.dequeue_buffer().unwrap(); + callback.call(buffer); + listeners.borrow_mut().triggered(&LISTENER_NAME.to_string()); + }) + .register() + .unwrap(); + self.listeners.borrow_mut().add(LISTENER_NAME.to_string(), Listener::new(listener, policy)); + } +} + +pub(super) struct PortStateProperties { + path: String, + channel: AudioChannel, + id: GlobalId, + name: String, + direction: Direction, + alias: String, + group: String, +} + +impl From<&pipewire::spa::utils::dict::DictRef> for PortStateProperties { + fn from(value: &pipewire::spa::utils::dict::DictRef) -> Self { + let properties = dict_ref_to_hashmap(value); + let path = properties.get("object.path").unwrap().to_string(); + let channel = properties.get("audio.channel").unwrap().to_string(); + let id = properties.get("port.id").unwrap().to_string(); + let name = properties.get("port.name").unwrap().to_string(); + let direction = properties.get("port.direction").unwrap().to_string(); + let alias = properties.get("port.alias").unwrap().to_string(); + let group = properties.get("port.group").unwrap().to_string(); + Self { + path, + channel: AudioChannel::UNKNOWN, + id: id.into(), + name, + direction: match direction.as_str() { + "in" => Direction::Input, + "out" => Direction::Output, + &_ => panic!("Cannot determine direction: {}", direction.as_str()), + }, + alias, + group, + } + } +} + +pub(super) struct PortState { + proxy: pipewire::link::Link, + properties: Rc>>, + pub(super) state: Rc>, + listeners: Rc>>>, +} + +// impl PortState { +// pub fn new(proxy: pipewire::port::Port) { +// proxy.add_listener_local().info(move |x| { +// x. +// }) +// .param(move |subject , key, kind, value| { +// +// }) +// } +// } + +pub(super) struct LinkState { + proxy: pipewire::link::Link, + input_node_id: GlobalId, + input_port_id: GlobalId, + output_node_id: GlobalId, + output_port_id: GlobalId, + pub(super) state: Rc>, + listeners: Rc>>>, +} + +#[derive(Debug, Clone)] +pub struct SettingsState { + pub(super) state: GlobalObjectState, + pub allowed_sample_rates: Vec, + pub sample_rate: u32, + pub min_buffer_size: u32, + pub max_buffer_size: u32, + pub default_buffer_size: u32, +} + +impl Default for SettingsState { + fn default() -> Self { + Self { + state: GlobalObjectState::Pending, + allowed_sample_rates: vec![], + sample_rate: 0, + min_buffer_size: 0, + max_buffer_size: 0, + default_buffer_size: 0, + } + } +} + +impl SettingsState { + pub(super) fn listener(state: Rc>, callback: F) -> impl Fn(u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static + where + F: Fn(&SettingsState) + 'static + { + const EXPECTED_PROPERTY: u32 = 5; + let property_count: Rc> = Rc::new(Cell::new(0)); + move |_: u32, key: Option<&str>, _: Option<&str>, value: Option<&str>| { + let settings = &mut state.borrow_mut().settings; + let key = key.unwrap(); + let value = value.unwrap(); + match key { + CLOCK_RATE_PROPERTY_KEY => { + settings.sample_rate = u32::from_str(value).unwrap(); + property_count.set(property_count.get() + 1); + }, + CLOCK_QUANTUM_PROPERTY_KEY => { + settings.default_buffer_size = u32::from_str(value).unwrap(); + property_count.set(property_count.get() + 1); + } + CLOCK_QUANTUM_MIN_PROPERTY_KEY => { + settings.min_buffer_size = u32::from_str(value).unwrap(); + property_count.set(property_count.get() + 1); + } + CLOCK_QUANTUM_MAX_PROPERTY_KEY => { + settings.max_buffer_size = u32::from_str(value).unwrap(); + property_count.set(property_count.get() + 1); + } + CLOCK_ALLOWED_RATES_PROPERTY_KEY => { + let rates: Result, _> = value[2..value.len() - 2] + .split_whitespace() + .map(|x| x.parse::()) + .collect(); + settings.allowed_sample_rates = rates.unwrap(); + property_count.set(property_count.get() + 1); + } + &_ => {} + }; + if let (GlobalObjectState::Pending, EXPECTED_PROPERTY) = (settings.state.clone(), property_count.get()) { + settings.state = GlobalObjectState::Initialized; + callback(settings) + } + 0 + } + } +} + +#[derive(Debug, Clone)] +pub struct DefaultAudioNodesState { + pub(super) state: GlobalObjectState, + pub source: String, + pub sink: String, +} + +impl Default for DefaultAudioNodesState { + fn default() -> Self { + Self { + state: GlobalObjectState::Pending, + source: "".to_string(), + sink: "".to_string(), + } + } +} + +impl DefaultAudioNodesState { + pub(super) fn listener(state: Rc>, callback: F) -> impl Fn(u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static + where + F: Fn(&DefaultAudioNodesState) + 'static + { + const EXPECTED_PROPERTY: u32 = 2; + let property_count: Rc> = Rc::new(Cell::new(0)); + move |_: u32, key: Option<&str>, _: Option<&str>, value: Option<&str>| { + let default_audio_devices = &mut state.borrow_mut().default_audio_nodes; + let key = key.unwrap(); + let value = value.unwrap(); + match key { + DEFAULT_AUDIO_SINK_PROPERTY_KEY => { + let value: serde_json::Value = serde_json::from_str(value).unwrap(); + default_audio_devices.sink = value.as_object() + .unwrap() + .get("name") + .unwrap() + .as_str() + .unwrap() + .to_string(); + property_count.set(property_count.get() + 1); + }, + DEFAULT_AUDIO_SOURCE_PROPERTY_KEY => { + let value: serde_json::Value = serde_json::from_str(value).unwrap(); + default_audio_devices.source = value.as_object() + .unwrap() + .get("name") + .unwrap() + .as_str() + .unwrap() + .to_string(); + property_count.set(property_count.get() + 1); + }, + &_ => {} + }; + if let (GlobalObjectState::Pending, EXPECTED_PROPERTY) = (default_audio_devices.state.clone(), property_count.get()) { + default_audio_devices.state = GlobalObjectState::Initialized; + callback(default_audio_devices) + } + 0 + } + } +} \ No newline at end of file diff --git a/pipewire-client/src/utils.rs b/pipewire-client/src/utils.rs new file mode 100644 index 000000000..6760803c1 --- /dev/null +++ b/pipewire-client/src/utils.rs @@ -0,0 +1,139 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use crate::listeners::{Listener, ListenerTriggerPolicy, Listeners}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Direction { + Input, + Output, +} + +impl From for pipewire::spa::utils::Direction { + fn from(value: Direction) -> Self { + match value { + Direction::Input => pipewire::spa::utils::Direction::Input, + Direction::Output => pipewire::spa::utils::Direction::Output, + } + } +} + +pub(super) fn dict_ref_to_hashmap(dict: &pipewire::spa::utils::dict::DictRef) -> HashMap { + dict + .iter() + .map(move |(k, v)| { + let k = String::from(k).clone(); + let v = String::from(v).clone(); + (k, v) + }) + .collect::>() +} + +pub(super) fn debug_dict_ref(dict: &pipewire::spa::utils::dict::DictRef) { + for (key, value) in dict.iter() { + println!("{} => {}", key ,value); + } + println!("\n"); +} + +pub(super) struct PipewireCoreSync { + core: Rc>, + listeners: Rc>>, +} + +impl PipewireCoreSync { + pub fn new(core: Rc>) -> Self { + Self { + core, + listeners: Rc::new(RefCell::new(Listeners::new())), + } + } + + pub fn register(&self, keep: bool, seq: u32, callback: F) + where + F: Fn() + 'static, + { + let sync_id = self.core.borrow_mut().sync(seq as i32).unwrap(); + let name = format!("sync-{}", sync_id.raw()); + let policy = match keep { + true => ListenerTriggerPolicy::Keep, + false => ListenerTriggerPolicy::Remove, + }; + let listeners = self.listeners.clone(); + let listener_name = name.clone(); + let listener = self + .core + .borrow_mut() + .add_listener_local() + .done(move |_, seq| { + if seq != sync_id { + return; + } + callback(); + listeners.borrow_mut().triggered(&listener_name); + }) + .register(); + self.listeners + .borrow_mut() + .add(name, Listener::new(listener, policy)); + } +} + +impl Clone for PipewireCoreSync { + fn clone(&self) -> Self { + Self { + core: self.core.clone(), + listeners: self.listeners.clone(), + } + } +} + +pub(super) struct Backoff { + attempts: u32, + maximum_attempts: u32, + wait_duration: std::time::Duration, + initial_wait_duration: std::time::Duration, + maximum_wait_duration: std::time::Duration, +} + +impl Backoff { + pub fn new( + maximum_attempts: u32, + initial_wait_duration: std::time::Duration, + maximum_wait_duration: std::time::Duration + ) -> Self { + Self { + attempts: 0, + maximum_attempts, + wait_duration: initial_wait_duration, + initial_wait_duration, + maximum_wait_duration, + } + } + + pub fn reset(&mut self) { + self.attempts = 0; + self.wait_duration = self.initial_wait_duration; + } + + pub fn retry(&mut self, mut operation: F) -> Result + where + F: FnMut() -> Result, + E: std::error::Error + { + self.reset(); + loop { + let error = match operation() { + Ok(value) => return Ok(value), + Err(value) => value + }; + std::thread::sleep(self.wait_duration); + self.wait_duration = self.maximum_wait_duration.min(self.wait_duration * 2); + self.attempts += 1; + if self.attempts < self.maximum_attempts { + continue; + } + return Err(error) + } + } +} \ No newline at end of file diff --git a/pipewire-spa-utils/Cargo.toml b/pipewire-spa-utils/Cargo.toml new file mode 100644 index 000000000..c358d667b --- /dev/null +++ b/pipewire-spa-utils/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "pipewire-spa-utils" +version = "0.1.0" +authors = ["Alexis Bekhdadi "] +description = "PipeWire SPA Utils" +repository = "https://github.com/RustAudio/cpal/" +documentation = "" +license = "Apache-2.0" +keywords = ["pipewire", "spa", "utils"] +build = "build.rs" + +[build-dependencies] +cargo = "0.84" +cargo_metadata = "0.19" +libspa = "0.8" +syn = "2.0" +quote = "1.0" +prettyplease = "0.2" +itertools = "0.14" +indexmap = "2.7" + +[dependencies] +libspa = { version = "0.8" } + +[features] +v0_3_33 = [] +v0_3_40 = ["v0_3_33", "libspa/v0_3_33"] +v0_3_65 = ["v0_3_40", "libspa/v0_3_65"] +v0_3_75 = ["v0_3_65", "libspa/v0_3_75"] + + diff --git a/pipewire-spa-utils/build.rs b/pipewire-spa-utils/build.rs new file mode 100644 index 000000000..4a656618f --- /dev/null +++ b/pipewire-spa-utils/build.rs @@ -0,0 +1,18 @@ +extern crate cargo; +extern crate syn; +extern crate itertools; +extern crate indexmap; +extern crate cargo_metadata; +extern crate quote; + +mod build_modules; + +use build_modules::format; +use build_modules::utils::map_package_info; + + +fn main() { + let package = map_package_info(); + format::generate_enums(&package.src_path, &package.build_path, &package.features); +} + diff --git a/pipewire-spa-utils/build_modules/format/mod.rs b/pipewire-spa-utils/build_modules/format/mod.rs new file mode 100644 index 000000000..99688cad5 --- /dev/null +++ b/pipewire-spa-utils/build_modules/format/mod.rs @@ -0,0 +1,445 @@ +use build_modules::syntax::generators::enumerator::{EnumInfo, EnumVariantInfo}; +use build_modules::syntax::parsers::{StructImplVisitor, StructVisitor}; +use build_modules::syntax::utils::AttributeExt; +use build_modules::utils::read_source_file; +use indexmap::IndexMap; +use itertools::Itertools; +use std::cmp::Ordering; +use std::path::PathBuf; +use syn::__private::quote::__private::ext::RepToTokensExt; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::token::PathSep; +use syn::{Attribute, Expr, Fields, Ident, ImplItemConst, Item, ItemConst, PathSegment, Type}; +use debug; + +#[derive(Debug, Clone)] +struct StructInfo { + ident: Ident, + unnamed_field_ident: Ident, +} + +#[derive(Debug, Clone)] +struct StructImplInfo { + attributes: Vec, + constants: Vec +} + +pub fn generate_enums(src_path: &PathBuf, build_path: &PathBuf, features: &Vec) { + let file_path = PathBuf::from(&"param/format.rs"); + let src = read_source_file(&src_path, &file_path); + + let media_type_enum_info = map_media_type_enum_info(&src.items); + let media_subtype_enum_info = map_media_subtype_enum_info( + &src.items, + move |constant | { + if features.is_empty() { + constant.attrs.contains(&"feature".to_string()) == false + } + else { + features.iter().any(|feature| { + constant.attrs.contains(feature) + }) == false + } + } + ); + + let enum_infos = vec![ + media_type_enum_info, + media_subtype_enum_info + ]; + + generate_enums_code(enum_infos, "format.rs"); + + let file_path = PathBuf::from(&"bindings.rs"); + let src = read_source_file(&build_path, &file_path); + + let audio_sample_format_enum_info = map_audio_sample_format_enum_info(&src.items); + let audio_channel_enum_info = map_audio_channel_enum_info(&src.items); + + let enum_infos = vec![ + audio_sample_format_enum_info, + audio_channel_enum_info + ]; + + generate_enums_code(enum_infos, "audio.rs"); +} + +fn map_media_type_enum_info(items: &Vec) -> EnumInfo { + const IDENT: &str = "MediaType"; + + let filter = move |ident: String| ident == IDENT; + let struct_info = map_struct_info(&items, filter); + let struct_impl_info = map_struct_impl_info(&items, filter); + + EnumInfo { + ident: struct_info.ident.clone(), + attributes: struct_impl_info.attributes, + spa_type: struct_info.unnamed_field_ident.clone(), + representation_type: "u32".to_string(), + variants: struct_impl_info.constants.iter() + .map(move |constant| { + let index = constant.ident.to_string(); + let variant = EnumVariantInfo { + attributes: constant.attrs.clone(), + fields: Fields::Unit, + ident: constant.ident.clone(), + discriminant: match constant.expr.clone() { + Expr::Call(value) => { + let mut arg = value.args[0].clone(); + match arg { + Expr::Path(ref mut value) => { + let mut segments = Punctuated::::new(); + for index in 1..value.path.segments.len() { + segments.push(value.path.segments[index].clone()); + } + value.path.segments = segments; + }, + _ => panic!("Expected a path expression"), + }; + arg + }, + _ => panic!("Expected a call expression"), + }, + }; + (index, variant) + }) + .collect::>(), + } +} + +fn map_media_subtype_enum_info(items: &Vec, filter: F) -> EnumInfo +where + F: FnMut(&&ImplItemConst) -> bool +{ + const IDENT: &str = "MediaSubtype"; + + let ident_filter = move |ident: String| ident == IDENT; + let struct_info = map_struct_info(&items, ident_filter); + let mut struct_impl_info = map_struct_impl_info(&items, ident_filter); + + struct_impl_info.attributes.push_one("allow", "unexpected_cfgs"); // TODO remove this when V0_3_68 will be added to libspa manifest + struct_impl_info.attributes.push_one("allow", "unused_doc_comments"); + + EnumInfo { + ident: struct_info.ident.clone(), + attributes: struct_impl_info.attributes, + spa_type: struct_info.unnamed_field_ident.clone(), + representation_type: "u32".to_string(), + variants: struct_impl_info.constants.iter() + .filter(filter) + .filter(move |constant| { + match &constant.expr { + Expr::Call(_) => true, + _ => false, + } + }) + .map(move |constant| { + let index = constant.ident.to_string(); + let variant = EnumVariantInfo { + attributes: constant.attrs.clone(), + fields: Fields::Unit, + ident: constant.ident.clone(), + discriminant: match constant.expr.clone() { + Expr::Call(value) => { + let mut arg = value.args[0].clone(); + match arg { + Expr::Path(ref mut value) => { + let mut segments = Punctuated::::new(); + for index in 1..value.path.segments.len() { + segments.push(value.path.segments[index].clone()); + } + value.path.segments = segments; + }, + _ => panic!("Expected a path expression"), + }; + arg + }, + _ => panic!("Expected a call expression: {:?}", constant.expr), + }, + }; + (index, variant) + }) + .collect::>(), + } +} + +fn spa_audio_format_idents() -> Vec { + let audio_formats: Vec = vec![ + "S8".to_string(), + "U8".to_string(), + "S16".to_string(), + "U16".to_string(), + "S24".to_string(), + "U24".to_string(), + "S24_32".to_string(), + "U24_32".to_string(), + "S32".to_string(), + "U32".to_string(), + "F32".to_string(), + "F64".to_string(), + ]; + + let ends = vec![ + "LE".to_string(), + "BE".to_string(), + "P".to_string(), + ]; + + audio_formats.iter() + .flat_map(move |format| { + ends.iter() + .map(move |end| { + if format.contains("8") && end != "P" { + format!("SPA_AUDIO_FORMAT_{}", format) + } + else if format.contains("8") && end == "P" { + format!("SPA_AUDIO_FORMAT_{}{}", format, end) + } + else if format.contains("8") == false && end == "P" { + format!("SPA_AUDIO_FORMAT_{}{}", format, end) + } + else { + format!("SPA_AUDIO_FORMAT_{}_{}", format, end) + } + }) + .collect::>() + }) + .collect() +} + +fn map_audio_sample_format_enum_info(items: &Vec) -> EnumInfo { + let spa_audio_format_idents = spa_audio_format_idents(); + let constants = map_constant_info( + &items, + move |constant| spa_audio_format_idents.contains(constant), + move |a, b| { + a.cmp(&b) + } + ); + + let ident = "AudioSampleFormat"; + let spa_type = "spa_audio_format"; + + let mut attributes: Vec = vec![]; + attributes.push_one("allow", "non_camel_case_types"); + + EnumInfo { + ident: Ident::new(ident, ident.span()), + attributes, + spa_type: Ident::new(spa_type, spa_type.span()), + representation_type: "u32".to_string(), + variants: constants.iter() + .map(move |constant| { + let index = constant.ident.to_string(); + let ident = constant.ident.to_string().replace("SPA_AUDIO_FORMAT_", ""); + let ident = Ident::new(&ident, ident.span()); + let discriminant = *constant.expr.clone(); + let variant = EnumVariantInfo { + attributes: constant.attrs.clone(), + fields: Fields::Unit, + ident, + discriminant, + }; + (index, variant) + }) + .collect::>(), + } +} + +fn map_audio_channel_enum_info(items: &Vec) -> EnumInfo { + let constants = map_constant_info( + &items, + move |constant| { + if constant.starts_with("SPA_AUDIO_CHANNEL") == false { + return false; + } + + let constant = constant.replace("SPA_AUDIO_CHANNEL_", ""); + + if constant.starts_with("START") || constant.starts_with("LAST") || constant.starts_with("AUX") { + return false; + } + + return true; + }, + move |a, b| { + a.cmp(&b) + } + ); + + let ident = "AudioChannel"; + let spa_type = "spa_audio_channel"; + + let mut attributes: Vec = vec![]; + attributes.push_one("allow", "unused_doc_comments"); + + EnumInfo { + ident: Ident::new(ident, ident.span()), + attributes, + spa_type: Ident::new(spa_type, spa_type.span()), + representation_type: "u32".to_string(), + variants: constants.iter() + .map(move |constant| { + let index = constant.ident.to_string(); + let ident = constant.ident.to_string().replace("SPA_AUDIO_CHANNEL_", ""); + let ident = Ident::new(&ident, ident.span()); + let discriminant = *constant.expr.clone(); + let variant = EnumVariantInfo { + attributes: constant.attrs.clone(), + fields: Fields::Unit, + ident, + discriminant, + }; + (index, variant) + }) + .collect::>(), + } +} + +fn map_constant_info(items: &Vec, filter: F, sorter: S) -> Vec<&ItemConst> +where + F: Fn(&String) -> bool, + S: Fn(&String, &String) -> Ordering +{ + items.iter() + .filter_map(move |item| { + match item { + Item::Const(value) => { + Some(value) + } + &_ => None + } + }) + .filter(move |constant| filter(&constant.ident.to_string())) + .sorted_by(move |a, b| sorter(&a.ident.to_string(), &b.ident.to_string())) + .collect::>() +} + +fn map_struct_info(items: &Vec, filter: F) -> StructInfo +where + F: Fn(String) -> bool +{ + items.iter() + .filter_map(move |item| { + let item = item.next().unwrap(); + let item = item.next().unwrap(); + match item { + Item::Struct(value) => { + let ident = value.ident.clone(); + if filter(ident.to_string()) == false { + return None; + } + let visitor = StructVisitor::new(value); + Some((visitor, ident)) + } + &_ => None + } + }) + .filter_map(move |(visitor, ident)| { + let fields = visitor.fields(); + if fields.is_empty() == false { + Some(StructInfo { + ident, + unnamed_field_ident: { + let field = fields + .iter() + .map(|field| field.clone()) + .collect::>() + .first() + .cloned() + .unwrap(); + let ident = match field.ty { + Type::Path(value) => { + value.path.segments.iter() + .map(|segment| segment.ident.to_string()) + .join("::") + } + _ => panic!("Unsupported type: {:?}", field.ty), + }; + let ident = ident + .replace("spa_sys::", ""); + Ident::new(&ident, ident.span()) + }, + }) + } + else { + None + } + }) + .collect::>() + .first() + .cloned() + .unwrap() +} + +fn map_struct_impl_info(items: &Vec, filter: F) -> StructImplInfo +where + F: Fn(String) -> bool +{ + items.iter() + .filter_map(move |item| { + let item = item.next().unwrap(); + let item = item.next().unwrap(); + match item { + Item::Impl(value) => { + let visitor = StructImplVisitor::new(value); + let self_ident = visitor.self_type() + .segments + .iter() + .map(|segment| segment.ident.to_string()) + .collect::>() + .join("::"); + if filter(self_ident) == false { + return None; + } + let attributes = visitor.attributes(); + Some((visitor, attributes)) + } + &_ => None + } + }) + .filter_map(move |(visitor, attributes)| { + if attributes.is_empty() { + return None; + } + let constants = visitor.constants() + .iter() + .filter_map(move |constant| { + match constant.ty { + Type::Path(_) => { + Some(constant.clone()) + } + _ => None + } + }) + .collect::>(); + if constants.is_empty() == false { + Some(StructImplInfo { + attributes, + constants: constants.clone(), + }) + } + else { + None + } + }) + .collect::>() + .first() + .cloned() + .unwrap() +} + +fn generate_enums_code(enums: Vec, filename: &str) { + let code = enums.iter() + .map(move |enum_info| enum_info.generate()) + .collect::>() + .join("\n"); + + let out_dir = std::env::var("OUT_DIR") + .expect("OUT_DIR not set"); + + let path = std::path::Path::new(&out_dir).join(filename); + std::fs::write(path, code) + .expect("Unable to write generated file"); +} \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/mod.rs b/pipewire-spa-utils/build_modules/mod.rs new file mode 100644 index 000000000..7f94a002b --- /dev/null +++ b/pipewire-spa-utils/build_modules/mod.rs @@ -0,0 +1,3 @@ +pub mod syntax; +pub mod format; +pub mod utils; \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/syntax/generators/enumerator.rs b/pipewire-spa-utils/build_modules/syntax/generators/enumerator.rs new file mode 100644 index 000000000..dc9cc03af --- /dev/null +++ b/pipewire-spa-utils/build_modules/syntax/generators/enumerator.rs @@ -0,0 +1,145 @@ +use build_modules::syntax::utils::AttributeExt; +use indexmap::IndexMap; +use quote::ToTokens; +use quote::__private::TokenStream; +use syn::__private::quote::quote; +use syn::__private::TokenStream2; +use syn::punctuated::Punctuated; +use syn::token::{Brace, Enum, Eq, Pub}; +use syn::{Attribute, Expr, Fields, Generics, Ident, ItemEnum, Variant, Visibility}; +use debug; + +#[derive(Debug)] +pub struct EnumInfo { + pub ident: Ident, + pub attributes: Vec, + pub spa_type: Ident, + pub representation_type: String, + pub variants: IndexMap +} + +#[derive(Debug)] +pub struct EnumVariantInfo { + pub attributes: Vec, + pub fields: Fields, + pub ident: Ident, + pub discriminant: Expr +} + +impl From<&EnumVariantInfo> for Variant { + fn from(value: &EnumVariantInfo) -> Self { + Variant { + attrs: value.attributes.clone(), + ident: value.ident.clone(), + fields: value.fields.clone(), + discriminant: Some((Eq::default(), value.discriminant.clone())), + } + } +} + +impl EnumInfo { + pub fn generate(&self) -> String { + let mut variants = Punctuated::new(); + self.variants.iter() + .for_each(|(_, variant)| { + variants.push(variant.into()) + }); + let mut attributes = self.attributes.clone(); + attributes.push_one("repr", self.representation_type.as_str()); + attributes.push_one("derive", "Debug"); + attributes.push_one("derive", "Clone"); + attributes.push_one("derive", "Copy"); + attributes.push_one("derive", "Ord"); + attributes.push_one("derive", "PartialOrd"); + attributes.push_one("derive", "Eq"); + attributes.push_one("derive", "PartialEq"); + let item = ItemEnum { + attrs: attributes.clone(), + vis: Visibility::Public(Pub::default()), + enum_token: Enum::default(), + ident: self.ident.clone(), + generics: Generics::default(), + brace_token: Brace::default(), + variants, + }; + let import_quote = quote! { + use libspa::sys::*; + }; + let attributes_quote = self.attributes.to_token_stream(); + let item_quote = quote!(#item); + let item_ident_quote = item.ident.to_token_stream(); + let representation_type_quote = self.representation_type.parse::().unwrap(); + let spa_type_quote = self.spa_type.to_token_stream(); + let from_representation_to_variant_quote = self.variants.iter() + .map(|(_, variant)| { + let ident = variant.ident.to_token_stream(); + let discriminant = variant.discriminant.to_token_stream(); + let attributes = variant.attributes.to_token_stream(); + quote! { + #attributes + #discriminant => Self::#ident, + } + }) + .collect::(); + let from_representation_type_quote = quote! { + #attributes_quote + impl From<#representation_type_quote> for #item_ident_quote { + fn from(value: #representation_type_quote) -> Self { + let value: #spa_type_quote = value; + match value { + #from_representation_to_variant_quote + _ => panic!("Unknown variant") + } + } + } + }; + let to_representation_type_quote = quote! { + #attributes_quote + impl From<&#item_ident_quote> for #representation_type_quote { + fn from(value: &#item_ident_quote) -> Self { + let value: #spa_type_quote = value.into(); + value + } + } + + #attributes_quote + impl From<#item_ident_quote> for #representation_type_quote { + fn from(value: #item_ident_quote) -> Self { + let value: #spa_type_quote = value.into(); + value + } + } + }; + let from_variant_to_string_quote = self.variants.iter() + .map(|(_, variant)| { + let ident = variant.ident.to_token_stream(); + let ident_string = variant.ident.to_string(); + let attributes = variant.attributes.to_token_stream(); + quote! { + #attributes + Self::#ident => #ident_string.to_string(), + } + }) + .collect::(); + let to_string_quote = quote! { + #attributes_quote + impl #item_ident_quote { + fn to_string(&self) -> String { + match self { + #from_variant_to_string_quote + } + } + } + }; + let items = vec![ + import_quote.to_string(), + item_quote.to_string(), + from_representation_type_quote.to_string(), + to_representation_type_quote.to_string(), + to_string_quote.to_string(), + ]; + let items = items.join("\n"); + let file = syn::parse_file(items.as_str()).unwrap(); + prettyplease::unparse(&file) + } +} \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/syntax/generators/mod.rs b/pipewire-spa-utils/build_modules/syntax/generators/mod.rs new file mode 100644 index 000000000..8e0a632e0 --- /dev/null +++ b/pipewire-spa-utils/build_modules/syntax/generators/mod.rs @@ -0,0 +1 @@ +pub mod enumerator; \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/syntax/mod.rs b/pipewire-spa-utils/build_modules/syntax/mod.rs new file mode 100644 index 000000000..06fb5ca66 --- /dev/null +++ b/pipewire-spa-utils/build_modules/syntax/mod.rs @@ -0,0 +1,3 @@ +pub mod generators; +pub mod parsers; +pub mod utils; \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/syntax/parsers/mod.rs b/pipewire-spa-utils/build_modules/syntax/parsers/mod.rs new file mode 100644 index 000000000..74fc5e3b9 --- /dev/null +++ b/pipewire-spa-utils/build_modules/syntax/parsers/mod.rs @@ -0,0 +1,59 @@ +use syn::{Attribute, Fields, ImplItem, ImplItemConst, ItemImpl, ItemStruct, Path, Type}; + +pub struct StructVisitor<'a> { + item: &'a ItemStruct, +} + +impl<'a> StructVisitor<'a> { + pub fn new(item: &'a ItemStruct) -> Self { + Self { + item, + } + } + + pub fn fields(&self) -> Fields { + self.item.fields.clone() + } +} + +pub struct StructImplVisitor<'a> { + item: &'a ItemImpl +} + +impl<'a> StructImplVisitor<'a> { + pub fn new(item: &'a ItemImpl) -> Self { + Self { + item, + } + } + pub fn self_type(&self) -> Path { + match *self.item.self_ty.clone() { + Type::Path(value) => { + value.path.clone() + } + _ => panic!("Path expected") + } + } + + pub fn attributes(&self) -> Vec { + self.item.attrs.iter() + .map(move |attribute| { + attribute.clone() + }) + .collect::>() + } + + pub fn constants(&self) -> Vec { + self.item.items.iter() + .filter_map(move |item| { + match item { + ImplItem::Const(value) => { + Some(value.clone()) + } + &_ => return None + } + }) + .collect::>() + } +} + diff --git a/pipewire-spa-utils/build_modules/syntax/utils.rs b/pipewire-spa-utils/build_modules/syntax/utils.rs new file mode 100644 index 000000000..fd2113a9c --- /dev/null +++ b/pipewire-spa-utils/build_modules/syntax/utils.rs @@ -0,0 +1,67 @@ +use quote::ToTokens; +use quote::__private::TokenStream; +use std::str::FromStr; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::token::{Bracket, Paren, Pound}; +use syn::{AttrStyle, Attribute, Ident, MacroDelimiter, Meta, MetaList, PathArguments, PathSegment}; + +fn add_attribute(attrs: &mut Vec, ident: &str, value: &str) { + attrs.push( + Attribute { + pound_token: Pound::default(), + style: AttrStyle::Outer, + bracket_token: Bracket::default(), + meta: Meta::List(MetaList { + path: syn::Path { + leading_colon: None, + segments: { + let mut segments = Punctuated::default(); + let ident = ident; + segments.push(PathSegment { + ident: Ident::new(ident, ident.span()), + arguments: PathArguments::None, + }); + segments + }, + }, + delimiter: MacroDelimiter::Paren(Paren::default()), + tokens: TokenStream::from_str(value).unwrap(), + }), + } + ); +} + +pub trait AttributeExt { + fn push_one(&mut self, ident: &str, value: &str); + fn to_token_stream(&self) -> TokenStream; + fn contains(&self, ident: &String) -> bool; +} + +impl AttributeExt for Vec { + fn push_one(&mut self, ident: &str, value: &str) { + add_attribute(self, ident, value); + } + + fn to_token_stream(&self) -> TokenStream { + self.iter() + .map(|attr| attr.to_token_stream()) + .collect() + } + + fn contains(&self, ident: &String) -> bool { + self.iter().any(move |attribute| { + match &attribute.meta { + Meta::Path(value) => { + value.segments.iter().any(|segment| segment.ident == ident) + } + Meta::List(value) => { + value.path.segments.iter().any(|segment| segment.ident == ident) + } + Meta::NameValue(value) => { + value.path.segments.iter().any(|segment| segment.ident == ident) + } + } + }) + } +} \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/utils/mod.rs b/pipewire-spa-utils/build_modules/utils/mod.rs new file mode 100644 index 000000000..0736c9a4b --- /dev/null +++ b/pipewire-spa-utils/build_modules/utils/mod.rs @@ -0,0 +1,104 @@ +use cargo_metadata::camino::Utf8PathBuf; +use cargo_metadata::{Message, Node, Package}; +use std::fs::File; +use std::io::{BufReader, Read}; +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +#[macro_export] +macro_rules! debug { + ($($tokens: tt)*) => { + println!("cargo:warning={}", format!($($tokens)*)) + } +} + +pub struct PackageInfo { + pub src_path: PathBuf, + pub build_path: PathBuf, + pub features: Vec, +} + +pub fn map_package_info() -> PackageInfo { + let (package, resolve) = find_dependency( + "./Cargo.toml", + move |package| package.name == "libspa" + ); + let src_path = package.manifest_path.parent().unwrap().as_str(); + let src_path = PathBuf::from(src_path).join("src"); + let build_path = dependency_build_path(&package.manifest_path, &resolve).unwrap(); + PackageInfo { + src_path, + build_path, + features: resolve.features.clone(), + } +} + +fn find_dependency(manifest_path: &str, filter: F) -> (Package, Node) +where + F: Fn(&Package) -> bool +{ + let mut cmd = cargo_metadata::MetadataCommand::new(); + let metadata = cmd + .manifest_path(manifest_path) + .exec().unwrap(); + let package = metadata.packages + .iter() + .find(move |package| filter(package)) + .unwrap() + .clone(); + let package_id = package.id.clone(); + let resolve = metadata.resolve.as_ref().unwrap().nodes + .iter() + .find(move |node| { + node.id == package_id + }) + .unwrap() + .clone(); + (package, resolve) +} + +fn dependency_build_path(manifest_path: &Utf8PathBuf, node: &Node) -> Option { + let dependency = node.deps.iter() + .find(move |dependency| dependency.name == "spa_sys") + .and_then(move |dependency| Some(dependency.pkg.clone())) + .unwrap(); + let (package, _) = find_dependency( + manifest_path.as_ref(), + move |package| package.id == dependency + ); + let mut command = Command::new("cargo") + .current_dir(package.manifest_path.parent().unwrap()) + .args(&["check", "--message-format=json", "--quiet"]) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + command.wait().unwrap(); + let reader = BufReader::new(command.stdout.take().unwrap()); + for message in Message::parse_stream(reader) { + match message.ok().unwrap() { + Message::BuildScriptExecuted(script) => { + if script.package_id.repr.starts_with("path+file://"){ + return Some(script.out_dir.clone().as_std_path().to_path_buf()) + } + }, + _ => () + } + } + + None +} + +pub fn read_source_file(src_path: &PathBuf, file_path: &PathBuf) -> syn::File { + let path = src_path.join(file_path); + let mut file = File::open(path) + .expect("Unable to open file"); + + let mut src = String::new(); + file.read_to_string(&mut src) + .expect("Unable to read file"); + + let syntax = syn::parse_file(&src) + .expect("Unable to parse file"); + syntax +} \ No newline at end of file diff --git a/pipewire-spa-utils/src/audio/mod.rs b/pipewire-spa-utils/src/audio/mod.rs new file mode 100644 index 000000000..af370afae --- /dev/null +++ b/pipewire-spa-utils/src/audio/mod.rs @@ -0,0 +1,57 @@ +use libspa::pod::deserialize::DeserializeError; +use libspa::pod::deserialize::DeserializeSuccess; +use libspa::pod::deserialize::PodDeserialize; +use libspa::pod::deserialize::PodDeserializer; +use libspa::pod::deserialize::VecVisitor; +use libspa::utils::Id; +use std::convert::TryInto; +use std::ops::Deref; +use impl_array_id_deserializer; +use utils::IdOrEnumId; + +pub mod raw; + +include!(concat!(env!("OUT_DIR"), "/audio.rs")); + +#[derive(Debug, Clone)] +pub struct AudioSampleFormatEnum(IdOrEnumId); + +impl Deref for AudioSampleFormatEnum { + type Target = IdOrEnumId; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct AudioChannelPosition(Vec); + +impl Default for AudioChannelPosition { + fn default() -> Self { + AudioChannelPosition(vec![]) + } +} + +impl AudioChannelPosition { + pub fn to_array(&self) -> [u32; N] { + let mut channels = self.0 + .iter() + .map(move |channel| *channel as u32) + .collect::>(); + if channels.len() < N { + channels.resize(N, AudioChannel::UNKNOWN as u32); + } + channels.try_into().unwrap() + } +} + +impl Deref for AudioChannelPosition { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +impl_array_id_deserializer!(AudioChannelPosition, AudioChannel); \ No newline at end of file diff --git a/pipewire-spa-utils/src/audio/raw.rs b/pipewire-spa-utils/src/audio/raw.rs new file mode 100644 index 000000000..b95e864c2 --- /dev/null +++ b/pipewire-spa-utils/src/audio/raw.rs @@ -0,0 +1,63 @@ +use audio::{AudioChannelPosition, AudioSampleFormat}; +use format::{MediaSubtype, MediaType}; +use libspa::pod::deserialize::{DeserializeError, DeserializeSuccess, ObjectPodDeserializer, PodDeserialize, PodDeserializer, Visitor}; +use utils::{IdOrEnumId, IntOrChoiceInt, IntOrRangeInt32}; + +#[derive(Debug, Clone)] +pub struct AudioInfoRaw { + pub media_type: MediaType, + pub media_subtype: MediaSubtype, + pub sample_format: IdOrEnumId, + pub sample_rate: IntOrRangeInt32, + pub channels: IntOrChoiceInt, + pub position: AudioChannelPosition +} + +impl<'de> PodDeserialize<'de> for AudioInfoRaw { + fn deserialize( + deserializer: PodDeserializer<'de>, + ) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> + where + Self: Sized, + { + struct EnumFormatVisitor; + + impl<'de> Visitor<'de> for EnumFormatVisitor { + type Value = AudioInfoRaw; + type ArrayElem = std::convert::Infallible; + + fn visit_object( + &self, + object_deserializer: &mut ObjectPodDeserializer<'de>, + ) -> Result> { + let media_type = object_deserializer + .deserialize_property_key(libspa::sys::SPA_FORMAT_mediaType)? + .0; + let media_subtype = object_deserializer + .deserialize_property_key(libspa::sys::SPA_FORMAT_mediaSubtype)? + .0; + let sample_format = object_deserializer + .deserialize_property_key(libspa::sys::SPA_FORMAT_AUDIO_format)? + .0; + let sample_rate = object_deserializer + .deserialize_property_key(libspa::sys::SPA_FORMAT_AUDIO_rate)? + .0; + let channels = object_deserializer + .deserialize_property_key(libspa::sys::SPA_FORMAT_AUDIO_channels)? + .0; + let position = object_deserializer + .deserialize_property_key(libspa::sys::SPA_FORMAT_AUDIO_position)? + .0; + Ok(AudioInfoRaw { + media_type, + media_subtype, + sample_format, + sample_rate, + channels, + position, + }) + } + } + deserializer.deserialize_object(EnumFormatVisitor) + } +} \ No newline at end of file diff --git a/pipewire-spa-utils/src/format/mod.rs b/pipewire-spa-utils/src/format/mod.rs new file mode 100644 index 000000000..a561546db --- /dev/null +++ b/pipewire-spa-utils/src/format/mod.rs @@ -0,0 +1,12 @@ +use libspa::utils::Id; +use libspa::pod::deserialize::DeserializeError; +use libspa::pod::deserialize::DeserializeSuccess; +use libspa::pod::deserialize::PodDeserialize; +use libspa::pod::deserialize::PodDeserializer; +use libspa::pod::deserialize::IdVisitor; +use ::{impl_id_deserializer}; + +include!(concat!(env!("OUT_DIR"), "/format.rs")); + +impl_id_deserializer!(MediaType); +impl_id_deserializer!(MediaSubtype); \ No newline at end of file diff --git a/pipewire-spa-utils/src/lib.rs b/pipewire-spa-utils/src/lib.rs new file mode 100644 index 000000000..06345d50e --- /dev/null +++ b/pipewire-spa-utils/src/lib.rs @@ -0,0 +1,5 @@ +extern crate libspa; +mod macros; +pub mod format; +pub mod audio; +pub mod utils; \ No newline at end of file diff --git a/pipewire-spa-utils/src/macros/mod.rs b/pipewire-spa-utils/src/macros/mod.rs new file mode 100644 index 000000000..23bc4be95 --- /dev/null +++ b/pipewire-spa-utils/src/macros/mod.rs @@ -0,0 +1,115 @@ +#[macro_export] +macro_rules! impl_id_deserializer { + ( + $name:ident + ) => { + impl From for $name { + fn from(value: Id) -> Self { + value.0.into() + } + } + + impl<'de> PodDeserialize<'de> for $name { + fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> + where + Self: Sized + { + let res = deserializer.deserialize_id(IdVisitor)?; + Ok((res.0.into(), res.1)) + } + } + } +} + +#[macro_export] +macro_rules! impl_choice_id_deserializer { + ( + $name:ident + ) => { + impl From> for $name { + fn from(value: Choice) -> Self { + value.1.into() + } + } + + impl<'de> PodDeserialize<'de> for $name { + fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> + where + Self: Sized + { + let res = deserializer.deserialize_choice(ChoiceIdVisitor)?; + Ok((res.0.into(), res.1)) + } + } + } +} + +#[macro_export] +macro_rules! impl_choice_int_deserializer { + ( + $name:ident + ) => { + impl From> for $name { + fn from(value: Choice) -> Self { + value.1.into() + } + } + + impl<'de> PodDeserialize<'de> for $name { + fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> + where + Self: Sized + { + let res = deserializer.deserialize_choice(ChoiceIntVisitor)?; + Ok((res.0.into(), res.1)) + } + } + } +} + +#[macro_export] +macro_rules! impl_array_id_deserializer { + ( + $array_name:ident, + $item_name:ident + ) => { + impl From<&Id> for $item_name { + fn from(value: &Id) -> Self { + value.0.into() + } + } + + impl From> for $array_name { + fn from(value: Vec) -> Self { + $array_name(value.iter().map(|id| id.into()).collect()) + } + } + + impl<'de> PodDeserialize<'de> for $array_name { + fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> + where + Self: Sized + { + let res = deserializer.deserialize_array(VecVisitor::default())?; + Ok((res.0.into(), res.1)) + } + } + } +} + +#[macro_export] +macro_rules! impl_any_deserializer { + ( + $name:ident + ) => { + impl<'de> PodDeserialize<'de> for $name { + fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> + where + Self: Sized + { + let res = deserializer.deserialize_any()?; + Ok((res.0.into(), res.1)) + } + } + } +} \ No newline at end of file diff --git a/pipewire-spa-utils/src/utils/mod.rs b/pipewire-spa-utils/src/utils/mod.rs new file mode 100644 index 000000000..18b44bedd --- /dev/null +++ b/pipewire-spa-utils/src/utils/mod.rs @@ -0,0 +1,255 @@ +use libspa::pod::deserialize::DeserializeError; +use libspa::pod::deserialize::DeserializeSuccess; +use libspa::pod::deserialize::PodDeserialize; +use libspa::pod::deserialize::PodDeserializer; +use libspa::pod::deserialize::{ChoiceIdVisitor, ChoiceIntVisitor}; +use libspa::pod::{ChoiceValue, Value}; +use libspa::utils::{Choice, ChoiceEnum, Id}; +use std::ops::Deref; +use ::impl_any_deserializer; +use impl_choice_int_deserializer; + +#[derive(Debug, Clone)] +pub struct IntOrChoiceInt(u32); + +impl From for IntOrChoiceInt { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for IntOrChoiceInt { + fn from(value: ChoiceValue) -> Self { + match value { + ChoiceValue::Int(value) => value.into(), + _ => panic!("Expected Int or ChoiceValue::Int"), + } + } +} + +impl From> for IntOrChoiceInt { + fn from(value: Choice) -> Self { + match value.1 { + ChoiceEnum::None(value) => IntOrChoiceInt(value as u32), + _ => panic!("Expected ChoiceEnum::None"), + } + } +} + +impl From for IntOrChoiceInt { + fn from(value: Value) -> Self { + match value { + Value::Int(value) => Self(value as u32), + Value::Choice(value) => value.into(), + _ => panic!("Expected Int or Choice") + } + } +} + +impl Deref for IntOrChoiceInt { + type Target = u32; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl_any_deserializer!(IntOrChoiceInt); + +#[derive(Debug, Clone)] +pub struct RangeInt32 { + pub value: u32, + pub minimum: u32, + pub maximum: u32, +} + +impl RangeInt32 { + fn new(value: u32, minimum: u32, maximum: u32) -> Self { + Self { + value, + minimum, + maximum, + } + } +} + +impl From> for RangeInt32 { + fn from(value: ChoiceEnum) -> Self { + match value { + ChoiceEnum::Range { + default, min, max + } => RangeInt32::new( + default as u32, min as u32, max as u32, + ), + _ => panic!("Expected ChoiceEnum::Range") + } + } +} + +impl_choice_int_deserializer!(RangeInt32); + +#[derive(Debug, Clone)] +pub struct IntOrRangeInt32(RangeInt32); + +impl From for IntOrRangeInt32 { + fn from(value: u32) -> Self { + Self(RangeInt32::new(value, value, value)) + } +} + +impl From for IntOrRangeInt32 { + fn from(value: i32) -> Self { + Self(RangeInt32::new(value as u32, value as u32, value as u32)) + } +} + +impl From for IntOrRangeInt32 { + fn from(value: ChoiceValue) -> Self { + match value { + ChoiceValue::Int(value) => value.into(), + _ => panic!("Expected ChoiceValue::Int") + } + } +} + +impl From> for IntOrRangeInt32 { + + fn from(value: Choice) -> Self { + match value.1 { + ChoiceEnum::None(value) => { + Self(RangeInt32::new(value as u32, value as u32, value as u32)) + } + ChoiceEnum::Range { default, min, max } => { + Self(RangeInt32::new(default as u32, min as u32, max as u32)) + } + _ => panic!("Expected Choice::None or Choice::Range") + } + } +} + +impl From for IntOrRangeInt32 { + + fn from(value: Value) -> Self { + match value { + Value::Int(value) => Self::from(value), + Value::Choice(value) => value.into(), + _ => panic!("Expected Int or Choice") + } + } +} + +impl Deref for IntOrRangeInt32 { + type Target = RangeInt32; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl_any_deserializer!(IntOrRangeInt32); + +#[derive(Debug, Clone)] +pub struct EnumId { + pub default: T, + pub alternatives: Vec, +} + +impl EnumId { + fn new(default: T, mut alternatives: Vec) -> Self { + alternatives.sort_by(move |a, b| { + a.cmp(b) + }); + Self { + default, + alternatives, + } + } +} + +impl + Ord> From> for EnumId { + fn from(value: ChoiceEnum) -> Self { + match value { + ChoiceEnum::Enum { + default, alternatives + } => EnumId::new( + default.0.into(), + alternatives.into_iter() + .map(move |id| id.0.into()) + .collect(), + ), + _ => panic!("Expected ChoiceEnum::Enum") + } + } +} + +impl + Ord> From> for EnumId { + fn from(value: Choice) -> Self { + value.1.into() + } +} + +impl <'de, T: From + Ord> PodDeserialize<'de> for EnumId { + fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> + where + Self: Sized + { + let res = deserializer.deserialize_choice(ChoiceIdVisitor)?; + Ok((res.0.into(), res.1)) + } +} + +#[derive(Debug, Clone)] +pub struct IdOrEnumId(EnumId); + +impl + Ord> From for IdOrEnumId { + fn from(value: ChoiceValue) -> Self { + match value { + ChoiceValue::Id(value) => value.into(), + _ => panic!("Expected ChoiceValue::Id") + } + } +} + +impl + Ord> From> for IdOrEnumId { + fn from(value: Choice) -> Self { + match value.1 { + ChoiceEnum::Enum { default, alternatives } => { + Self(EnumId::new( + default.0.into(), + alternatives.into_iter() + .map(move |id| id.0.into()) + .collect::>() + )) + } + _ => panic!("Expected Choice::Enum") + } + } +} + +impl + Ord> From for IdOrEnumId { + fn from(value: Value) -> Self { + match value { + Value::Id(value) => Self(EnumId::new(value.0.into(), vec![value.0.into()])), + Value::Choice(value) => value.into(), + _ => panic!("Expected Id or Choice") + } + } +} + +impl Deref for IdOrEnumId { + type Target = EnumId; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl <'de, T: From + Ord> PodDeserialize<'de> for IdOrEnumId { + fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> + where + Self: Sized + { + let res = deserializer.deserialize_any()?; + Ok((res.0.into(), res.1)) + } +} \ No newline at end of file diff --git a/src/host/mod.rs b/src/host/mod.rs index 8de06cbe0..94e25b94d 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -21,6 +21,16 @@ pub(crate) mod emscripten; feature = "jack" ))] pub(crate) mod jack; +#[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "pipewire" +))] +pub(crate)mod pipewire; pub(crate) mod null; #[cfg(target_os = "android")] pub(crate) mod oboe; diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs new file mode 100644 index 000000000..8908b7c78 --- /dev/null +++ b/src/host/pipewire/device.rs @@ -0,0 +1,208 @@ +use crate::host::pipewire::utils::{AudioBuffer, FromStreamConfigWithSampleFormat}; +use crate::host::pipewire::Stream; +use crate::traits::DeviceTrait; +use crate::{BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, OutputStreamTimestamp, SampleFormat, SampleRate, StreamConfig, StreamError, StreamInstant, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError}; +use std::rc::Rc; +use std::time::Duration; +use pipewire_client::{AudioStreamInfo, Direction, PipewireClient, NodeInfo}; +use pipewire_client::spa_utils::audio::raw::AudioInfoRaw; + +pub type SupportedInputConfigs = std::vec::IntoIter; +pub type SupportedOutputConfigs = std::vec::IntoIter; + +#[derive(Debug, Clone)] +pub struct Device { + pub(super) id: u32, + pub(crate) name: String, + pub(crate) description: String, + pub(crate) nickname: String, + pub(crate) direction: Direction, + pub(super) is_default: bool, + pub(crate) format: AudioInfoRaw, + pub(super) client: Rc, +} + +impl Device { + pub(super) fn from( + info: &NodeInfo, + client: Rc, + ) -> Result { + Ok(Self { + id: info.id.clone(), + name: info.name.clone(), + description: info.description.clone(), + nickname: info.nickname.clone(), + direction: info.direction.clone(), + is_default: info.is_default.clone(), + format: info.format.clone(), + client, + }) + } + + pub fn default_config(&self) -> Result { + let settings = match self.client.settings() { + Ok(value) => value, + Err(value) => return Err(DefaultStreamConfigError::BackendSpecific { + err: BackendSpecificError { + description: value.description, + } + }), + }; + Ok(SupportedStreamConfig { + channels: *self.format.channels as u16, + sample_rate: SampleRate(self.format.sample_rate.value), + buffer_size: SupportedBufferSize::Range { + min: settings.min_buffer_size, + max: settings.max_buffer_size, + }, + sample_format: self.format.sample_format.default.try_into()?, + }) + } + + pub fn supported_configs(&self) -> Vec { + let f = match self.default_config() { + Err(_) => return vec![], + Ok(f) => f, + }; + let mut supported_configs = vec![]; + for &sample_format in self.format.sample_format.alternatives.iter() { + supported_configs.push(SupportedStreamConfigRange { + channels: f.channels, + min_sample_rate: SampleRate(self.format.sample_rate.minimum), + max_sample_rate: SampleRate(self.format.sample_rate.maximum), + buffer_size: f.buffer_size.clone(), + sample_format: sample_format.try_into().unwrap(), + }); + } + supported_configs + } + + fn build_stream_raw ( + &self, + direction: Direction, + config: &StreamConfig, + sample_format: SampleFormat, + mut data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result where + D: FnMut(&mut Data) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let format: AudioStreamInfo = FromStreamConfigWithSampleFormat::from((config, sample_format)); + let channels = config.channels; + let stream_name = self.client.create_stream( + self.id, + direction, + format, + move |buffer| { + let mut buffer = AudioBuffer::from( + buffer, + sample_format, + channels + ); + let data = buffer.data(); + if data.is_none() { + return; + } + let mut data = data.unwrap(); + data_callback(&mut data) + } + ).unwrap(); + Ok(Stream::new(stream_name, self.client.clone())) + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn name(&self) -> Result { + Ok(self.nickname.clone()) + } + + fn supported_input_configs( + &self, + ) -> Result { + Ok(self.supported_configs().into_iter()) + } + + fn supported_output_configs( + &self, + ) -> Result { + Ok(self.supported_configs().into_iter()) + } + + fn default_input_config(&self) -> Result { + self.default_config() + } + + fn default_output_config(&self) -> Result { + self.default_config() + } + + fn build_input_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + mut data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + self.build_stream_raw( + Direction::Input, + config, + sample_format, + move |data| { + data_callback( + data, + &InputCallbackInfo { + timestamp: InputStreamTimestamp { + callback: StreamInstant::from_nanos(0), + capture: StreamInstant::from_nanos(0), + }, + } + ) + }, + error_callback, + timeout + ) + } + + fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + mut data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + self.build_stream_raw( + Direction::Output, + config, + sample_format, + move |data| { + data_callback( + data, + &OutputCallbackInfo { + timestamp: OutputStreamTimestamp { + callback: StreamInstant::from_nanos(0), + playback: StreamInstant::from_nanos(0), + }, + } + ) + }, + error_callback, + timeout + ) + } +} diff --git a/src/host/pipewire/host.rs b/src/host/pipewire/host.rs new file mode 100644 index 000000000..f0aa68eda --- /dev/null +++ b/src/host/pipewire/host.rs @@ -0,0 +1,79 @@ +use crate::traits::HostTrait; +use crate::{BackendSpecificError, DevicesError, HostUnavailable, SupportedStreamConfigRange}; +use std::rc::Rc; +use pipewire_client::{Direction, PipewireClient}; +use crate::host::pipewire::Device; + +pub type SupportedInputConfigs = std::vec::IntoIter; +pub type SupportedOutputConfigs = std::vec::IntoIter; +pub type Devices = std::vec::IntoIter; + +#[derive(Debug)] +pub struct Host { + client: Rc, +} + +impl Host { + pub fn new() -> Result { + let client = PipewireClient::new() + .map_err(move |error| { + eprintln!("{}", error.description); + HostUnavailable + })?; + let client = Rc::new(client); + let host = Host { client }; + Ok(host) + } + + fn default_device(&self, direction: Direction) -> Option { + self.devices() + .unwrap() + .filter(move |device| device.direction == direction && device.is_default) + .collect::>() + .first() + .cloned() + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + true + } + + fn devices(&self) -> Result { + let input_devices = match self.client.enumerate_nodes(Direction::Input) { + Ok(values) => values.into_iter(), + Err(value) => return Err(DevicesError::BackendSpecific { + err: BackendSpecificError { + description: value.description, + }, + }), + }; + let output_devices = match self.client.enumerate_nodes(Direction::Output) { + Ok(values) => values.into_iter(), + Err(value) => return Err(DevicesError::BackendSpecific { + err: BackendSpecificError { + description: value.description, + }, + }), + }; + let devices = input_devices.chain(output_devices) + .map(move |device| { + Device::from(&device, self.client.clone()).unwrap() + }) + .collect::>() + .into_iter(); + Ok(devices) + } + + fn default_input_device(&self) -> Option { + self.default_device(Direction::Input) + } + + fn default_output_device(&self) -> Option { + self.default_device(Direction::Output) + } +} \ No newline at end of file diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs new file mode 100644 index 000000000..debd81a75 --- /dev/null +++ b/src/host/pipewire/mod.rs @@ -0,0 +1,11 @@ +mod host; +pub use self::host::Host; +pub use self::host::Devices; +pub use self::host::SupportedInputConfigs; +pub use self::host::SupportedOutputConfigs; +mod device; + +pub use self::device::Device; +mod stream; +pub use self::stream::Stream; +mod utils; diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs new file mode 100644 index 000000000..d8a2f5217 --- /dev/null +++ b/src/host/pipewire/stream.rs @@ -0,0 +1,36 @@ +use std::rc::Rc; +use pipewire_client::PipewireClient; +use crate::{PauseStreamError, PlayStreamError}; +use crate::traits::StreamTrait; + +pub struct Stream { + name: String, + client: Rc, +} + +impl Stream { + pub(super) fn new(name: String, client: Rc) -> Self { + Self { + name, + client, + } + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + self.client.connect_stream(self.name.clone()).unwrap(); + Ok(()) + } + + fn pause(&self) -> Result<(), PauseStreamError> { + self.client.disconnect_stream(self.name.clone()).unwrap(); + Ok(()) + } +} + +impl Drop for Stream { + fn drop(&mut self) { + self.client.delete_stream(self.name.clone()).unwrap() + } +} \ No newline at end of file diff --git a/src/host/pipewire/utils.rs b/src/host/pipewire/utils.rs new file mode 100644 index 000000000..39aa5d91f --- /dev/null +++ b/src/host/pipewire/utils.rs @@ -0,0 +1,126 @@ +use pipewire_client::spa_utils::audio::{AudioChannelPosition, AudioSampleFormat, AudioSampleFormatEnum}; +use pipewire_client::spa_utils::format::{MediaType, MediaSubtype}; +use pipewire_client::{pipewire, AudioStreamInfo}; +use crate::{BackendSpecificError, ChannelCount, Data, SampleFormat, StreamConfig}; + +impl TryFrom for AudioSampleFormat { + type Error = BackendSpecificError; + + fn try_from(value: SampleFormat) -> Result { + let value = match value { + SampleFormat::I8 => AudioSampleFormat::S8, + SampleFormat::U8 => AudioSampleFormat::U8, + SampleFormat::I16 => AudioSampleFormat::S16_LE, + SampleFormat::U16 => AudioSampleFormat::U16_LE, + SampleFormat::I32 => AudioSampleFormat::S32_LE, + SampleFormat::U32 => AudioSampleFormat::U32_LE, + SampleFormat::F32 => AudioSampleFormat::F32_LE, + SampleFormat::F64 => AudioSampleFormat::F64_LE, + _ => return Err(BackendSpecificError { + description: "Unsupported sample format".to_string(), + })}; + Ok(value) + } +} + +impl TryFrom for SampleFormat { + type Error = BackendSpecificError; + + fn try_from(value: AudioSampleFormat) -> Result { + let value = match value { + AudioSampleFormat::S8 => SampleFormat::I8, + AudioSampleFormat::U8 => SampleFormat::U8, + AudioSampleFormat::S16_LE => SampleFormat::I16, + AudioSampleFormat::U16_LE => SampleFormat::U16, + AudioSampleFormat::S32_LE => SampleFormat::I32, + AudioSampleFormat::U32_LE => SampleFormat::U32, + AudioSampleFormat::F32_LE => SampleFormat::F32, + AudioSampleFormat::F64_LE => SampleFormat::F64, + _ => return Err(BackendSpecificError { + description: "Unsupported sample format".to_string(), + })}; + Ok(value) + } +} + +impl TryFrom for SampleFormat { + type Error = BackendSpecificError; + + fn try_from(value: AudioSampleFormatEnum) -> Result { + let sample_format = SampleFormat::try_from(value.default); + if sample_format.is_ok() { + return sample_format; + } + let sample_format = value.alternatives.iter() + .map(move |sample_format| { + SampleFormat::try_from(sample_format.clone()) + }) + .filter(move |result| result.is_ok()) + .last(); + sample_format.unwrap() + } +} + +pub trait FromStreamConfigWithSampleFormat { + fn from(value: (&StreamConfig, SampleFormat)) -> Self; +} + +impl FromStreamConfigWithSampleFormat for AudioStreamInfo { + fn from(value: (&StreamConfig, SampleFormat)) -> Self { + Self { + media_type: MediaType::Audio, + media_subtype: MediaSubtype::Raw, + sample_format: value.1.try_into().unwrap(), + sample_rate: value.0.sample_rate.0, + channels: value.0.channels as u32, + position: AudioChannelPosition::default(), + } + } +} + +pub(super) struct AudioBuffer<'a> { + buffer: pipewire::buffer::Buffer<'a>, + sample_format: SampleFormat, + channels: ChannelCount, +} + +impl <'a> AudioBuffer<'a> { + pub fn from( + buffer: pipewire::buffer::Buffer<'a>, + sample_format: SampleFormat, + channels: ChannelCount, + ) -> Self { + Self { + buffer, + sample_format, + channels + } + } + + pub fn data(&mut self) -> Option { + let datas = self.buffer.datas_mut(); + let data = &mut datas[0]; + + let stride = self.sample_format.sample_size() * self.channels as usize; + + let data_info = if let Some(data) = data.data() { + let len = data.len(); + let data = unsafe { + Some(Data::from_parts( + data.as_mut_ptr() as *mut (), + data.len() / self.sample_format.sample_size(), + self.sample_format, + )) + }; + (data, len) + } + else { + return None + }; + let chunk = data.chunk_mut(); + *chunk.offset_mut() = 0; + *chunk.stride_mut() = stride as i32; + *chunk.size_mut() = data_info.1 as u32; + data_info.0 + } +} \ No newline at end of file diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 65d77ca40..24eb6a342 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -607,8 +607,18 @@ mod platform_impl { SupportedInputConfigs as JackSupportedInputConfigs, SupportedOutputConfigs as JackSupportedOutputConfigs, }; + #[cfg(feature = "pipewire")] + pub use crate::host::pipewire::{ + Device as PipeWireDevice, Devices as PipeWireDevices, Host as PipeWireHost, + Stream as PipeWireStream, SupportedInputConfigs as PipeWireSupportedInputConfigs, + SupportedOutputConfigs as PipeWireSupportedOutputConfigs, + }; - impl_platform_host!(#[cfg(feature = "jack")] Jack jack "JACK", Alsa alsa "ALSA"); + impl_platform_host!( + #[cfg(feature = "pipewire")] PipeWire pipewire "PipeWire", + #[cfg(feature = "jack")] Jack jack "JACK", + Alsa alsa "ALSA" + ); /// The default host for the current compilation target platform. pub fn default_host() -> Host { From f4fce9cd65ffc78a9acc5fcb06b23ce811928156 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Sat, 18 Jan 2025 10:13:39 +0000 Subject: [PATCH 02/17] Refactor client implementation/test + replace mpsc with crossbeam channel --- pipewire-client/Cargo.toml | 3 +- pipewire-client/src/client/api/core.rs | 53 ++++ pipewire-client/src/client/api/core_test.rs | 30 ++ pipewire-client/src/client/api/fixtures.rs | 37 +++ pipewire-client/src/client/api/internal.rs | 66 ++++ pipewire-client/src/client/api/mod.rs | 24 ++ pipewire-client/src/client/api/node.rs | 115 +++++++ pipewire-client/src/client/api/node_test.rs | 58 ++++ pipewire-client/src/client/api/stream.rs | 95 ++++++ pipewire-client/src/client/api/stream_test.rs | 202 ++++++++++++ pipewire-client/src/client/handlers/event.rs | 15 +- .../src/client/handlers/registry.rs | 13 +- .../src/client/handlers/request.rs | 39 +-- pipewire-client/src/client/handlers/thread.rs | 3 +- pipewire-client/src/client/implementation.rs | 288 +++--------------- .../src/client/implementation_test.rs | 150 +-------- pipewire-client/src/client/mod.rs | 3 +- pipewire-client/src/states.rs | 4 + src/host/pipewire/device.rs | 4 +- src/host/pipewire/host.rs | 4 +- src/host/pipewire/stream.rs | 6 +- 21 files changed, 764 insertions(+), 448 deletions(-) create mode 100644 pipewire-client/src/client/api/core.rs create mode 100644 pipewire-client/src/client/api/core_test.rs create mode 100644 pipewire-client/src/client/api/fixtures.rs create mode 100644 pipewire-client/src/client/api/internal.rs create mode 100644 pipewire-client/src/client/api/mod.rs create mode 100644 pipewire-client/src/client/api/node.rs create mode 100644 pipewire-client/src/client/api/node_test.rs create mode 100644 pipewire-client/src/client/api/stream.rs create mode 100644 pipewire-client/src/client/api/stream_test.rs diff --git a/pipewire-client/Cargo.toml b/pipewire-client/Cargo.toml index fd0e7cfe2..26d8b9ed3 100644 --- a/pipewire-client/Cargo.toml +++ b/pipewire-client/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" authors = ["Alexis Bekhdadi "] description = "PipeWire Client" repository = "https://github.com/RustAudio/cpal/" -#documentation = "" +documentation = "" license = "Apache-2.0" keywords = ["pipewire", "client"] @@ -13,6 +13,7 @@ keywords = ["pipewire", "client"] pipewire = { version = "0.8" } pipewire-spa-utils = { version = "0.1", path = "../pipewire-spa-utils"} serde_json = "1.0" +crossbeam-channel = "0.5" [dev-dependencies] rstest = "0.24" diff --git a/pipewire-client/src/client/api/core.rs b/pipewire-client/src/client/api/core.rs new file mode 100644 index 000000000..8c4d9abbe --- /dev/null +++ b/pipewire-client/src/client/api/core.rs @@ -0,0 +1,53 @@ +use crate::client::api::internal::InternalApi; +use crate::error::Error; +use crate::messages::{MessageRequest, MessageResponse}; +use crate::states::{DefaultAudioNodesState, SettingsState}; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; + +pub struct CoreApi { + api: Arc, +} + +impl CoreApi { + pub(crate) fn new(api: Arc) -> Self { + CoreApi { + api, + } + } + + pub(crate) fn check_session_manager_registered(&self) -> Result<(), Error> { + let request = MessageRequest::CheckSessionManagerRegistered; + self.api.send_request_without_response(&request) + } + + pub fn quit(&self) { + let request = MessageRequest::Quit; + self.api.send_request_without_response(&request).unwrap(); + } + + pub fn get_settings(&self) -> Result { + let request = MessageRequest::Settings; + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::Settings(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + + pub fn get_default_audio_nodes(&self) -> Result { + let request = MessageRequest::DefaultAudioNodes; + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::DefaultAudioNodes(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } +} \ No newline at end of file diff --git a/pipewire-client/src/client/api/core_test.rs b/pipewire-client/src/client/api/core_test.rs new file mode 100644 index 000000000..688c7b447 --- /dev/null +++ b/pipewire-client/src/client/api/core_test.rs @@ -0,0 +1,30 @@ +use crate::client::api::fixtures::client; +use crate::PipewireClient; +use rstest::rstest; +use serial_test::serial; + +#[rstest] +#[serial] +fn quit() { + let client = PipewireClient::new().unwrap(); + client.core().quit(); +} + +#[rstest] +#[serial] +pub fn settings(client: &PipewireClient) { + let settings = client.core().get_settings().unwrap(); + assert_eq!(true, settings.sample_rate > u32::default()); + assert_eq!(true, settings.default_buffer_size > u32::default()); + assert_eq!(true, settings.min_buffer_size > u32::default()); + assert_eq!(true, settings.max_buffer_size > u32::default()); + assert_eq!(true, settings.allowed_sample_rates[0] > u32::default()); +} + +#[rstest] +#[serial] +pub fn default_audio_nodes(client: &PipewireClient) { + let default_audio_nodes = client.core().get_default_audio_nodes().unwrap(); + assert_eq!(false, default_audio_nodes.sink.is_empty()); + assert_eq!(false, default_audio_nodes.source.is_empty()); +} \ No newline at end of file diff --git a/pipewire-client/src/client/api/fixtures.rs b/pipewire-client/src/client/api/fixtures.rs new file mode 100644 index 000000000..d4acc4c46 --- /dev/null +++ b/pipewire-client/src/client/api/fixtures.rs @@ -0,0 +1,37 @@ +use std::panic::UnwindSafe; +use crate::{Direction, NodeInfo, PipewireClient}; +use rstest::fixture; + +#[fixture] +#[once] +pub(crate) fn client() -> PipewireClient { + PipewireClient::new().unwrap() +} + +#[fixture] +pub(crate) fn input_nodes(client: &PipewireClient) -> Vec { + client.node().enumerate(Direction::Input).unwrap() +} + +#[fixture] +pub(crate) fn output_nodes(client: &PipewireClient) -> Vec { + client.node().enumerate(Direction::Output).unwrap() +} + +#[fixture] +pub(crate) fn default_input_node(input_nodes: Vec) -> NodeInfo { + input_nodes.iter() + .filter(|node| node.is_default) + .last() + .cloned() + .unwrap() +} + +#[fixture] +pub(crate) fn default_output_node(output_nodes: Vec) -> NodeInfo { + output_nodes.iter() + .filter(|node| node.is_default) + .last() + .cloned() + .unwrap() +} \ No newline at end of file diff --git a/pipewire-client/src/client/api/internal.rs b/pipewire-client/src/client/api/internal.rs new file mode 100644 index 000000000..0fe2bda91 --- /dev/null +++ b/pipewire-client/src/client/api/internal.rs @@ -0,0 +1,66 @@ +use crate::error::Error; +use crate::messages::{MessageRequest, MessageResponse}; +use std::time::Duration; +use crossbeam_channel::{RecvError, RecvTimeoutError}; + +pub(crate) struct InternalApi { + sender: pipewire::channel::Sender, + receiver: crossbeam_channel::Receiver, +} + +impl InternalApi { + pub(crate) fn new( + sender: pipewire::channel::Sender, + receiver: crossbeam_channel::Receiver, + ) -> Self { + InternalApi { + sender, + receiver, + } + } + + pub(crate) fn wait_response(&self) -> Result { + self.receiver.recv() + } + + pub(crate) fn wait_response_with_timeout(&self, timeout: Duration) -> Result { + self.receiver.recv_timeout(timeout) + } + + pub(crate) fn send_request(&self, request: &MessageRequest) -> Result { + let response = self.sender.send(request.clone()); + let response = match response { + Ok(_) => self.receiver.recv(), + Err(_) => return Err(Error { + description: format!("Failed to send request: {:?}", request), + }), + }; + match response { + Ok(value) => { + match value { + MessageResponse::Error(value) => Err(value), + _ => Ok(value), + } + }, + Err(value) => Err(Error { + description: format!( + "Failed to execute request ({:?}): {:?}", + request, value + ), + }), + } + } + + pub(crate) fn send_request_without_response(&self, request: &MessageRequest) -> Result<(), Error> { + let response = self.sender.send(request.clone()); + match response { + Ok(_) => Ok(()), + Err(value) => Err(Error { + description: format!( + "Failed to execute request ({:?}): {:?}", + request, value + ), + }), + } + } +} \ No newline at end of file diff --git a/pipewire-client/src/client/api/mod.rs b/pipewire-client/src/client/api/mod.rs new file mode 100644 index 000000000..c0b1ea2ea --- /dev/null +++ b/pipewire-client/src/client/api/mod.rs @@ -0,0 +1,24 @@ +mod core; +pub(super) use core::CoreApi; +#[cfg(test)] +#[path = "core_test.rs"] +mod core_test; + +mod node; +pub(super) use node::NodeApi; +#[cfg(test)] +#[path = "node_test.rs"] +mod node_test; + +mod stream; +pub(super) use stream::StreamApi; +#[cfg(test)] +#[path = "stream_test.rs"] +mod stream_test; + +mod internal; +pub(super) use internal::InternalApi; + +#[cfg(test)] +#[path = "fixtures.rs"] +mod fixtures; \ No newline at end of file diff --git a/pipewire-client/src/client/api/node.rs b/pipewire-client/src/client/api/node.rs new file mode 100644 index 000000000..e00fc8309 --- /dev/null +++ b/pipewire-client/src/client/api/node.rs @@ -0,0 +1,115 @@ +use crate::client::api::internal::InternalApi; +use crate::error::Error; +use crate::messages::{MessageRequest, MessageResponse}; +use crate::states::{GlobalId, GlobalObjectState}; +use crate::utils::Backoff; +use crate::{Direction, NodeInfo}; +use std::sync::Arc; + +pub struct NodeApi { + api: Arc +} + +impl NodeApi { + pub(crate) fn new(api: Arc) -> Self { + NodeApi { + api, + } + } + + pub(crate) fn get_state( + &self, + id: &GlobalId, + ) -> Result<(), Error> { + let request = MessageRequest::NodeState(id.clone()); + self.api.send_request_without_response(&request) + } + + pub(crate) fn get_states( + &self, + ) -> Result<(), Error> { + let request = MessageRequest::NodeStates; + self.api.send_request_without_response(&request) + } + + pub fn create( + &self, + name: String, + description: String, + nickname: String, + direction: Direction, + channels: u16, + ) -> Result<(), Error> { + let request = MessageRequest::CreateNode { + name, + description, + nickname, + direction, + channels, + }; + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::CreateNode { + id + }) => { + #[cfg(debug_assertions)] + let timeout_duration = std::time::Duration::from_secs(u64::MAX); + #[cfg(not(debug_assertions))] + let timeout_duration = std::time::Duration::from_millis(500); + self.get_state(&id)?; + let operation = move || { + let response = self.api.wait_response_with_timeout(timeout_duration); + return match response { + Ok(value) => match value { + MessageResponse::NodeState(state) => { + match state == GlobalObjectState::Initialized { + true => { + Ok(()) + }, + false => { + self.get_state(&id)?; + Err(Error { + description: "Created node should be initialized at this point".to_string(), + }) + } + } + } + _ => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + }, + Err(value) => Err(Error { + description: format!("Failed during post initialization: {:?}", value), + }) + }; + }; + let mut backoff = Backoff::new( + 10, + std::time::Duration::from_millis(10), + std::time::Duration::from_millis(100), + ); + backoff.retry(operation) + }, + Ok(MessageResponse::Error(value)) => Err(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + + pub fn enumerate( + &self, + direction: Direction, + ) -> Result, Error> { + let request = MessageRequest::EnumerateNodes(direction); + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::EnumerateNodes(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } +} \ No newline at end of file diff --git a/pipewire-client/src/client/api/node_test.rs b/pipewire-client/src/client/api/node_test.rs new file mode 100644 index 000000000..32dfef35c --- /dev/null +++ b/pipewire-client/src/client/api/node_test.rs @@ -0,0 +1,58 @@ +use crate::client::api::fixtures::client; +use crate::{Direction, PipewireClient}; +use rstest::rstest; +use serial_test::serial; + +fn internal_enumerate(client: &PipewireClient, direction: Direction) { + let nodes = client.node().enumerate(direction).unwrap(); + assert_eq!(false, nodes.is_empty()); + let default_node = nodes.iter() + .filter(|node| node.is_default) + .last(); + assert_eq!(true, default_node.is_some()); +} + +fn internal_create(client: &PipewireClient, direction: Direction) { + client.node() + .create( + "test".to_string(), + "test".to_string(), + "test".to_string(), + direction, + 2 + ).unwrap(); +} + +#[rstest] +#[case::input(Direction::Input)] +#[case::output(Direction::Output)] +#[serial] +fn enumerate( + client: &PipewireClient, + #[case] direction: Direction +) { + internal_enumerate(&client, direction); +} + +#[rstest] +#[case::input(Direction::Input)] +#[case::output(Direction::Output)] +#[serial] +fn create( + client: &PipewireClient, + #[case] direction: Direction +) { + internal_create(&client, direction); +} + +#[rstest] +#[case::input(Direction::Input)] +#[case::output(Direction::Output)] +#[serial] +fn create_then_enumerate( + client: &PipewireClient, + #[case] direction: Direction +) { + internal_create(&client, direction.clone()); + internal_enumerate(&client, direction.clone()); +} \ No newline at end of file diff --git a/pipewire-client/src/client/api/stream.rs b/pipewire-client/src/client/api/stream.rs new file mode 100644 index 000000000..f3be8d22e --- /dev/null +++ b/pipewire-client/src/client/api/stream.rs @@ -0,0 +1,95 @@ +use crate::client::api::internal::InternalApi; +use crate::error::Error; +use crate::messages::{MessageRequest, MessageResponse, StreamCallback}; +use crate::states::GlobalId; +use crate::{AudioStreamInfo, Direction}; +use std::sync::Arc; + +pub struct StreamApi { + api: Arc, +} + +impl StreamApi { + pub(crate) fn new(api: Arc) -> Self { + StreamApi { + api, + } + } + + pub fn create( + &self, + node_id: u32, + direction: Direction, + format: AudioStreamInfo, + callback: F, + ) -> Result + where + F: FnMut(pipewire::buffer::Buffer) + Send + 'static + { + let request = MessageRequest::CreateStream { + node_id: GlobalId::from(node_id), + direction, + format, + callback: StreamCallback::from(callback), + }; + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::CreateStream{name}) => Ok(name), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + + pub fn delete( + &self, + name: String + ) -> Result<(), Error> { + let request = MessageRequest::DeleteStream { + name, + }; + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::DeleteStream) => Ok(()), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value) + }), + } + } + + pub fn connect( + &self, + name: String + ) -> Result<(), Error> { + let request = MessageRequest::ConnectStream { + name, + }; + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::ConnectStream) => Ok(()), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + + pub fn disconnect( + &self, + name: String + ) -> Result<(), Error> { + let request = MessageRequest::DisconnectStream { + name, + }; + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::DisconnectStream) => Ok(()), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } +} \ No newline at end of file diff --git a/pipewire-client/src/client/api/stream_test.rs b/pipewire-client/src/client/api/stream_test.rs new file mode 100644 index 000000000..7ea342b5a --- /dev/null +++ b/pipewire-client/src/client/api/stream_test.rs @@ -0,0 +1,202 @@ +use std::panic; +use crate::client::api::fixtures::{client, default_input_node, default_output_node}; +use crate::{Direction, NodeInfo, PipewireClient}; +use rstest::rstest; +use serial_test::serial; + +fn internal_create( + client: &PipewireClient, + node: NodeInfo, + direction: Direction, + callback: F, +) -> String { + client.stream() + .create( + node.id, + direction, + node.format.clone().into(), + callback + ) + .unwrap() +} + +fn internal_delete( + client: &PipewireClient, + stream: &String +) { + client.stream() + .delete(stream.clone()) + .unwrap() +} + +fn internal_create_connected( + client: &PipewireClient, + node: NodeInfo, + direction: Direction, + callback: F, +) -> String { + let stream = client.stream() + .create( + node.id, + direction, + node.format.clone().into(), + callback + ) + .unwrap(); + client.stream().connect(stream.clone()).unwrap(); + stream +} + +struct StreamTest +where + S: Fn() -> String, + T: Fn(&String) -> (), + D: Fn(&String), +{ + setup: S, + test: T, + teardown: D, + stream_name: Option +} + +impl StreamTest +where + S: Fn() -> String, + T: Fn(&String) -> (), + D: Fn(&String), +{ + fn new(setup: S, test: T, teardown: D) -> Self { + Self { + setup, + test, + teardown, + stream_name: None, + } + } + + fn run(&mut self) { + self.stream_name = Some((self.setup)()); + (self.test)(self.stream_name.as_ref().unwrap()); + } +} + +impl Drop for StreamTest +where + S: Fn() -> String, + T: Fn(&String) -> (), + D: Fn(&String), +{ + fn drop(&mut self) { + (self.teardown)(self.stream_name.as_ref().unwrap()) + } +} + +#[rstest] +#[case::input(Direction::Input)] +#[case::output(Direction::Output)] +#[serial] +fn create( + client: &PipewireClient, + default_input_node: NodeInfo, + default_output_node: NodeInfo, + #[case] direction: Direction +) { + let mut test = StreamTest::new( + || { + internal_create( + &client, + match direction { + Direction::Input => default_input_node.clone(), + Direction::Output => default_output_node.clone() + }, + direction.clone(), + move |_| { + assert!(true); + } + ) + }, + |stream| { + match direction { + Direction::Input => assert_eq!(true, stream.ends_with(".stream_input")), + Direction::Output => assert_eq!(true, stream.ends_with(".stream_output")) + }; + }, + |stream| { + internal_delete(&client, stream); + } + ); + test.run(); +} + +#[rstest] +#[case::input(Direction::Input)] +#[case::output(Direction::Output)] +#[serial] +fn connect( + client: &PipewireClient, + default_input_node: NodeInfo, + default_output_node: NodeInfo, + #[case] direction: Direction +) { + let mut test = StreamTest::new( + || { + internal_create( + &client, + match direction { + Direction::Input => default_input_node.clone(), + Direction::Output => default_output_node.clone() + }, + direction.clone(), + move |mut buffer| { + let data = buffer.datas_mut(); + let data = &mut data[0]; + let data = data.data().unwrap(); + assert_eq!(true, data.len() > 0); + } + ) + }, + |stream| { + client.stream().connect(stream.clone()).ok().unwrap(); + // Wait a bit to test if stream callback will panic + std::thread::sleep(std::time::Duration::from_millis(1 * 1000)); + }, + |stream| { + internal_delete(&client, stream); + } + ); + test.run(); +} + +#[rstest] +#[case::input(Direction::Input)] +#[case::output(Direction::Output)] +#[serial] +fn disconnect( + client: &PipewireClient, + default_input_node: NodeInfo, + default_output_node: NodeInfo, + #[case] direction: Direction +) { + let mut test = StreamTest::new( + || { + internal_create_connected( + &client, + match direction { + Direction::Input => default_input_node.clone(), + Direction::Output => default_output_node.clone() + }, + direction.clone(), + move |_| { + assert!(true); + } + ) + }, + |stream| { + client.stream().disconnect(stream.clone()).unwrap(); + }, + |stream| { + internal_delete(&client, stream); + } + ); + test.run(); +} \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/event.rs b/pipewire-client/src/client/handlers/event.rs index b7ee2e95d..f63c0c1e2 100644 --- a/pipewire-client/src/client/handlers/event.rs +++ b/pipewire-client/src/client/handlers/event.rs @@ -1,7 +1,6 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use std::sync::mpsc; use pipewire_spa_utils::audio::raw::AudioInfoRaw; use crate::constants::{METADATA_NAME_PROPERTY_VALUE_DEFAULT, METADATA_NAME_PROPERTY_VALUE_SETTINGS}; use crate::error::Error; @@ -11,7 +10,7 @@ use crate::states::{DefaultAudioNodesState, GlobalId, GlobalState, SettingsState pub(super) fn event_handler( state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, event_sender: pipewire::channel::Sender, ) -> impl Fn(EventMessage) + 'static { @@ -59,7 +58,7 @@ pub(super) fn event_handler( fn handle_set_metadata_listeners( id: GlobalId, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let listener_state = state.clone(); @@ -113,7 +112,7 @@ fn handle_set_metadata_listeners( fn handle_remove_node( id: GlobalId, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let mut state = state.borrow_mut(); @@ -131,7 +130,7 @@ fn handle_remove_node( fn handle_set_node_properties_listener( id: GlobalId, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, event_sender: pipewire::channel::Sender, ) { @@ -188,7 +187,7 @@ fn handle_set_node_properties_listener( fn handle_set_node_format_listener( id: GlobalId, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, event_sender: pipewire::channel::Sender, ) { @@ -227,7 +226,7 @@ fn handle_set_node_properties( id: GlobalId, properties: HashMap, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let mut state = state.borrow_mut(); @@ -246,7 +245,7 @@ fn handle_set_node_format( id: GlobalId, format: AudioInfoRaw, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let mut state = state.borrow_mut(); diff --git a/pipewire-client/src/client/handlers/registry.rs b/pipewire-client/src/client/handlers/registry.rs index d0be01cca..80a716a43 100644 --- a/pipewire-client/src/client/handlers/registry.rs +++ b/pipewire-client/src/client/handlers/registry.rs @@ -1,6 +1,5 @@ use std::cell::RefCell; use std::rc::Rc; -use std::sync::mpsc; use pipewire::registry::GlobalObject; use pipewire::spa; use crate::constants::{APPLICATION_NAME_PROPERTY_KEY, APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION, APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER, MEDIA_CLASS_PROPERTY_KEY, MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK, MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE, METADATA_NAME_PROPERTY_KEY, METADATA_NAME_PROPERTY_VALUE_DEFAULT, METADATA_NAME_PROPERTY_VALUE_SETTINGS}; @@ -11,7 +10,7 @@ use crate::utils::debug_dict_ref; pub(super) fn registry_global_handler( state: Rc>, registry: Rc, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, event_sender: pipewire::channel::Sender, ) -> impl Fn(&GlobalObject<&spa::utils::dict::DictRef>) + 'static { @@ -56,7 +55,7 @@ pub(super) fn registry_global_handler( fn handle_client( global: &GlobalObject<&spa::utils::dict::DictRef>, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { if global.props.is_none() { @@ -90,7 +89,7 @@ fn handle_metadata( global: &GlobalObject<&spa::utils::dict::DictRef>, state: Rc>, registry: Rc, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, event_sender: pipewire::channel::Sender, ) { @@ -129,7 +128,7 @@ fn handle_node( global: &GlobalObject<&spa::utils::dict::DictRef>, state: Rc>, registry: Rc, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, event_sender: pipewire::channel::Sender, ) { @@ -164,7 +163,7 @@ fn handle_port( global: &GlobalObject<&spa::utils::dict::DictRef>, state: Rc>, registry: Rc, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, event_sender: pipewire::channel::Sender, ) { @@ -203,7 +202,7 @@ fn handle_link( global: &GlobalObject<&spa::utils::dict::DictRef>, state: Rc>, registry: Rc, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, event_sender: pipewire::channel::Sender, ) { diff --git a/pipewire-client/src/client/handlers/request.rs b/pipewire-client/src/client/handlers/request.rs index dcfa485f7..9b6364cff 100644 --- a/pipewire-client/src/client/handlers/request.rs +++ b/pipewire-client/src/client/handlers/request.rs @@ -1,6 +1,5 @@ use std::cell::RefCell; use std::rc::Rc; -use std::sync::mpsc; use pipewire::proxy::ProxyT; use crate::constants::*; use crate::{AudioStreamInfo, Direction, NodeInfo}; @@ -15,7 +14,7 @@ pub(super) fn request_handler( core_sync: Rc, main_loop: pipewire::main_loop::MainLoop, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) -> impl Fn(MessageRequest) + 'static { move |message_request: MessageRequest| match message_request { @@ -120,7 +119,7 @@ pub(super) fn request_handler( fn handle_settings( state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let state = state.borrow(); @@ -129,7 +128,7 @@ fn handle_settings( } fn handle_default_audio_nodes( state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let state = state.borrow(); @@ -145,7 +144,7 @@ fn handle_create_node( core: Rc, core_sync: Rc, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let default_audio_position = format!( @@ -250,7 +249,7 @@ fn handle_create_node( fn handle_enumerate_node( direction: Direction, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let state = state.borrow(); @@ -314,7 +313,7 @@ fn handle_create_stream( callback: StreamCallback, core: Rc, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let mut state = state.borrow_mut(); @@ -385,7 +384,7 @@ fn handle_create_stream( fn handle_delete_stream( name: String, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let mut state = state.borrow_mut(); @@ -398,12 +397,14 @@ fn handle_delete_stream( return; } }; - if let Err(value) = stream.disconnect() { - main_sender - .send(MessageResponse::Error(value)) - .unwrap(); - return; - }; + if stream.is_connected() { + if let Err(value) = stream.disconnect() { + main_sender + .send(MessageResponse::Error(value)) + .unwrap(); + return; + }; + } if let Err(value) = state.remove_stream(&name) { main_sender .send(MessageResponse::Error(value)) @@ -415,7 +416,7 @@ fn handle_delete_stream( fn handle_connect_stream( name: String, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let mut state = state.borrow_mut(); @@ -439,7 +440,7 @@ fn handle_connect_stream( fn handle_disconnect_stream( name: String, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let mut state = state.borrow_mut(); @@ -462,7 +463,7 @@ fn handle_disconnect_stream( } fn handle_check_session_manager_registered( state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { // Checking if session manager is registered because we need "default" metadata @@ -521,7 +522,7 @@ fn handle_check_session_manager_registered( fn handle_node_state( id: GlobalId, state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let state = state.borrow(); let node = match state.get_node(&id) { @@ -538,7 +539,7 @@ fn handle_node_state( } fn handle_node_states( state: Rc>, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, ) { let state = state.borrow_mut(); diff --git a/pipewire-client/src/client/handlers/thread.rs b/pipewire-client/src/client/handlers/thread.rs index f3b1b481a..aa83d40d8 100644 --- a/pipewire-client/src/client/handlers/thread.rs +++ b/pipewire-client/src/client/handlers/thread.rs @@ -9,11 +9,10 @@ use crate::states::GlobalState; use crate::utils::PipewireCoreSync; use std::cell::RefCell; use std::rc::Rc; -use std::sync::mpsc; pub fn pw_thread( client_info: PipewireClientInfo, - main_sender: mpsc::Sender, + main_sender: crossbeam_channel::Sender, pw_receiver: pipewire::channel::Receiver, event_sender: pipewire::channel::Sender, event_receiver: pipewire::channel::Receiver, diff --git a/pipewire-client/src/client/implementation.rs b/pipewire-client/src/client/implementation.rs index 067ae3ae0..23f9e3ab5 100644 --- a/pipewire-client/src/client/implementation.rs +++ b/pipewire-client/src/client/implementation.rs @@ -1,16 +1,16 @@ extern crate pipewire; +use crate::client::api::{CoreApi, InternalApi, NodeApi, StreamApi}; use crate::client::connection_string::{PipewireClientConnectionString, PipewireClientInfo}; use crate::client::handlers::thread; use crate::error::Error; -use crate::info::{AudioStreamInfo, NodeInfo}; -use crate::messages::{EventMessage, MessageRequest, MessageResponse, StreamCallback}; -use crate::states::{DefaultAudioNodesState, GlobalId, GlobalObjectState, SettingsState}; -use crate::utils::{Direction, Backoff}; +use crate::messages::{EventMessage, MessageRequest, MessageResponse}; +use crate::states::GlobalObjectState; +use crate::utils::Backoff; use std::fmt::{Debug, Formatter}; use std::string::ToString; use std::sync::atomic::{AtomicU32, Ordering}; -use std::sync::mpsc; +use std::sync::Arc; use std::thread; use std::thread::JoinHandle; @@ -20,9 +20,11 @@ pub(super) static CLIENT_INDEX: AtomicU32 = AtomicU32::new(0); pub struct PipewireClient { pub(crate) name: String, connection_string: String, - sender: pipewire::channel::Sender, - receiver: mpsc::Receiver, thread_handle: Option>, + internal_api: Arc, + core_api: CoreApi, + node_api: NodeApi, + stream_api: StreamApi, } impl PipewireClient { @@ -37,7 +39,7 @@ impl PipewireClient { connection_string: connection_string.clone(), }; - let (main_sender, main_receiver) = mpsc::channel(); + let (main_sender, main_receiver) = crossbeam_channel::unbounded(); let (pw_sender, pw_receiver) = pipewire::channel::channel(); let (event_sender, event_receiver) = pipewire::channel::channel::(); @@ -49,12 +51,19 @@ impl PipewireClient { event_receiver )); + let internal_api = Arc::new(InternalApi::new(pw_sender, main_receiver)); + let core_api = CoreApi::new(internal_api.clone()); + let node_api = NodeApi::new(internal_api.clone()); + let stream_api = StreamApi::new(internal_api.clone()); + let client = Self { name, connection_string, - sender: pw_sender, - receiver: main_receiver, thread_handle: Some(pw_thread), + internal_api, + core_api, + node_api, + stream_api, }; match client.wait_initialization() { @@ -69,7 +78,7 @@ impl PipewireClient { } fn wait_initialization(&self) -> Result<(), Error> { - let response = self.receiver.recv(); + let response = self.internal_api.wait_response(); let response = match response { Ok(value) => value, Err(value) => { @@ -97,10 +106,10 @@ impl PipewireClient { let timeout_duration = std::time::Duration::from_secs(u64::MAX); #[cfg(not(debug_assertions))] let timeout_duration = std::time::Duration::from_millis(500); - self.check_session_manager_registered()?; - self.node_states()?; + self.core_api.check_session_manager_registered()?; + self.node_api.get_states()?; let operation = move || { - let response = self.receiver.recv_timeout(timeout_duration); + let response = self.internal_api.wait_response_with_timeout(timeout_duration); match response { Ok(value) => match value { MessageResponse::SettingsState(state) => { @@ -131,7 +140,7 @@ impl PipewireClient { nodes_initialized = true; }, false => { - self.node_states()?; + self.node_api.get_states()?; return Err(Error { description: "All nodes should be initialized at this point".to_string(), }) @@ -151,7 +160,7 @@ impl PipewireClient { return Err(Error { description: "Post initialization not yet finalized".to_string(), }) - + } return Ok(()); }; @@ -163,249 +172,20 @@ impl PipewireClient { backoff.retry(operation) } - fn send_request(&self, request: &MessageRequest) -> Result { - let response = self.sender.send(request.clone()); - let response = match response { - Ok(_) => self.receiver.recv(), - Err(_) => return Err(Error { - description: format!("Failed to send request: {:?}", request), - }), - }; - match response { - Ok(value) => { - match value { - MessageResponse::Error(value) => Err(value), - _ => Ok(value), - } - }, - Err(value) => Err(Error { - description: format!( - "Failed to execute request ({:?}): {:?}", - request, value - ), - }), - } - } - - fn send_request_without_response(&self, request: &MessageRequest) -> Result<(), Error> { - let response = self.sender.send(request.clone()); - match response { - Ok(_) => Ok(()), - Err(value) => Err(Error { - description: format!( - "Failed to execute request ({:?}): {:?}", - request, value - ), - }), - } - } - - pub fn quit(&self) { - let request = MessageRequest::Quit; - self.send_request_without_response(&request).unwrap(); - } - - pub fn settings(&self) -> Result { - let request = MessageRequest::Settings; - let response = self.send_request(&request); - match response { - Ok(MessageResponse::Settings(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn default_audio_nodes(&self) -> Result { - let request = MessageRequest::DefaultAudioNodes; - let response = self.send_request(&request); - match response { - Ok(MessageResponse::DefaultAudioNodes(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn create_node( - &self, - name: String, - description: String, - nickname: String, - direction: Direction, - channels: u16, - ) -> Result<(), Error> { - let request = MessageRequest::CreateNode { - name, - description, - nickname, - direction, - channels, - }; - let response = self.send_request(&request); - match response { - Ok(MessageResponse::CreateNode { - id - }) => { - #[cfg(debug_assertions)] - let timeout_duration = std::time::Duration::from_secs(u64::MAX); - #[cfg(not(debug_assertions))] - let timeout_duration = std::time::Duration::from_millis(500); - self.node_state(&id)?; - let operation = move || { - let response = self.receiver.recv_timeout(timeout_duration); - return match response { - Ok(value) => match value { - MessageResponse::NodeState(state) => { - match state == GlobalObjectState::Initialized { - true => { - Ok(()) - }, - false => { - self.node_state(&id)?; - Err(Error { - description: "Created node should be initialized at this point".to_string(), - }) - } - } - } - _ => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - }, - Err(value) => Err(Error { - description: format!("Failed during post initialization: {:?}", value), - }) - }; - }; - let mut backoff = Backoff::new( - 10, - std::time::Duration::from_millis(10), - std::time::Duration::from_millis(100), - ); - backoff.retry(operation) - }, - Ok(MessageResponse::Error(value)) => Err(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn enumerate_nodes( - &self, - direction: Direction, - ) -> Result, Error> { - let request = MessageRequest::EnumerateNodes(direction); - let response = self.send_request(&request); - match response { - Ok(MessageResponse::EnumerateNodes(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn create_stream( - &self, - node_id: u32, - direction: Direction, - format: AudioStreamInfo, - callback: F, - ) -> Result - where - F: FnMut(pipewire::buffer::Buffer) + Send + 'static - { - let request = MessageRequest::CreateStream { - node_id: GlobalId::from(node_id), - direction, - format, - callback: StreamCallback::from(callback), - }; - let response = self.send_request(&request); - match response { - Ok(MessageResponse::CreateStream{name}) => Ok(name), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn delete_stream( - &self, - name: String - ) -> Result<(), Error> { - let request = MessageRequest::DeleteStream { - name, - }; - let response = self.send_request(&request); - match response { - Ok(MessageResponse::DeleteStream) => Ok(()), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value) - }), - } - } - - pub fn connect_stream( - &self, - name: String - ) -> Result<(), Error> { - let request = MessageRequest::ConnectStream { - name, - }; - let response = self.send_request(&request); - match response { - Ok(MessageResponse::ConnectStream) => Ok(()), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn disconnect_stream( - &self, - name: String - ) -> Result<(), Error> { - let request = MessageRequest::DisconnectStream { - name, - }; - let response = self.send_request(&request); - match response { - Ok(MessageResponse::DisconnectStream) => Ok(()), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } + pub(crate) fn internal(&self) -> Arc { + self.internal_api.clone() } - // Internal requests - pub(super) fn check_session_manager_registered(&self) -> Result<(), Error> { - let request = MessageRequest::CheckSessionManagerRegistered; - self.send_request_without_response(&request) + pub fn core(&self) -> &CoreApi { + &self.core_api } - pub(super) fn node_state( - &self, - id: &GlobalId, - ) -> Result<(), Error> { - let request = MessageRequest::NodeState(id.clone()); - self.send_request_without_response(&request) + pub fn node(&self) -> &NodeApi { + &self.node_api } - pub(super) fn node_states( - &self, - ) -> Result<(), Error> { - let request = MessageRequest::NodeStates; - self.send_request_without_response(&request) + pub fn stream(&self) -> &StreamApi { + &self.stream_api } } @@ -417,7 +197,7 @@ impl Debug for PipewireClient { impl Drop for PipewireClient { fn drop(&mut self) { - if self.sender.send(MessageRequest::Quit).is_ok() { + if self.internal_api.send_request_without_response(&MessageRequest::Quit).is_ok() { if let Some(thread_handle) = self.thread_handle.take() { if let Err(err) = thread_handle.join() { panic!("Failed to join PipeWire thread: {:?}", err); diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs index c844b69bf..a02079b3b 100644 --- a/pipewire-client/src/client/implementation_test.rs +++ b/pipewire-client/src/client/implementation_test.rs @@ -2,22 +2,7 @@ use std::sync::atomic::Ordering; use rstest::rstest; use serial_test::serial; use crate::client::implementation::CLIENT_INDEX; -use crate::{Direction, PipewireClient}; - -#[rstest] -#[serial] -pub fn all() { - for _ in 0..100 { - name(); - quit(); - settings(); - default_audio_nodes(); - create_node(); - create_node_then_enumerate_nodes(); - create_stream(); - enumerate_nodes(); - } -} +use crate::{PipewireClient}; #[rstest] #[serial] @@ -26,137 +11,4 @@ pub fn name() { assert_eq!(format!("cpal-client-{}", CLIENT_INDEX.load(Ordering::SeqCst) - 1), client_1.name); let client_2 = PipewireClient::new().unwrap(); assert_eq!(format!("cpal-client-{}", CLIENT_INDEX.load(Ordering::SeqCst) - 1), client_2.name); -} - -#[rstest] -#[serial] -fn quit() { - let client = PipewireClient::new().unwrap(); - client.quit(); -} - -#[rstest] -#[serial] -fn settings() { - let client = PipewireClient::new().unwrap(); - let response = client.settings(); - assert!( - response.is_ok(), - "Should send settings message without errors" - ); - let settings = response.unwrap(); - assert_eq!(true, settings.sample_rate > u32::default()); - assert_eq!(true, settings.default_buffer_size > u32::default()); - assert_eq!(true, settings.min_buffer_size > u32::default()); - assert_eq!(true, settings.max_buffer_size > u32::default()); - assert_eq!(true, settings.allowed_sample_rates[0] > u32::default()); -} - -#[rstest] -#[serial] -fn default_audio_nodes() { - let client = PipewireClient::new().unwrap(); - let response = client.default_audio_nodes(); - assert!( - response.is_ok(), - "Should send default audio nodes message without errors" - ); - let default_audio_nodes = response.unwrap(); - assert_eq!(false, default_audio_nodes.sink.is_empty()); - assert_eq!(false, default_audio_nodes.source.is_empty()); -} - -#[rstest] -#[serial] -fn create_node() { - let client = PipewireClient::new().unwrap(); - let response = client.create_node( - "test".to_string(), - "test".to_string(), - "test".to_string(), - Direction::Output, - 2 - ); - assert!( - response.is_ok(), - "Should send create node message without errors" - ); -} - -#[rstest] -#[serial] -fn create_node_then_enumerate_nodes() { - let client = PipewireClient::new().unwrap(); - let response = client.create_node( - "test".to_string(), - "test".to_string(), - "test".to_string(), - Direction::Output, - 2 - ); - assert!( - response.is_ok(), - "Should send create node message without errors" - ); - let response = client.enumerate_nodes(Direction::Output); - assert!( - response.is_ok(), - "Should send enumerate devices message without errors" - ); - let nodes = response.unwrap(); - assert_eq!(false, nodes.is_empty()); - let default_node = nodes.iter() - .filter(|node| node.is_default) - .last(); - assert_eq!(true, default_node.is_some()); -} - -#[rstest] -#[serial] -fn create_stream() { - let client = PipewireClient::new().unwrap(); - let response = client.enumerate_nodes(Direction::Output).unwrap(); - let default_node = response.iter() - .filter(|node| node.is_default) - .last() - .unwrap(); - let response = client.create_stream( - default_node.id, - Direction::Output, - default_node.format.clone().into(), - move |mut buffer| { - let data = buffer.datas_mut(); - let data = &mut data[0]; - let data = data.data().unwrap(); - assert_eq!(true, data.len() > 0); - } - ); - assert!( - response.is_ok(), - "Should send create stream message without errors" - ); - let stream_name = response.ok().unwrap(); - let response = client.connect_stream(stream_name); - std::thread::sleep(std::time::Duration::from_millis(1 * 1000)); - assert!( - response.is_ok(), - "Should send connect stream message without errors" - ); -} - -#[rstest] -#[serial] -fn enumerate_nodes() { - let client = PipewireClient::new().unwrap(); - let response = client.enumerate_nodes(Direction::Output); - assert!( - response.is_ok(), - "Should send enumerate devices message without errors" - ); - let nodes = response.unwrap(); - assert_eq!(false, nodes.is_empty()); - let default_node = nodes.iter() - .filter(|node| node.is_default) - .last(); - assert_eq!(true, default_node.is_some()); } \ No newline at end of file diff --git a/pipewire-client/src/client/mod.rs b/pipewire-client/src/client/mod.rs index 7b16bb32a..43b6fdac2 100644 --- a/pipewire-client/src/client/mod.rs +++ b/pipewire-client/src/client/mod.rs @@ -2,7 +2,8 @@ mod implementation; pub use implementation::PipewireClient; mod connection_string; mod handlers; +mod api; #[cfg(test)] -#[path = "implementation_test.rs"] +#[path = "./implementation_test.rs"] mod implementation_test; \ No newline at end of file diff --git a/pipewire-client/src/states.rs b/pipewire-client/src/states.rs index 1bbfb5693..c1d848308 100644 --- a/pipewire-client/src/states.rs +++ b/pipewire-client/src/states.rs @@ -506,6 +506,10 @@ impl StreamState { listeners: Rc::new(RefCell::new(Listeners::new())), } } + + pub fn is_connected(&self) -> bool { + self.is_connected + } pub fn connect(&mut self) -> Result<(), Error> { if self.is_connected { diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 8908b7c78..dbbe95ef9 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -40,7 +40,7 @@ impl Device { } pub fn default_config(&self) -> Result { - let settings = match self.client.settings() { + let settings = match self.client.core().get_settings() { Ok(value) => value, Err(value) => return Err(DefaultStreamConfigError::BackendSpecific { err: BackendSpecificError { @@ -91,7 +91,7 @@ impl Device { { let format: AudioStreamInfo = FromStreamConfigWithSampleFormat::from((config, sample_format)); let channels = config.channels; - let stream_name = self.client.create_stream( + let stream_name = self.client.stream().create( self.id, direction, format, diff --git a/src/host/pipewire/host.rs b/src/host/pipewire/host.rs index f0aa68eda..c2816b1c0 100644 --- a/src/host/pipewire/host.rs +++ b/src/host/pipewire/host.rs @@ -44,7 +44,7 @@ impl HostTrait for Host { } fn devices(&self) -> Result { - let input_devices = match self.client.enumerate_nodes(Direction::Input) { + let input_devices = match self.client.node().enumerate(Direction::Input) { Ok(values) => values.into_iter(), Err(value) => return Err(DevicesError::BackendSpecific { err: BackendSpecificError { @@ -52,7 +52,7 @@ impl HostTrait for Host { }, }), }; - let output_devices = match self.client.enumerate_nodes(Direction::Output) { + let output_devices = match self.client.node().enumerate(Direction::Output) { Ok(values) => values.into_iter(), Err(value) => return Err(DevicesError::BackendSpecific { err: BackendSpecificError { diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index d8a2f5217..38643f827 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -19,18 +19,18 @@ impl Stream { impl StreamTrait for Stream { fn play(&self) -> Result<(), PlayStreamError> { - self.client.connect_stream(self.name.clone()).unwrap(); + self.client.stream().connect(self.name.clone()).unwrap(); Ok(()) } fn pause(&self) -> Result<(), PauseStreamError> { - self.client.disconnect_stream(self.name.clone()).unwrap(); + self.client.stream().disconnect(self.name.clone()).unwrap(); Ok(()) } } impl Drop for Stream { fn drop(&mut self) { - self.client.delete_stream(self.name.clone()).unwrap() + self.client.stream().delete(self.name.clone()).unwrap() } } \ No newline at end of file From b45bab80abad9b4b9ffab51ea426eaeda60fc565 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Sat, 18 Jan 2025 10:30:15 +0000 Subject: [PATCH 03/17] Optimize imports --- pipewire-client/Cargo.toml | 3 ++- pipewire-client/src/client/api/core.rs | 2 -- pipewire-client/src/client/api/fixtures.rs | 1 - pipewire-client/src/client/api/internal.rs | 2 +- pipewire-client/src/client/api/stream_test.rs | 3 +-- .../src/client/connection_string.rs | 2 +- pipewire-client/src/client/handlers/event.rs | 8 ++++---- .../src/client/handlers/registry.rs | 8 ++++---- .../src/client/handlers/request.rs | 8 ++++---- .../src/client/implementation_test.rs | 6 +++--- pipewire-client/src/info.rs | 6 +++--- pipewire-client/src/lib.rs | 2 +- pipewire-client/src/messages.rs | 8 ++++---- pipewire-client/src/states.rs | 20 +++++++++---------- pipewire-client/src/utils.rs | 2 +- 15 files changed, 39 insertions(+), 42 deletions(-) diff --git a/pipewire-client/Cargo.toml b/pipewire-client/Cargo.toml index 26d8b9ed3..62b0d41ba 100644 --- a/pipewire-client/Cargo.toml +++ b/pipewire-client/Cargo.toml @@ -17,4 +17,5 @@ crossbeam-channel = "0.5" [dev-dependencies] rstest = "0.24" -serial_test = "3.2" \ No newline at end of file +serial_test = "3.2" +testcontainers = "0.23" \ No newline at end of file diff --git a/pipewire-client/src/client/api/core.rs b/pipewire-client/src/client/api/core.rs index 8c4d9abbe..fff99370f 100644 --- a/pipewire-client/src/client/api/core.rs +++ b/pipewire-client/src/client/api/core.rs @@ -2,8 +2,6 @@ use crate::client::api::internal::InternalApi; use crate::error::Error; use crate::messages::{MessageRequest, MessageResponse}; use crate::states::{DefaultAudioNodesState, SettingsState}; -use std::cell::RefCell; -use std::rc::Rc; use std::sync::Arc; pub struct CoreApi { diff --git a/pipewire-client/src/client/api/fixtures.rs b/pipewire-client/src/client/api/fixtures.rs index d4acc4c46..9179b9ddc 100644 --- a/pipewire-client/src/client/api/fixtures.rs +++ b/pipewire-client/src/client/api/fixtures.rs @@ -1,4 +1,3 @@ -use std::panic::UnwindSafe; use crate::{Direction, NodeInfo, PipewireClient}; use rstest::fixture; diff --git a/pipewire-client/src/client/api/internal.rs b/pipewire-client/src/client/api/internal.rs index 0fe2bda91..f4336eb3b 100644 --- a/pipewire-client/src/client/api/internal.rs +++ b/pipewire-client/src/client/api/internal.rs @@ -1,7 +1,7 @@ use crate::error::Error; use crate::messages::{MessageRequest, MessageResponse}; -use std::time::Duration; use crossbeam_channel::{RecvError, RecvTimeoutError}; +use std::time::Duration; pub(crate) struct InternalApi { sender: pipewire::channel::Sender, diff --git a/pipewire-client/src/client/api/stream_test.rs b/pipewire-client/src/client/api/stream_test.rs index 7ea342b5a..46658f70b 100644 --- a/pipewire-client/src/client/api/stream_test.rs +++ b/pipewire-client/src/client/api/stream_test.rs @@ -1,4 +1,3 @@ -use std::panic; use crate::client::api::fixtures::{client, default_input_node, default_output_node}; use crate::{Direction, NodeInfo, PipewireClient}; use rstest::rstest; @@ -73,7 +72,7 @@ where stream_name: None, } } - + fn run(&mut self) { self.stream_name = Some((self.setup)()); (self.test)(self.stream_name.as_ref().unwrap()); diff --git a/pipewire-client/src/client/connection_string.rs b/pipewire-client/src/client/connection_string.rs index de0d46ada..1b8368be2 100644 --- a/pipewire-client/src/client/connection_string.rs +++ b/pipewire-client/src/client/connection_string.rs @@ -1,5 +1,5 @@ -use std::path::PathBuf; use crate::constants::*; +use std::path::PathBuf; pub(super) struct PipewireClientConnectionString; diff --git a/pipewire-client/src/client/handlers/event.rs b/pipewire-client/src/client/handlers/event.rs index f63c0c1e2..78c2dad85 100644 --- a/pipewire-client/src/client/handlers/event.rs +++ b/pipewire-client/src/client/handlers/event.rs @@ -1,12 +1,12 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; -use pipewire_spa_utils::audio::raw::AudioInfoRaw; use crate::constants::{METADATA_NAME_PROPERTY_VALUE_DEFAULT, METADATA_NAME_PROPERTY_VALUE_SETTINGS}; use crate::error::Error; use crate::listeners::ListenerTriggerPolicy; use crate::messages::{EventMessage, MessageResponse}; use crate::states::{DefaultAudioNodesState, GlobalId, GlobalState, SettingsState}; +use pipewire_spa_utils::audio::raw::AudioInfoRaw; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; pub(super) fn event_handler( state: Rc>, diff --git a/pipewire-client/src/client/handlers/registry.rs b/pipewire-client/src/client/handlers/registry.rs index 80a716a43..84ac3f312 100644 --- a/pipewire-client/src/client/handlers/registry.rs +++ b/pipewire-client/src/client/handlers/registry.rs @@ -1,11 +1,11 @@ -use std::cell::RefCell; -use std::rc::Rc; -use pipewire::registry::GlobalObject; -use pipewire::spa; use crate::constants::{APPLICATION_NAME_PROPERTY_KEY, APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION, APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER, MEDIA_CLASS_PROPERTY_KEY, MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK, MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE, METADATA_NAME_PROPERTY_KEY, METADATA_NAME_PROPERTY_VALUE_DEFAULT, METADATA_NAME_PROPERTY_VALUE_SETTINGS}; use crate::messages::{EventMessage, MessageResponse}; use crate::states::{ClientState, GlobalId, GlobalObjectState, GlobalState, MetadataState, NodeState}; use crate::utils::debug_dict_ref; +use pipewire::registry::GlobalObject; +use pipewire::spa; +use std::cell::RefCell; +use std::rc::Rc; pub(super) fn registry_global_handler( state: Rc>, diff --git a/pipewire-client/src/client/handlers/request.rs b/pipewire-client/src/client/handlers/request.rs index 9b6364cff..be120cccb 100644 --- a/pipewire-client/src/client/handlers/request.rs +++ b/pipewire-client/src/client/handlers/request.rs @@ -1,13 +1,13 @@ -use std::cell::RefCell; -use std::rc::Rc; -use pipewire::proxy::ProxyT; use crate::constants::*; -use crate::{AudioStreamInfo, Direction, NodeInfo}; use crate::error::Error; use crate::listeners::ListenerTriggerPolicy; use crate::messages::{MessageRequest, MessageResponse, StreamCallback}; use crate::states::{GlobalId, GlobalObjectState, GlobalState, OrphanState, StreamState}; use crate::utils::PipewireCoreSync; +use crate::{AudioStreamInfo, Direction, NodeInfo}; +use pipewire::proxy::ProxyT; +use std::cell::RefCell; +use std::rc::Rc; pub(super) fn request_handler( core: Rc, diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs index a02079b3b..5e8a83d52 100644 --- a/pipewire-client/src/client/implementation_test.rs +++ b/pipewire-client/src/client/implementation_test.rs @@ -1,8 +1,8 @@ -use std::sync::atomic::Ordering; +use crate::client::implementation::CLIENT_INDEX; +use crate::PipewireClient; use rstest::rstest; use serial_test::serial; -use crate::client::implementation::CLIENT_INDEX; -use crate::{PipewireClient}; +use std::sync::atomic::Ordering; #[rstest] #[serial] diff --git a/pipewire-client/src/info.rs b/pipewire-client/src/info.rs index 9f6f07ec9..eb48b298a 100644 --- a/pipewire-client/src/info.rs +++ b/pipewire-client/src/info.rs @@ -1,8 +1,8 @@ -use pipewire_spa_utils::audio::{AudioChannelPosition}; -use pipewire_spa_utils::audio::AudioSampleFormat; +use crate::utils::Direction; use pipewire_spa_utils::audio::raw::AudioInfoRaw; +use pipewire_spa_utils::audio::AudioSampleFormat; +use pipewire_spa_utils::audio::AudioChannelPosition; use pipewire_spa_utils::format::{MediaSubtype, MediaType}; -use crate::utils::Direction; #[derive(Debug, Clone)] pub struct NodeInfo { diff --git a/pipewire-client/src/lib.rs b/pipewire-client/src/lib.rs index d330433a3..b2b33367d 100644 --- a/pipewire-client/src/lib.rs +++ b/pipewire-client/src/lib.rs @@ -12,8 +12,8 @@ pub use utils::Direction; mod error; mod info; -pub use info::NodeInfo; pub use info::AudioStreamInfo; +pub use info::NodeInfo; pub use pipewire as pipewire; pub use pipewire_spa_utils as spa_utils; diff --git a/pipewire-client/src/messages.rs b/pipewire-client/src/messages.rs index 0d2991cf1..85762a676 100644 --- a/pipewire-client/src/messages.rs +++ b/pipewire-client/src/messages.rs @@ -1,11 +1,11 @@ -use pipewire_spa_utils::audio::raw::AudioInfoRaw; -use std::collections::HashMap; -use std::fmt::{Debug, Formatter}; -use std::sync::{Arc, Mutex}; use crate::error::Error; use crate::info::{AudioStreamInfo, NodeInfo}; use crate::states::{DefaultAudioNodesState, GlobalId, GlobalObjectState, SettingsState}; use crate::utils::Direction; +use pipewire_spa_utils::audio::raw::AudioInfoRaw; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; +use std::sync::{Arc, Mutex}; pub(super) struct StreamCallback { callback: Arc>> diff --git a/pipewire-client/src/states.rs b/pipewire-client/src/states.rs index c1d848308..d6f7e1873 100644 --- a/pipewire-client/src/states.rs +++ b/pipewire-client/src/states.rs @@ -1,19 +1,19 @@ use super::constants::*; +use crate::error::Error; +use crate::listeners::{Listener, ListenerTriggerPolicy, Listeners}; +use crate::messages::StreamCallback; +use crate::utils::dict_ref_to_hashmap; +use crate::Direction; +use pipewire::spa::utils::dict::ParsableValue; +use pipewire_spa_utils::audio::raw::AudioInfoRaw; +use pipewire_spa_utils::audio::AudioChannel; +use pipewire_spa_utils::format::{MediaSubtype, MediaType}; use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::io::Cursor; use std::rc::Rc; use std::str::FromStr; -use pipewire::spa::utils::dict::ParsableValue; -use pipewire_spa_utils::audio::AudioChannel; -use pipewire_spa_utils::audio::raw::AudioInfoRaw; -use pipewire_spa_utils::format::{MediaSubtype, MediaType}; -use crate::Direction; -use crate::error::Error; -use crate::listeners::{Listener, ListenerTriggerPolicy, Listeners}; -use crate::messages::StreamCallback; -use crate::utils::dict_ref_to_hashmap; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub(super) struct GlobalId(u32); @@ -506,7 +506,7 @@ impl StreamState { listeners: Rc::new(RefCell::new(Listeners::new())), } } - + pub fn is_connected(&self) -> bool { self.is_connected } diff --git a/pipewire-client/src/utils.rs b/pipewire-client/src/utils.rs index 6760803c1..eab7b3c01 100644 --- a/pipewire-client/src/utils.rs +++ b/pipewire-client/src/utils.rs @@ -1,7 +1,7 @@ +use crate::listeners::{Listener, ListenerTriggerPolicy, Listeners}; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use crate::listeners::{Listener, ListenerTriggerPolicy, Listeners}; #[derive(Debug, Clone, PartialEq)] pub enum Direction { From 0a541b3fcd4aaa2be11ce80319789a8e7bc9e4d9 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Sun, 19 Jan 2025 18:45:48 +0000 Subject: [PATCH 04/17] Add testcontainer and some tests depending of server configuration --- .../.containers/pipewire.test.container | 18 + pipewire-client/Cargo.toml | 6 +- pipewire-client/src/client/api/core.rs | 27 +- pipewire-client/src/client/api/node.rs | 14 + .../src/client/connection_string.rs | 11 +- pipewire-client/src/client/handlers/event.rs | 18 +- .../src/client/handlers/registry.rs | 2 +- .../src/client/handlers/request.rs | 82 ++++- pipewire-client/src/client/handlers/thread.rs | 15 +- pipewire-client/src/client/implementation.rs | 93 ++++-- .../src/client/implementation_test.rs | 311 +++++++++++++++++- pipewire-client/src/constants.rs | 1 + pipewire-client/src/messages.rs | 10 +- pipewire-client/src/states.rs | 10 +- 14 files changed, 537 insertions(+), 81 deletions(-) create mode 100644 pipewire-client/.containers/pipewire.test.container diff --git a/pipewire-client/.containers/pipewire.test.container b/pipewire-client/.containers/pipewire.test.container new file mode 100644 index 000000000..ca1843c36 --- /dev/null +++ b/pipewire-client/.containers/pipewire.test.container @@ -0,0 +1,18 @@ +from fedora:41 as base + +run dnf update --assumeyes +run dnf install --assumeyes \ + procps-ng \ + pipewire \ + pipewire-utils \ + pipewire-alsa \ + pipewire-pulse \ + pipewire-devel \ + wireplumber \ + pulseaudio-utils + +env PIPEWIRE_CORE="pipewire-0" +env PIPEWIRE_REMOTE="pipewire-0" +env PIPEWIRE_RUNTIME_DIR="/run" + +entrypoint ["bash", "-c", "sleep 999d"] \ No newline at end of file diff --git a/pipewire-client/Cargo.toml b/pipewire-client/Cargo.toml index 62b0d41ba..d5e1e8a83 100644 --- a/pipewire-client/Cargo.toml +++ b/pipewire-client/Cargo.toml @@ -18,4 +18,8 @@ crossbeam-channel = "0.5" [dev-dependencies] rstest = "0.24" serial_test = "3.2" -testcontainers = "0.23" \ No newline at end of file +testcontainers = "0.23" +docker-api = "0.14" +futures = "0.3" +tokio = { version = "1", features = ["full"] } +uuid = { version = "1.12", features = ["v4"] } \ No newline at end of file diff --git a/pipewire-client/src/client/api/core.rs b/pipewire-client/src/client/api/core.rs index fff99370f..f35f8c68a 100644 --- a/pipewire-client/src/client/api/core.rs +++ b/pipewire-client/src/client/api/core.rs @@ -17,7 +17,22 @@ impl CoreApi { pub(crate) fn check_session_manager_registered(&self) -> Result<(), Error> { let request = MessageRequest::CheckSessionManagerRegistered; - self.api.send_request_without_response(&request) + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::CheckSessionManagerRegistered{ + session_manager_registered, + error + }) => { + if session_manager_registered { + return Ok(()); + } + Err(error.unwrap()) + }, + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } } pub fn quit(&self) { @@ -37,6 +52,11 @@ impl CoreApi { } } + pub(crate) fn get_settings_state(&self) -> Result<(), Error> { + let request = MessageRequest::SettingsState; + self.api.send_request_without_response(&request) + } + pub fn get_default_audio_nodes(&self) -> Result { let request = MessageRequest::DefaultAudioNodes; let response = self.api.send_request(&request); @@ -48,4 +68,9 @@ impl CoreApi { }), } } + + pub(crate) fn get_default_audio_nodes_state(&self) -> Result<(), Error> { + let request = MessageRequest::DefaultAudioNodesState; + self.api.send_request_without_response(&request) + } } \ No newline at end of file diff --git a/pipewire-client/src/client/api/node.rs b/pipewire-client/src/client/api/node.rs index e00fc8309..3d52ccd70 100644 --- a/pipewire-client/src/client/api/node.rs +++ b/pipewire-client/src/client/api/node.rs @@ -32,6 +32,20 @@ impl NodeApi { self.api.send_request_without_response(&request) } + pub(crate) fn get_count( + &self, + ) -> Result { + let request = MessageRequest::NodeCount; + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::NodeCount(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + pub fn create( &self, name: String, diff --git a/pipewire-client/src/client/connection_string.rs b/pipewire-client/src/client/connection_string.rs index 1b8368be2..5862498f6 100644 --- a/pipewire-client/src/client/connection_string.rs +++ b/pipewire-client/src/client/connection_string.rs @@ -1,10 +1,10 @@ use crate::constants::*; use std::path::PathBuf; -pub(super) struct PipewireClientConnectionString; +pub(super) struct PipewireClientSocketPath; -impl PipewireClientConnectionString { - pub(super) fn from_env() -> String { +impl PipewireClientSocketPath { + pub(super) fn from_env() -> PathBuf { let pipewire_runtime_dir = std::env::var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY); let xdg_runtime_dir = std::env::var(XDG_RUNTIME_DIR_ENVIRONMENT_KEY); @@ -26,11 +26,12 @@ impl PipewireClientConnectionString { }; let socket_path = PathBuf::from(socket_directory).join(pipewire_remote); - socket_path.to_str().unwrap().to_string() + socket_path } } pub(super) struct PipewireClientInfo { pub name: String, - pub connection_string: String, + pub socket_location: String, + pub socket_name: String, } \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/event.rs b/pipewire-client/src/client/handlers/event.rs index 78c2dad85..a2f645a40 100644 --- a/pipewire-client/src/client/handlers/event.rs +++ b/pipewire-client/src/client/handlers/event.rs @@ -77,27 +77,13 @@ fn handle_set_metadata_listeners( METADATA_NAME_PROPERTY_VALUE_SETTINGS => { metadata.add_property_listener( ListenerTriggerPolicy::Keep, - SettingsState::listener( - listener_state, - move |settings: &SettingsState| { - main_sender - .send(MessageResponse::SettingsState(settings.state.clone())) - .unwrap(); - } - ) + SettingsState::listener(listener_state) ) }, METADATA_NAME_PROPERTY_VALUE_DEFAULT => { metadata.add_property_listener( ListenerTriggerPolicy::Keep, - DefaultAudioNodesState::listener( - listener_state, - move |default_audio_devices: &DefaultAudioNodesState| { - main_sender - .send(MessageResponse::DefaultAudioNodesState(default_audio_devices.state.clone())) - .unwrap(); - } - ) + DefaultAudioNodesState::listener(listener_state) ) }, _ => { diff --git a/pipewire-client/src/client/handlers/registry.rs b/pipewire-client/src/client/handlers/registry.rs index 84ac3f312..abdeaf0e1 100644 --- a/pipewire-client/src/client/handlers/registry.rs +++ b/pipewire-client/src/client/handlers/registry.rs @@ -133,7 +133,7 @@ fn handle_node( ) { if global.props.is_none() { - + return; } let properties = global.props.unwrap(); let node = match properties.get(MEDIA_CLASS_PROPERTY_KEY) { diff --git a/pipewire-client/src/client/handlers/request.rs b/pipewire-client/src/client/handlers/request.rs index be120cccb..186c41615 100644 --- a/pipewire-client/src/client/handlers/request.rs +++ b/pipewire-client/src/client/handlers/request.rs @@ -101,6 +101,18 @@ pub(super) fn request_handler( main_sender.clone() ) } + MessageRequest::SettingsState => { + handle_settings_state( + state.clone(), + main_sender.clone() + ) + } + MessageRequest::DefaultAudioNodesState => { + handle_default_audio_nodes_state( + state.clone(), + main_sender.clone() + ) + } MessageRequest::NodeState(id) => { handle_node_state( id, @@ -114,6 +126,12 @@ pub(super) fn request_handler( main_sender.clone() ) } + MessageRequest::NodeCount => { + handle_node_count( + state.clone(), + main_sender.clone() + ) + } } } @@ -466,9 +484,7 @@ fn handle_check_session_manager_registered( main_sender: crossbeam_channel::Sender, ) { - // Checking if session manager is registered because we need "default" metadata - // object to determine default audio nodes (sink and source). - fn generate_error_message(session_managers: &Vec<&str>) -> String { + pub(crate) fn generate_error_message(session_managers: &Vec<&str>) -> String { let session_managers = session_managers.iter() .map(move |session_manager| { let session_manager = match *session_manager { @@ -486,14 +502,17 @@ fn handle_check_session_manager_registered( ); message } + // Checking if session manager is registered because we need "default" metadata + // object to determine default audio nodes (sink and source). let session_managers = vec![ APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER, APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION ]; + let error_description = generate_error_message(&session_managers); let state = state.borrow_mut(); let clients = state.get_clients().map_err(|_| { Error { - description: generate_error_message(&session_managers), + description: error_description.clone(), } }); let clients = match clients { @@ -509,14 +528,36 @@ fn handle_check_session_manager_registered( .any(|(_, client)| { session_managers.contains(&client.name.as_str()) }); - if session_manager_registered { - return; - } - let description = generate_error_message(&session_managers); main_sender - .send(MessageResponse::Error(Error { - description, - })) + .send(MessageResponse::CheckSessionManagerRegistered { + session_manager_registered, + error: match session_manager_registered { + true => Some(Error { + description: error_description.clone() + }), + false => None + }, + }) + .unwrap(); +} +fn handle_settings_state( + state: Rc>, + main_sender: crossbeam_channel::Sender, +) +{ + let state = state.borrow_mut(); + main_sender + .send(MessageResponse::SettingsState(state.get_settings().state)) + .unwrap(); +} +fn handle_default_audio_nodes_state( + state: Rc>, + main_sender: crossbeam_channel::Sender, +) +{ + let state = state.borrow_mut(); + main_sender + .send(MessageResponse::DefaultAudioNodesState(state.get_default_audio_nodes().state)) .unwrap(); } fn handle_node_state( @@ -558,4 +599,23 @@ fn handle_node_states( }) .collect::>(); main_sender.send(MessageResponse::NodeStates(states)).unwrap(); +} +fn handle_node_count( + state: Rc>, + main_sender: crossbeam_channel::Sender, +) +{ + let state = state.borrow_mut(); + match state.get_nodes() { + Ok(value) => { + main_sender + .send(MessageResponse::NodeCount(value.len() as u32)) + .unwrap(); + }, + Err(_) => { + main_sender + .send(MessageResponse::NodeCount(0)) + .unwrap(); + } + }; } \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/thread.rs b/pipewire-client/src/client/handlers/thread.rs index aa83d40d8..fee0fb207 100644 --- a/pipewire-client/src/client/handlers/thread.rs +++ b/pipewire-client/src/client/handlers/thread.rs @@ -2,7 +2,7 @@ use crate::client::connection_string::PipewireClientInfo; use crate::client::handlers::event::event_handler; use crate::client::handlers::registry::registry_global_handler; use crate::client::handlers::request::request_handler; -use crate::constants::PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ; +use crate::constants::{PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ, PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY}; use crate::error::Error; use crate::messages::{EventMessage, MessageRequest, MessageResponse}; use crate::states::GlobalState; @@ -18,7 +18,8 @@ pub fn pw_thread( event_receiver: pipewire::channel::Receiver, ) { let connection_properties = Some(pipewire::properties::properties! { - *pipewire::keys::REMOTE_NAME => client_info.connection_string, + PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY => client_info.socket_location, + *pipewire::keys::REMOTE_NAME => client_info.socket_name, *pipewire::keys::APP_NAME => client_info.name, }); @@ -73,10 +74,12 @@ pub fn pw_thread( let registry = match core.get_registry() { Ok(value) => Rc::new(value), Err(value) => { - unsafe { - pipewire::deinit(); - } - panic!("Failed to get Pipewire registry: {}", value); + main_sender + .send(MessageResponse::Error(Error { + description: format!("Failed to get Pipewire registry: {}", value), + })) + .unwrap(); + return; } }; diff --git a/pipewire-client/src/client/implementation.rs b/pipewire-client/src/client/implementation.rs index 23f9e3ab5..27f2f4e2f 100644 --- a/pipewire-client/src/client/implementation.rs +++ b/pipewire-client/src/client/implementation.rs @@ -1,13 +1,14 @@ extern crate pipewire; use crate::client::api::{CoreApi, InternalApi, NodeApi, StreamApi}; -use crate::client::connection_string::{PipewireClientConnectionString, PipewireClientInfo}; +use crate::client::connection_string::{PipewireClientInfo, PipewireClientSocketPath}; use crate::client::handlers::thread; use crate::error::Error; use crate::messages::{EventMessage, MessageRequest, MessageResponse}; use crate::states::GlobalObjectState; use crate::utils::Backoff; use std::fmt::{Debug, Formatter}; +use std::path::PathBuf; use std::string::ToString; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; @@ -19,7 +20,7 @@ pub(super) static CLIENT_INDEX: AtomicU32 = AtomicU32::new(0); pub struct PipewireClient { pub(crate) name: String, - connection_string: String, + socket_path: PathBuf, thread_handle: Option>, internal_api: Arc, core_api: CoreApi, @@ -32,11 +33,12 @@ impl PipewireClient { let name = format!("{}-{}", CLIENT_NAME_PREFIX, CLIENT_INDEX.load(Ordering::SeqCst)); CLIENT_INDEX.fetch_add(1, Ordering::SeqCst); - let connection_string = PipewireClientConnectionString::from_env(); + let socket_path = PipewireClientSocketPath::from_env(); let client_info = PipewireClientInfo { name: name.clone(), - connection_string: connection_string.clone(), + socket_location: socket_path.parent().unwrap().to_str().unwrap().to_string(), + socket_name: socket_path.file_name().unwrap().to_str().unwrap().to_string(), }; let (main_sender, main_receiver) = crossbeam_channel::unbounded(); @@ -58,7 +60,7 @@ impl PipewireClient { let client = Self { name, - connection_string, + socket_path, thread_handle: Some(pw_thread), internal_api, core_api, @@ -68,26 +70,27 @@ impl PipewireClient { match client.wait_initialization() { Ok(_) => {} - Err(value) => return Err(value) + Err(value) => return Err(Error { + description: format!("Initialization error: {}", value), + }) }; match client.wait_post_initialization() { Ok(_) => {} - Err(value) => return Err(value), + Err(value) => return Err(Error { + description: format!("Post initialization error: {}", value), + }), }; Ok(client) } fn wait_initialization(&self) -> Result<(), Error> { - let response = self.internal_api.wait_response(); + let timeout_duration = std::time::Duration::from_millis(10 * 1000); + let response = self.internal_api.wait_response_with_timeout(timeout_duration); let response = match response { Ok(value) => value, - Err(value) => { - return Err(Error { - description: format!( - "Failed during pipewire initialization: {:?}", - value - ), - }) + Err(_) => { + // Timeout is certainly due to missing session manager + return self.core_api.check_session_manager_registered(); } }; match response { @@ -102,11 +105,20 @@ impl PipewireClient { let mut settings_initialized = false; let mut default_audio_devices_initialized = false; let mut nodes_initialized = false; - #[cfg(debug_assertions)] - let timeout_duration = std::time::Duration::from_secs(u64::MAX); - #[cfg(not(debug_assertions))] - let timeout_duration = std::time::Duration::from_millis(500); + let timeout_duration = std::time::Duration::from_millis(1); self.core_api.check_session_manager_registered()?; + match self.node_api.get_count() { + Ok(value) => { + if value == 0 { + return Err(Error { + description: "Zero node registered".to_string(), + }) + } + } + Err(value) => return Err(value), + } + self.core_api.get_settings_state()?; + self.core_api.get_default_audio_nodes_state()?; self.node_api.get_states()?; let operation = move || { let response = self.internal_api.wait_response_with_timeout(timeout_duration); @@ -117,9 +129,12 @@ impl PipewireClient { GlobalObjectState::Initialized => { settings_initialized = true; } - _ => return Err(Error { - description: "Settings not yet initialized".to_string(), - }) + _ => { + self.core_api.get_settings_state()?; + return Err(Error { + description: "Settings not yet initialized".to_string(), + }) + } }; }, MessageResponse::DefaultAudioNodesState(state) => { @@ -127,9 +142,12 @@ impl PipewireClient { GlobalObjectState::Initialized => { default_audio_devices_initialized = true; } - _ => return Err(Error { - description: "Default audio nodes not yet initialized".to_string(), - }) + _ => { + self.core_api.get_default_audio_nodes_state()?; + return Err(Error { + description: "Default audio nodes not yet initialized".to_string(), + }) + } } }, MessageResponse::NodeStates(states) => { @@ -152,15 +170,30 @@ impl PipewireClient { description: format!("Received unexpected response: {:?}", value), }), } - Err(value) => return Err(Error { - description: format!("Failed during post initialization: {:?}", value), + Err(_) => return Err(Error { + description: format!( + r"Timeout: + - settings: {} + - default audio nodes: {} + - nodes: {}", + settings_initialized, + default_audio_devices_initialized, + nodes_initialized + ), }) }; if settings_initialized == false || default_audio_devices_initialized == false || nodes_initialized == false { return Err(Error { - description: "Post initialization not yet finalized".to_string(), + description: format!( + r"Conditions not yet initialized: + - settings: {} + - default audio nodes: {} + - nodes: {}", + settings_initialized, + default_audio_devices_initialized, + nodes_initialized + ), }) - } return Ok(()); }; @@ -191,7 +224,7 @@ impl PipewireClient { impl Debug for PipewireClient { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "PipewireClient: {}", self.connection_string) + writeln!(f, "PipewireClient: {}", self.socket_path.to_str().unwrap()) } } diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs index 5e8a83d52..c139c77a5 100644 --- a/pipewire-client/src/client/implementation_test.rs +++ b/pipewire-client/src/client/implementation_test.rs @@ -1,8 +1,294 @@ use crate::client::implementation::CLIENT_INDEX; +use crate::constants::*; use crate::PipewireClient; -use rstest::rstest; +use docker_api::models::ImageBuildChunk; +use docker_api::opts::ImageBuildOpts; +use docker_api::Docker; +use futures::StreamExt; +use pipewire::spa::utils::dict::ParsableValue; +use rstest::{fixture, rstest}; use serial_test::serial; +use std::path::{Path, PathBuf}; use std::sync::atomic::Ordering; +use testcontainers::core::{CmdWaitFor, ExecCommand, Mount}; +use testcontainers::runners::AsyncRunner; +use testcontainers::{ContainerAsync, GenericImage, Image, ImageExt}; +use tokio::io::AsyncReadExt; +use uuid::Uuid; + +struct Container { + name: String, + tag: String, + container_file_path: PathBuf, + container: Option>, + socket_id: Uuid, + pipewire_pid: Option, + wireplumber_pid: Option, + pulse_pid: Option, +} + +impl Container { + pub fn new( + name: String, + container_file_path: PathBuf, + ) -> Self { + Self { + name, + tag: "latest".to_string(), + container_file_path, + container: None, + socket_id: Uuid::new_v4(), + pipewire_pid: None, + wireplumber_pid: None, + pulse_pid: None, + } + } + + pub fn socket_location(&self) -> PathBuf { + Path::new("/run/pipewire-sockets").join(self.socket_id.to_string()).to_path_buf() + } + + pub fn socket_name(&self) -> String { + format!("{}", self.socket_id) + } + + pub fn build(&self) { + const DOCKER_HOST_ENVIRONMENT_KEY: &str = "DOCKER_HOST"; + const CONTAINER_HOST_ENVIRONMENT_KEY: &str = "CONTAINER_HOST"; + let docker_host = std::env::var(DOCKER_HOST_ENVIRONMENT_KEY); + let container_host = std::env::var(CONTAINER_HOST_ENVIRONMENT_KEY); + let uri = match (docker_host, container_host) { + (Ok(value), Ok(_)) => value, + (Ok(value), Err(_)) => value, + (Err(_), Ok(value)) => { + // TestContainer does not recognize CONTAINER_HOST. + // Instead, with set DOCKET_HOST env var with the same value + std::env::set_var(DOCKER_HOST_ENVIRONMENT_KEY, value.clone()); + value + }, + (Err(_), Err(_)) => panic!( + "${} or ${} should be set.", + DOCKER_HOST_ENVIRONMENT_KEY, CONTAINER_HOST_ENVIRONMENT_KEY + ), + }; + let api = Docker::new(uri).unwrap(); + let images = api.images(); + let build_image_options= ImageBuildOpts::builder(self.container_file_path.parent().unwrap().to_str().unwrap()) + .tag(format!("{}:{}", self.name, self.tag)) + .dockerfile(self.container_file_path.file_name().unwrap().to_str().unwrap()) + .build(); + let mut stream = images.build(&build_image_options); + let runtime = tokio::runtime::Runtime::new().unwrap(); + while let Some(build_result) = runtime.block_on(stream.next()) { + match build_result { + Ok(output) => { + let output = match output { + ImageBuildChunk::Update { stream } => stream, + ImageBuildChunk::Error { error, error_detail } => { + panic!("Error {}: {}", error, error_detail.message); + } + ImageBuildChunk::Digest { aux } => aux.id, + ImageBuildChunk::PullStatus { .. } => { + return + } + }; + print!("{}", output); + }, + Err(e) => panic!("Error: {e}"), + } + } + } + + pub fn run(&mut self) { + let socket_location = self.socket_location(); + let socket_name = self.socket_name(); + let container = GenericImage::new(self.name.clone(), self.tag.clone()) + .with_env_var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, socket_location.to_str().unwrap()) + .with_env_var(PIPEWIRE_CORE_ENVIRONMENT_KEY, socket_name.clone()) + .with_env_var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, socket_name.clone()) + .with_env_var("PULSE_RUNTIME_PATH", socket_location.join("pulse").to_str().unwrap()) + .with_mount(Mount::volume_mount( + "pipewire-sockets", + socket_location.parent().unwrap().to_str().unwrap(), + )); + let runtime = tokio::runtime::Runtime::new().unwrap(); + let container = runtime.block_on(container.start()).unwrap(); + self.container = Some(container); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "mkdir", + "--parent", + socket_location.to_str().unwrap(), + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + self.start_pipewire(); + } + + fn run_process(&mut self, process_name: &str) -> u32 { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + process_name + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + let mut result = runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "pidof", + process_name, + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + let mut pid = String::new(); + runtime.block_on(result.stdout().read_to_string(&mut pid)).unwrap(); + pid = pid.trim_end().to_string(); + u32::parse_value(pid.as_str()).unwrap() + } + + fn kill_process(&mut self, process_id: u32) { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "kill", + format!("{}", process_id).as_str() + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + } + + fn start_pipewire(&mut self) { + let pid = self.run_process("pipewire"); + self.pipewire_pid = Some(pid); + } + + fn stop_pipewire(&mut self) { + self.kill_process(self.pipewire_pid.unwrap()) + } + + pub fn start_wireplumber(&mut self) { + let pid = self.run_process("wireplumber"); + self.wireplumber_pid = Some(pid); + } + + pub fn stop_wireplumber(&mut self) { + if self.wireplumber_pid.is_none() { + return; + } + self.kill_process(self.wireplumber_pid.unwrap()); + } + + pub fn start_pulse(&mut self) { + let pid = self.run_process("pipewire-pulse"); + self.pulse_pid = Some(pid); + } + + pub fn stop_pulse(&mut self) { + if self.pulse_pid.is_none() { + return; + } + self.kill_process(self.pulse_pid.unwrap()); + } + + pub fn load_null_sink_module(&self) { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "pactl", + "load-module", + "module-null-sink" + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + } + + pub fn create_virtual_node(&self) { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "pw-cli", + "create-node", + "adapter", + "'{ factory.name=support.null-audio-sink node.name=test-sink media.class=Audio/Sink object.linger=true audio.position=[FL FR] }'" + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + } +} + +impl Drop for Container { + fn drop(&mut self) { + if self.container.is_none() { + return; + } + self.stop_pulse(); + self.stop_wireplumber(); + self.stop_pipewire(); + let socket_location = self.socket_location(); + let container = self.container.take().unwrap(); + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(container.exec( + ExecCommand::new(vec![ + "rm", + "--force", + "--recursive", + socket_location.join("*").to_str().unwrap(), + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + runtime.block_on(container.stop()).unwrap(); + runtime.block_on(container.rm()).unwrap(); + } +} + +#[fixture] +fn pipewire_server_with_default_configuration() -> Container { + let mut container = Container::new( + "pipewire-default".to_string(), + PathBuf::from(".containers/pipewire.test.container"), + ); + container.build(); + container.run(); + container.start_wireplumber(); + container.start_pulse(); + container.load_null_sink_module(); + container.create_virtual_node(); + container +} + +#[fixture] +fn pipewire_server_without_session_manager() -> Container { + let mut container = Container::new( + "pipewire-without-session-manager".to_string(), + PathBuf::from(".containers/pipewire.test.container"), + ); + container.build(); + container.run(); + container +} + +#[fixture] +fn pipewire_server_without_node() -> Container { + let mut container = Container::new( + "pipewire-without-node".to_string(), + PathBuf::from(".containers/pipewire.test.container"), + ); + container.build(); + container.run(); + container.start_wireplumber(); + container +} + +fn set_socket_env_vars(server: &Container) { + std::env::set_var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, server.socket_location()); + std::env::set_var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, server.socket_name()); +} + +#[rstest] +#[serial] +pub fn initialization() { + let _ = PipewireClient::new().unwrap(); +} #[rstest] #[serial] @@ -11,4 +297,27 @@ pub fn name() { assert_eq!(format!("cpal-client-{}", CLIENT_INDEX.load(Ordering::SeqCst) - 1), client_1.name); let client_2 = PipewireClient::new().unwrap(); assert_eq!(format!("cpal-client-{}", CLIENT_INDEX.load(Ordering::SeqCst) - 1), client_2.name); +} + +#[rstest] +#[serial] +pub fn server_with_default_configuration(pipewire_server_with_default_configuration: Container) { + set_socket_env_vars(&pipewire_server_with_default_configuration); + let _ = PipewireClient::new().unwrap(); +} + +#[rstest] +#[serial] +pub fn server_without_session_manager(pipewire_server_without_session_manager: Container) { + set_socket_env_vars(&pipewire_server_without_session_manager); + let error = PipewireClient::new().unwrap_err(); + assert_eq!(true, error.description.contains("No session manager registered")) +} + +#[rstest] +#[serial] +pub fn server_without_node(pipewire_server_without_node: Container) { + set_socket_env_vars(&pipewire_server_without_node); + let error = PipewireClient::new().unwrap_err(); + assert_eq!("Post initialization error: Zero node registered", error.description) } \ No newline at end of file diff --git a/pipewire-client/src/constants.rs b/pipewire-client/src/constants.rs index 0bb88192e..c7057c400 100644 --- a/pipewire-client/src/constants.rs +++ b/pipewire-client/src/constants.rs @@ -1,4 +1,5 @@ pub(super) const PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY: &str = "PIPEWIRE_RUNTIME_DIR"; +pub(super) const PIPEWIRE_CORE_ENVIRONMENT_KEY: &str = "PIPEWIRE_CORE"; pub(super) const PIPEWIRE_REMOTE_ENVIRONMENT_KEY: &str = "PIPEWIRE_REMOTE"; pub(super) const XDG_RUNTIME_DIR_ENVIRONMENT_KEY: &str = "XDG_RUNTIME_DIR"; pub(super) const PIPEWIRE_REMOTE_ENVIRONMENT_DEFAULT: &str = "pipewire-0"; diff --git a/pipewire-client/src/messages.rs b/pipewire-client/src/messages.rs index 85762a676..2887ba12e 100644 --- a/pipewire-client/src/messages.rs +++ b/pipewire-client/src/messages.rs @@ -66,8 +66,11 @@ pub(super) enum MessageRequest { }, // Internal requests CheckSessionManagerRegistered, + SettingsState, + DefaultAudioNodesState, NodeState(GlobalId), NodeStates, + NodeCount } #[derive(Debug, Clone)] @@ -87,10 +90,15 @@ pub(super) enum MessageResponse { ConnectStream, DisconnectStream, // Internals responses + CheckSessionManagerRegistered { + session_manager_registered: bool, + error: Option, + }, SettingsState(GlobalObjectState), DefaultAudioNodesState(GlobalObjectState), NodeState(GlobalObjectState), - NodeStates(Vec) + NodeStates(Vec), + NodeCount(u32), } #[derive(Debug, Clone)] diff --git a/pipewire-client/src/states.rs b/pipewire-client/src/states.rs index d6f7e1873..52990221c 100644 --- a/pipewire-client/src/states.rs +++ b/pipewire-client/src/states.rs @@ -672,9 +672,7 @@ impl Default for SettingsState { } impl SettingsState { - pub(super) fn listener(state: Rc>, callback: F) -> impl Fn(u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static - where - F: Fn(&SettingsState) + 'static + pub(super) fn listener(state: Rc>) -> impl Fn(u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static { const EXPECTED_PROPERTY: u32 = 5; let property_count: Rc> = Rc::new(Cell::new(0)); @@ -711,7 +709,6 @@ impl SettingsState { }; if let (GlobalObjectState::Pending, EXPECTED_PROPERTY) = (settings.state.clone(), property_count.get()) { settings.state = GlobalObjectState::Initialized; - callback(settings) } 0 } @@ -736,9 +733,7 @@ impl Default for DefaultAudioNodesState { } impl DefaultAudioNodesState { - pub(super) fn listener(state: Rc>, callback: F) -> impl Fn(u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static - where - F: Fn(&DefaultAudioNodesState) + 'static + pub(super) fn listener(state: Rc>) -> impl Fn(u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static { const EXPECTED_PROPERTY: u32 = 2; let property_count: Rc> = Rc::new(Cell::new(0)); @@ -773,7 +768,6 @@ impl DefaultAudioNodesState { }; if let (GlobalObjectState::Pending, EXPECTED_PROPERTY) = (default_audio_devices.state.clone(), property_count.get()) { default_audio_devices.state = GlobalObjectState::Initialized; - callback(default_audio_devices) } 0 } From 70643fc4112de4027ef0368eb90c000405ebae08 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Sun, 19 Jan 2025 18:48:37 +0000 Subject: [PATCH 05/17] Optimize imports --- pipewire-client/src/info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipewire-client/src/info.rs b/pipewire-client/src/info.rs index eb48b298a..9551f7475 100644 --- a/pipewire-client/src/info.rs +++ b/pipewire-client/src/info.rs @@ -1,7 +1,7 @@ use crate::utils::Direction; use pipewire_spa_utils::audio::raw::AudioInfoRaw; -use pipewire_spa_utils::audio::AudioSampleFormat; use pipewire_spa_utils::audio::AudioChannelPosition; +use pipewire_spa_utils::audio::AudioSampleFormat; use pipewire_spa_utils::format::{MediaSubtype, MediaType}; #[derive(Debug, Clone)] From a5a4e871380f4c49d2e26a2aab5cc60f478099f6 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Sun, 19 Jan 2025 19:11:27 +0000 Subject: [PATCH 06/17] Add tini as container entrypoint to handle process signal termination properly --- pipewire-client/.containers/pipewire.test.container | 5 ++++- pipewire-client/src/client/implementation_test.rs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pipewire-client/.containers/pipewire.test.container b/pipewire-client/.containers/pipewire.test.container index ca1843c36..8823d5800 100644 --- a/pipewire-client/.containers/pipewire.test.container +++ b/pipewire-client/.containers/pipewire.test.container @@ -2,6 +2,7 @@ from fedora:41 as base run dnf update --assumeyes run dnf install --assumeyes \ + tini \ procps-ng \ pipewire \ pipewire-utils \ @@ -15,4 +16,6 @@ env PIPEWIRE_CORE="pipewire-0" env PIPEWIRE_REMOTE="pipewire-0" env PIPEWIRE_RUNTIME_DIR="/run" -entrypoint ["bash", "-c", "sleep 999d"] \ No newline at end of file +# For handling signal properly. Avoid to hangout before container runtime send SIGKILL signal. +entrypoint ["tini", "--"] +cmd ["bash", "-c", "sleep 999d"] \ No newline at end of file diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs index c139c77a5..fa47794d5 100644 --- a/pipewire-client/src/client/implementation_test.rs +++ b/pipewire-client/src/client/implementation_test.rs @@ -151,6 +151,7 @@ impl Container { runtime.block_on(self.container.as_ref().unwrap().exec( ExecCommand::new(vec![ "kill", + "-s", "SIGKILL", format!("{}", process_id).as_str() ]) .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), From 82dc7a3cafdd355c6200a0af04fd5b061205286c Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Sun, 19 Jan 2025 22:28:22 +0000 Subject: [PATCH 07/17] Move items to test_utils module. Refactor tests to user containerized server and test client. Remove rstest use of case directive. --- .../.containers/pipewire.nodes.conf | 30 ++ .../.containers/pipewire.test.container | 2 + pipewire-client/src/client/api/core_test.rs | 10 +- pipewire-client/src/client/api/fixtures.rs | 36 -- pipewire-client/src/client/api/mod.rs | 6 +- pipewire-client/src/client/api/node_test.rs | 61 ++-- pipewire-client/src/client/api/stream_test.rs | 258 +++++++------- pipewire-client/src/client/implementation.rs | 13 +- .../src/client/implementation_test.rs | 318 +----------------- pipewire-client/src/lib.rs | 4 + pipewire-client/src/states.rs | 3 + pipewire-client/src/test_utils/fixtures.rs | 83 +++++ pipewire-client/src/test_utils/mod.rs | 2 + pipewire-client/src/test_utils/server.rs | 312 +++++++++++++++++ 14 files changed, 642 insertions(+), 496 deletions(-) create mode 100644 pipewire-client/.containers/pipewire.nodes.conf delete mode 100644 pipewire-client/src/client/api/fixtures.rs create mode 100644 pipewire-client/src/test_utils/fixtures.rs create mode 100644 pipewire-client/src/test_utils/mod.rs create mode 100644 pipewire-client/src/test_utils/server.rs diff --git a/pipewire-client/.containers/pipewire.nodes.conf b/pipewire-client/.containers/pipewire.nodes.conf new file mode 100644 index 000000000..73babe17d --- /dev/null +++ b/pipewire-client/.containers/pipewire.nodes.conf @@ -0,0 +1,30 @@ +context.objects = [ + { factory = adapter + args = { + factory.name = support.null-audio-sink + node.name = "test-sink" + node.description = "test-sink" + node.nick = "test-sink" + media.class = Audio/Sink + audio.channel = 2 + audio.position = [ FL FR ] + object.linger = true + monitor.passthrough = true + monitor.channel-volumes = true + } + } + { factory = adapter + args = { + factory.name = support.null-audio-sink + node.name = "test-source" + node.description = "test-source" + node.nick = "test-source" + media.class = Audio/Source + audio.channel = 2 + audio.position = [ FL FR ] + object.linger = true + monitor.passthrough = true + monitor.channel-volumes = true + } + } +] \ No newline at end of file diff --git a/pipewire-client/.containers/pipewire.test.container b/pipewire-client/.containers/pipewire.test.container index 8823d5800..7db129c2f 100644 --- a/pipewire-client/.containers/pipewire.test.container +++ b/pipewire-client/.containers/pipewire.test.container @@ -16,6 +16,8 @@ env PIPEWIRE_CORE="pipewire-0" env PIPEWIRE_REMOTE="pipewire-0" env PIPEWIRE_RUNTIME_DIR="/run" +copy pipewire.nodes.conf /root/pipewire.nodes.conf + # For handling signal properly. Avoid to hangout before container runtime send SIGKILL signal. entrypoint ["tini", "--"] cmd ["bash", "-c", "sleep 999d"] \ No newline at end of file diff --git a/pipewire-client/src/client/api/core_test.rs b/pipewire-client/src/client/api/core_test.rs index 688c7b447..2c230be23 100644 --- a/pipewire-client/src/client/api/core_test.rs +++ b/pipewire-client/src/client/api/core_test.rs @@ -1,18 +1,16 @@ -use crate::client::api::fixtures::client; -use crate::PipewireClient; use rstest::rstest; use serial_test::serial; +use crate::test_utils::fixtures::{client, PipewireTestClient}; #[rstest] #[serial] -fn quit() { - let client = PipewireClient::new().unwrap(); +fn quit(client: PipewireTestClient) { client.core().quit(); } #[rstest] #[serial] -pub fn settings(client: &PipewireClient) { +pub fn settings(client: PipewireTestClient) { let settings = client.core().get_settings().unwrap(); assert_eq!(true, settings.sample_rate > u32::default()); assert_eq!(true, settings.default_buffer_size > u32::default()); @@ -23,7 +21,7 @@ pub fn settings(client: &PipewireClient) { #[rstest] #[serial] -pub fn default_audio_nodes(client: &PipewireClient) { +pub fn default_audio_nodes(client: PipewireTestClient) { let default_audio_nodes = client.core().get_default_audio_nodes().unwrap(); assert_eq!(false, default_audio_nodes.sink.is_empty()); assert_eq!(false, default_audio_nodes.source.is_empty()); diff --git a/pipewire-client/src/client/api/fixtures.rs b/pipewire-client/src/client/api/fixtures.rs deleted file mode 100644 index 9179b9ddc..000000000 --- a/pipewire-client/src/client/api/fixtures.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::{Direction, NodeInfo, PipewireClient}; -use rstest::fixture; - -#[fixture] -#[once] -pub(crate) fn client() -> PipewireClient { - PipewireClient::new().unwrap() -} - -#[fixture] -pub(crate) fn input_nodes(client: &PipewireClient) -> Vec { - client.node().enumerate(Direction::Input).unwrap() -} - -#[fixture] -pub(crate) fn output_nodes(client: &PipewireClient) -> Vec { - client.node().enumerate(Direction::Output).unwrap() -} - -#[fixture] -pub(crate) fn default_input_node(input_nodes: Vec) -> NodeInfo { - input_nodes.iter() - .filter(|node| node.is_default) - .last() - .cloned() - .unwrap() -} - -#[fixture] -pub(crate) fn default_output_node(output_nodes: Vec) -> NodeInfo { - output_nodes.iter() - .filter(|node| node.is_default) - .last() - .cloned() - .unwrap() -} \ No newline at end of file diff --git a/pipewire-client/src/client/api/mod.rs b/pipewire-client/src/client/api/mod.rs index c0b1ea2ea..57c4db349 100644 --- a/pipewire-client/src/client/api/mod.rs +++ b/pipewire-client/src/client/api/mod.rs @@ -17,8 +17,4 @@ pub(super) use stream::StreamApi; mod stream_test; mod internal; -pub(super) use internal::InternalApi; - -#[cfg(test)] -#[path = "fixtures.rs"] -mod fixtures; \ No newline at end of file +pub(super) use internal::InternalApi; \ No newline at end of file diff --git a/pipewire-client/src/client/api/node_test.rs b/pipewire-client/src/client/api/node_test.rs index 32dfef35c..36b098ce1 100644 --- a/pipewire-client/src/client/api/node_test.rs +++ b/pipewire-client/src/client/api/node_test.rs @@ -1,9 +1,10 @@ -use crate::client::api::fixtures::client; -use crate::{Direction, PipewireClient}; +use crate::{Direction}; +use crate::test_utils::fixtures::client; use rstest::rstest; use serial_test::serial; +use crate::test_utils::fixtures::PipewireTestClient; -fn internal_enumerate(client: &PipewireClient, direction: Direction) { +fn internal_enumerate(client: &PipewireTestClient, direction: Direction) { let nodes = client.node().enumerate(direction).unwrap(); assert_eq!(false, nodes.is_empty()); let default_node = nodes.iter() @@ -12,7 +13,7 @@ fn internal_enumerate(client: &PipewireClient, direction: Direction) { assert_eq!(true, default_node.is_some()); } -fn internal_create(client: &PipewireClient, direction: Direction) { +fn internal_create(client: &PipewireTestClient, direction: Direction) { client.node() .create( "test".to_string(), @@ -24,35 +25,53 @@ fn internal_create(client: &PipewireClient, direction: Direction) { } #[rstest] -#[case::input(Direction::Input)] -#[case::output(Direction::Output)] #[serial] -fn enumerate( - client: &PipewireClient, - #[case] direction: Direction +fn enumerate_input( + client: PipewireTestClient, ) { - internal_enumerate(&client, direction); + internal_enumerate(&client, Direction::Input); } #[rstest] -#[case::input(Direction::Input)] -#[case::output(Direction::Output)] #[serial] -fn create( - client: &PipewireClient, - #[case] direction: Direction +fn enumerate_output( + client: PipewireTestClient, ) { - internal_create(&client, direction); + internal_enumerate(&client, Direction::Output); } #[rstest] -#[case::input(Direction::Input)] -#[case::output(Direction::Output)] #[serial] -fn create_then_enumerate( - client: &PipewireClient, - #[case] direction: Direction +fn create_input( + client: PipewireTestClient, ) { + internal_create(&client, Direction::Input); +} + +#[rstest] +#[serial] +fn create_output( + client: PipewireTestClient, +) { + internal_create(&client, Direction::Output); +} + +#[rstest] +#[serial] +fn create_then_enumerate_input( + client: PipewireTestClient, +) { + let direction = Direction::Input; + internal_create(&client, direction.clone()); + internal_enumerate(&client, direction.clone()); +} + +#[rstest] +#[serial] +fn create_then_enumerate_output( + client: PipewireTestClient, +) { + let direction = Direction::Output; internal_create(&client, direction.clone()); internal_enumerate(&client, direction.clone()); } \ No newline at end of file diff --git a/pipewire-client/src/client/api/stream_test.rs b/pipewire-client/src/client/api/stream_test.rs index 46658f70b..b35c88477 100644 --- a/pipewire-client/src/client/api/stream_test.rs +++ b/pipewire-client/src/client/api/stream_test.rs @@ -1,10 +1,13 @@ -use crate::client::api::fixtures::{client, default_input_node, default_output_node}; -use crate::{Direction, NodeInfo, PipewireClient}; +use crate::{Direction, NodeInfo}; +use crate::test_utils::fixtures::client; +use crate::test_utils::fixtures::default_input_node; +use crate::test_utils::fixtures::default_output_node; use rstest::rstest; use serial_test::serial; +use crate::test_utils::fixtures::PipewireTestClient; fn internal_create( - client: &PipewireClient, + client: &PipewireTestClient, node: NodeInfo, direction: Direction, callback: F, @@ -20,7 +23,7 @@ fn internal_create( } fn internal_delete( - client: &PipewireClient, + client: &PipewireTestClient, stream: &String ) { client.stream() @@ -29,7 +32,7 @@ fn internal_delete( } fn internal_create_connected( - client: &PipewireClient, + client: &PipewireTestClient, node: NodeInfo, direction: Direction, callback: F, @@ -46,156 +49,161 @@ fn internal_create_connected -where - S: Fn() -> String, - T: Fn(&String) -> (), - D: Fn(&String), -{ - setup: S, - test: T, - teardown: D, - stream_name: Option +fn abstract_create( + client: &PipewireTestClient, + default_input_node: NodeInfo, + default_output_node: NodeInfo, + direction: Direction +) -> String { + let stream = internal_create( + &client, + match direction { + Direction::Input => default_input_node.clone(), + Direction::Output => default_output_node.clone() + }, + direction.clone(), + move |_| { + assert!(true); + } + ); + match direction { + Direction::Input => assert_eq!(true, stream.ends_with(".stream_input")), + Direction::Output => assert_eq!(true, stream.ends_with(".stream_output")) + }; + stream } -impl StreamTest -where - S: Fn() -> String, - T: Fn(&String) -> (), - D: Fn(&String), -{ - fn new(setup: S, test: T, teardown: D) -> Self { - Self { - setup, - test, - teardown, - stream_name: None, - } - } +#[rstest] +#[serial] +fn create_input( + client: PipewireTestClient, + default_input_node: NodeInfo, + default_output_node: NodeInfo, +) { + let direction = Direction::Input; + abstract_create(&client, default_input_node, default_output_node, direction); +} - fn run(&mut self) { - self.stream_name = Some((self.setup)()); - (self.test)(self.stream_name.as_ref().unwrap()); - } +#[rstest] +#[serial] +fn create_output( + client: PipewireTestClient, + default_input_node: NodeInfo, + default_output_node: NodeInfo, +) { + let direction = Direction::Output; + abstract_create(&client, default_input_node, default_output_node, direction); } -impl Drop for StreamTest -where - S: Fn() -> String, - T: Fn(&String) -> (), - D: Fn(&String), -{ - fn drop(&mut self) { - (self.teardown)(self.stream_name.as_ref().unwrap()) - } +#[rstest] +#[serial] +fn delete_input( + client: PipewireTestClient, + default_input_node: NodeInfo, + default_output_node: NodeInfo, +) { + let direction = Direction::Input; + let stream = abstract_create(&client, default_input_node, default_output_node, direction); + client.stream().delete(stream).unwrap() } #[rstest] -#[case::input(Direction::Input)] -#[case::output(Direction::Output)] #[serial] -fn create( - client: &PipewireClient, +fn delete_output( + client: PipewireTestClient, default_input_node: NodeInfo, default_output_node: NodeInfo, - #[case] direction: Direction ) { - let mut test = StreamTest::new( - || { - internal_create( - &client, - match direction { - Direction::Input => default_input_node.clone(), - Direction::Output => default_output_node.clone() - }, - direction.clone(), - move |_| { - assert!(true); - } - ) - }, - |stream| { - match direction { - Direction::Input => assert_eq!(true, stream.ends_with(".stream_input")), - Direction::Output => assert_eq!(true, stream.ends_with(".stream_output")) - }; + let direction = Direction::Output; + let stream = abstract_create(&client, default_input_node, default_output_node, direction); + client.stream().delete(stream).unwrap() +} + +fn abstract_connect( + client: &PipewireTestClient, + default_input_node: NodeInfo, + default_output_node: NodeInfo, + direction: Direction +) { + let stream = internal_create( + &client, + match direction { + Direction::Input => default_input_node.clone(), + Direction::Output => default_output_node.clone() }, - |stream| { - internal_delete(&client, stream); + direction.clone(), + move |mut buffer| { + let data = buffer.datas_mut(); + let data = &mut data[0]; + let data = data.data().unwrap(); + assert_eq!(true, data.len() > 0); } ); - test.run(); + client.stream().connect(stream.clone()).ok().unwrap(); + // Wait a bit to test if stream callback will panic + std::thread::sleep(std::time::Duration::from_millis(1 * 1000)); } #[rstest] -#[case::input(Direction::Input)] -#[case::output(Direction::Output)] #[serial] -fn connect( - client: &PipewireClient, +fn connect_input( + client: PipewireTestClient, default_input_node: NodeInfo, default_output_node: NodeInfo, - #[case] direction: Direction ) { - let mut test = StreamTest::new( - || { - internal_create( - &client, - match direction { - Direction::Input => default_input_node.clone(), - Direction::Output => default_output_node.clone() - }, - direction.clone(), - move |mut buffer| { - let data = buffer.datas_mut(); - let data = &mut data[0]; - let data = data.data().unwrap(); - assert_eq!(true, data.len() > 0); - } - ) - }, - |stream| { - client.stream().connect(stream.clone()).ok().unwrap(); - // Wait a bit to test if stream callback will panic - std::thread::sleep(std::time::Duration::from_millis(1 * 1000)); - }, - |stream| { - internal_delete(&client, stream); - } - ); - test.run(); + let direction = Direction::Input; + abstract_connect(&client, default_input_node, default_output_node, direction); } #[rstest] -#[case::input(Direction::Input)] -#[case::output(Direction::Output)] #[serial] -fn disconnect( - client: &PipewireClient, +fn connect_output( + client: PipewireTestClient, default_input_node: NodeInfo, default_output_node: NodeInfo, - #[case] direction: Direction ) { - let mut test = StreamTest::new( - || { - internal_create_connected( - &client, - match direction { - Direction::Input => default_input_node.clone(), - Direction::Output => default_output_node.clone() - }, - direction.clone(), - move |_| { - assert!(true); - } - ) - }, - |stream| { - client.stream().disconnect(stream.clone()).unwrap(); + let direction = Direction::Output; + abstract_connect(&client, default_input_node, default_output_node, direction); +} + +fn abstract_disconnect( + client: &PipewireTestClient, + default_input_node: NodeInfo, + default_output_node: NodeInfo, + direction: Direction +) { + let stream = internal_create_connected( + &client, + match direction { + Direction::Input => default_input_node.clone(), + Direction::Output => default_output_node.clone() }, - |stream| { - internal_delete(&client, stream); + direction.clone(), + move |_| { + assert!(true); } ); - test.run(); + client.stream().disconnect(stream.clone()).unwrap(); +} + +#[rstest] +#[serial] +fn disconnect_input( + client: PipewireTestClient, + default_input_node: NodeInfo, + default_output_node: NodeInfo, +) { + let direction = Direction::Input; + abstract_disconnect(&client, default_input_node, default_output_node, direction); +} + +#[rstest] +#[serial] +fn disconnect_output( + client: PipewireTestClient, + default_input_node: NodeInfo, + default_output_node: NodeInfo, +) { + let direction = Direction::Output; + abstract_disconnect(&client, default_input_node, default_output_node, direction); } \ No newline at end of file diff --git a/pipewire-client/src/client/implementation.rs b/pipewire-client/src/client/implementation.rs index 27f2f4e2f..b51cbe5ea 100644 --- a/pipewire-client/src/client/implementation.rs +++ b/pipewire-client/src/client/implementation.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use std::thread; use std::thread::JoinHandle; -pub(super) static CLIENT_NAME_PREFIX: &str = "cpal-client"; +pub(super) static CLIENT_NAME_PREFIX: &str = "pipewire-client"; pub(super) static CLIENT_INDEX: AtomicU32 = AtomicU32::new(0); pub struct PipewireClient { @@ -88,9 +88,16 @@ impl PipewireClient { let response = self.internal_api.wait_response_with_timeout(timeout_duration); let response = match response { Ok(value) => value, - Err(_) => { + Err(value) => { // Timeout is certainly due to missing session manager - return self.core_api.check_session_manager_registered(); + // We need to check if that the case. If session manager is running then we return + // timeout error. + return match self.core_api.check_session_manager_registered() { + Ok(_) => Err(Error { + description: value.to_string(), + }), + Err(value) => Err(value) + }; } }; match response { diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs index fa47794d5..64dbb110b 100644 --- a/pipewire-client/src/client/implementation_test.rs +++ b/pipewire-client/src/client/implementation_test.rs @@ -1,324 +1,42 @@ -use crate::client::implementation::CLIENT_INDEX; -use crate::constants::*; +use crate::client::implementation::{CLIENT_INDEX, CLIENT_NAME_PREFIX}; use crate::PipewireClient; -use docker_api::models::ImageBuildChunk; -use docker_api::opts::ImageBuildOpts; -use docker_api::Docker; -use futures::StreamExt; -use pipewire::spa::utils::dict::ParsableValue; -use rstest::{fixture, rstest}; +use rstest::rstest; use serial_test::serial; -use std::path::{Path, PathBuf}; use std::sync::atomic::Ordering; -use testcontainers::core::{CmdWaitFor, ExecCommand, Mount}; -use testcontainers::runners::AsyncRunner; -use testcontainers::{ContainerAsync, GenericImage, Image, ImageExt}; -use tokio::io::AsyncReadExt; -use uuid::Uuid; - -struct Container { - name: String, - tag: String, - container_file_path: PathBuf, - container: Option>, - socket_id: Uuid, - pipewire_pid: Option, - wireplumber_pid: Option, - pulse_pid: Option, -} - -impl Container { - pub fn new( - name: String, - container_file_path: PathBuf, - ) -> Self { - Self { - name, - tag: "latest".to_string(), - container_file_path, - container: None, - socket_id: Uuid::new_v4(), - pipewire_pid: None, - wireplumber_pid: None, - pulse_pid: None, - } - } - - pub fn socket_location(&self) -> PathBuf { - Path::new("/run/pipewire-sockets").join(self.socket_id.to_string()).to_path_buf() - } - - pub fn socket_name(&self) -> String { - format!("{}", self.socket_id) - } - - pub fn build(&self) { - const DOCKER_HOST_ENVIRONMENT_KEY: &str = "DOCKER_HOST"; - const CONTAINER_HOST_ENVIRONMENT_KEY: &str = "CONTAINER_HOST"; - let docker_host = std::env::var(DOCKER_HOST_ENVIRONMENT_KEY); - let container_host = std::env::var(CONTAINER_HOST_ENVIRONMENT_KEY); - let uri = match (docker_host, container_host) { - (Ok(value), Ok(_)) => value, - (Ok(value), Err(_)) => value, - (Err(_), Ok(value)) => { - // TestContainer does not recognize CONTAINER_HOST. - // Instead, with set DOCKET_HOST env var with the same value - std::env::set_var(DOCKER_HOST_ENVIRONMENT_KEY, value.clone()); - value - }, - (Err(_), Err(_)) => panic!( - "${} or ${} should be set.", - DOCKER_HOST_ENVIRONMENT_KEY, CONTAINER_HOST_ENVIRONMENT_KEY - ), - }; - let api = Docker::new(uri).unwrap(); - let images = api.images(); - let build_image_options= ImageBuildOpts::builder(self.container_file_path.parent().unwrap().to_str().unwrap()) - .tag(format!("{}:{}", self.name, self.tag)) - .dockerfile(self.container_file_path.file_name().unwrap().to_str().unwrap()) - .build(); - let mut stream = images.build(&build_image_options); - let runtime = tokio::runtime::Runtime::new().unwrap(); - while let Some(build_result) = runtime.block_on(stream.next()) { - match build_result { - Ok(output) => { - let output = match output { - ImageBuildChunk::Update { stream } => stream, - ImageBuildChunk::Error { error, error_detail } => { - panic!("Error {}: {}", error, error_detail.message); - } - ImageBuildChunk::Digest { aux } => aux.id, - ImageBuildChunk::PullStatus { .. } => { - return - } - }; - print!("{}", output); - }, - Err(e) => panic!("Error: {e}"), - } - } - } - - pub fn run(&mut self) { - let socket_location = self.socket_location(); - let socket_name = self.socket_name(); - let container = GenericImage::new(self.name.clone(), self.tag.clone()) - .with_env_var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, socket_location.to_str().unwrap()) - .with_env_var(PIPEWIRE_CORE_ENVIRONMENT_KEY, socket_name.clone()) - .with_env_var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, socket_name.clone()) - .with_env_var("PULSE_RUNTIME_PATH", socket_location.join("pulse").to_str().unwrap()) - .with_mount(Mount::volume_mount( - "pipewire-sockets", - socket_location.parent().unwrap().to_str().unwrap(), - )); - let runtime = tokio::runtime::Runtime::new().unwrap(); - let container = runtime.block_on(container.start()).unwrap(); - self.container = Some(container); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "mkdir", - "--parent", - socket_location.to_str().unwrap(), - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); - self.start_pipewire(); - } - - fn run_process(&mut self, process_name: &str) -> u32 { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - process_name - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); - let mut result = runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "pidof", - process_name, - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); - let mut pid = String::new(); - runtime.block_on(result.stdout().read_to_string(&mut pid)).unwrap(); - pid = pid.trim_end().to_string(); - u32::parse_value(pid.as_str()).unwrap() - } - - fn kill_process(&mut self, process_id: u32) { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "kill", - "-s", "SIGKILL", - format!("{}", process_id).as_str() - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); - } - - fn start_pipewire(&mut self) { - let pid = self.run_process("pipewire"); - self.pipewire_pid = Some(pid); - } - - fn stop_pipewire(&mut self) { - self.kill_process(self.pipewire_pid.unwrap()) - } - - pub fn start_wireplumber(&mut self) { - let pid = self.run_process("wireplumber"); - self.wireplumber_pid = Some(pid); - } - - pub fn stop_wireplumber(&mut self) { - if self.wireplumber_pid.is_none() { - return; - } - self.kill_process(self.wireplumber_pid.unwrap()); - } - - pub fn start_pulse(&mut self) { - let pid = self.run_process("pipewire-pulse"); - self.pulse_pid = Some(pid); - } - - pub fn stop_pulse(&mut self) { - if self.pulse_pid.is_none() { - return; - } - self.kill_process(self.pulse_pid.unwrap()); - } - - pub fn load_null_sink_module(&self) { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "pactl", - "load-module", - "module-null-sink" - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); - } - - pub fn create_virtual_node(&self) { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "pw-cli", - "create-node", - "adapter", - "'{ factory.name=support.null-audio-sink node.name=test-sink media.class=Audio/Sink object.linger=true audio.position=[FL FR] }'" - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); - } -} - -impl Drop for Container { - fn drop(&mut self) { - if self.container.is_none() { - return; - } - self.stop_pulse(); - self.stop_wireplumber(); - self.stop_pipewire(); - let socket_location = self.socket_location(); - let container = self.container.take().unwrap(); - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(container.exec( - ExecCommand::new(vec![ - "rm", - "--force", - "--recursive", - socket_location.join("*").to_str().unwrap(), - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); - runtime.block_on(container.stop()).unwrap(); - runtime.block_on(container.rm()).unwrap(); - } -} - -#[fixture] -fn pipewire_server_with_default_configuration() -> Container { - let mut container = Container::new( - "pipewire-default".to_string(), - PathBuf::from(".containers/pipewire.test.container"), - ); - container.build(); - container.run(); - container.start_wireplumber(); - container.start_pulse(); - container.load_null_sink_module(); - container.create_virtual_node(); - container -} - -#[fixture] -fn pipewire_server_without_session_manager() -> Container { - let mut container = Container::new( - "pipewire-without-session-manager".to_string(), - PathBuf::from(".containers/pipewire.test.container"), - ); - container.build(); - container.run(); - container -} - -#[fixture] -fn pipewire_server_without_node() -> Container { - let mut container = Container::new( - "pipewire-without-node".to_string(), - PathBuf::from(".containers/pipewire.test.container"), - ); - container.build(); - container.run(); - container.start_wireplumber(); - container -} - -fn set_socket_env_vars(server: &Container) { - std::env::set_var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, server.socket_location()); - std::env::set_var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, server.socket_name()); -} - -#[rstest] -#[serial] -pub fn initialization() { - let _ = PipewireClient::new().unwrap(); -} +use crate::test_utils::fixtures::{client, client2, PipewireTestClient}; +use crate::test_utils::server::{server_with_default_configuration, server_without_node, server_without_session_manager, set_socket_env_vars, Container}; #[rstest] #[serial] -pub fn name() { - let client_1 = PipewireClient::new().unwrap(); - assert_eq!(format!("cpal-client-{}", CLIENT_INDEX.load(Ordering::SeqCst) - 1), client_1.name); - let client_2 = PipewireClient::new().unwrap(); - assert_eq!(format!("cpal-client-{}", CLIENT_INDEX.load(Ordering::SeqCst) - 1), client_2.name); +pub fn names( + #[from(client2)] (client_1, client_2): (PipewireTestClient, PipewireTestClient) +) { + let client_1_index = client_1.name.replace(format!("{}-", CLIENT_NAME_PREFIX).as_str(), "") + .parse::() + .unwrap(); + assert_eq!(format!("{}-{}", CLIENT_NAME_PREFIX, client_1_index), client_1.name); + assert_eq!(format!("{}-{}", CLIENT_NAME_PREFIX, client_1_index + 1), client_2.name); } #[rstest] #[serial] -pub fn server_with_default_configuration(pipewire_server_with_default_configuration: Container) { - set_socket_env_vars(&pipewire_server_with_default_configuration); +pub fn with_default_configuration(server_with_default_configuration: Container) { + set_socket_env_vars(&server_with_default_configuration); let _ = PipewireClient::new().unwrap(); } #[rstest] #[serial] -pub fn server_without_session_manager(pipewire_server_without_session_manager: Container) { - set_socket_env_vars(&pipewire_server_without_session_manager); +pub fn without_session_manager(server_without_session_manager: Container) { + set_socket_env_vars(&server_without_session_manager); let error = PipewireClient::new().unwrap_err(); assert_eq!(true, error.description.contains("No session manager registered")) } #[rstest] #[serial] -pub fn server_without_node(pipewire_server_without_node: Container) { - set_socket_env_vars(&pipewire_server_without_node); +pub fn without_node(server_without_node: Container) { + set_socket_env_vars(&server_without_node); let error = PipewireClient::new().unwrap_err(); assert_eq!("Post initialization error: Zero node registered", error.description) } \ No newline at end of file diff --git a/pipewire-client/src/lib.rs b/pipewire-client/src/lib.rs index b2b33367d..e38d942ba 100644 --- a/pipewire-client/src/lib.rs +++ b/pipewire-client/src/lib.rs @@ -12,6 +12,10 @@ pub use utils::Direction; mod error; mod info; + +#[cfg(test)] +mod test_utils; + pub use info::AudioStreamInfo; pub use info::NodeInfo; diff --git a/pipewire-client/src/states.rs b/pipewire-client/src/states.rs index 52990221c..4ece63109 100644 --- a/pipewire-client/src/states.rs +++ b/pipewire-client/src/states.rs @@ -740,6 +740,9 @@ impl DefaultAudioNodesState { move |_: u32, key: Option<&str>, _: Option<&str>, value: Option<&str>| { let default_audio_devices = &mut state.borrow_mut().default_audio_nodes; let key = key.unwrap(); + if value.is_none() { + return 0; + } let value = value.unwrap(); match key { DEFAULT_AUDIO_SINK_PROPERTY_KEY => { diff --git a/pipewire-client/src/test_utils/fixtures.rs b/pipewire-client/src/test_utils/fixtures.rs new file mode 100644 index 000000000..eae8acd22 --- /dev/null +++ b/pipewire-client/src/test_utils/fixtures.rs @@ -0,0 +1,83 @@ +use std::cell::RefCell; +use std::ops::Deref; +use std::rc::Rc; +use crate::{Direction, NodeInfo, PipewireClient}; +use rstest::fixture; +use crate::test_utils::server::{server_with_default_configuration, set_socket_env_vars, Container}; + +pub struct PipewireTestClient { + server: Rc>, + client: PipewireClient, +} + +impl PipewireTestClient { + pub(self) fn new(server: Rc>, client: PipewireClient) -> Self { + Self { + server, + client, + } + } +} + +impl Deref for PipewireTestClient { + type Target = PipewireClient; + + fn deref(&self) -> &Self::Target { + &self.client + } +} + +#[fixture] +pub fn client(server_with_default_configuration: Container) -> PipewireTestClient { + set_socket_env_vars(&server_with_default_configuration); + PipewireTestClient::new( + Rc::new(RefCell::new(server_with_default_configuration)), + PipewireClient::new().unwrap() + ) +} + +#[fixture] +pub fn client2(server_with_default_configuration: Container) -> (PipewireTestClient, PipewireTestClient) { + set_socket_env_vars(&server_with_default_configuration); + let server = Rc::new(RefCell::new(server_with_default_configuration)); + let client_1 = PipewireClient::new().unwrap(); + let client_2 = PipewireClient::new().unwrap(); + ( + PipewireTestClient::new( + server.clone(), + client_1 + ), + PipewireTestClient::new( + server.clone(), + client_2 + ) + ) +} + +#[fixture] +pub fn input_nodes(client: PipewireTestClient) -> Vec { + client.node().enumerate(Direction::Input).unwrap() +} + +#[fixture] +pub fn output_nodes(client: PipewireTestClient) -> Vec { + client.node().enumerate(Direction::Output).unwrap() +} + +#[fixture] +pub fn default_input_node(input_nodes: Vec) -> NodeInfo { + input_nodes.iter() + .filter(|node| node.is_default) + .last() + .cloned() + .unwrap() +} + +#[fixture] +pub fn default_output_node(output_nodes: Vec) -> NodeInfo { + output_nodes.iter() + .filter(|node| node.is_default) + .last() + .cloned() + .unwrap() +} \ No newline at end of file diff --git a/pipewire-client/src/test_utils/mod.rs b/pipewire-client/src/test_utils/mod.rs new file mode 100644 index 000000000..27aeb978c --- /dev/null +++ b/pipewire-client/src/test_utils/mod.rs @@ -0,0 +1,2 @@ +pub mod fixtures; +pub mod server; \ No newline at end of file diff --git a/pipewire-client/src/test_utils/server.rs b/pipewire-client/src/test_utils/server.rs new file mode 100644 index 000000000..5868e1330 --- /dev/null +++ b/pipewire-client/src/test_utils/server.rs @@ -0,0 +1,312 @@ +use std::path::{Path, PathBuf}; +use docker_api::Docker; +use docker_api::models::ImageBuildChunk; +use docker_api::opts::ImageBuildOpts; +use futures::StreamExt; +use pipewire::spa::utils::dict::ParsableValue; +use rstest::fixture; +use testcontainers::{ContainerAsync, GenericImage, ImageExt}; +use testcontainers::core::{CmdWaitFor, ExecCommand, Mount}; +use testcontainers::runners::AsyncRunner; +use tokio::io::AsyncReadExt; +use uuid::Uuid; +use crate::constants::{PIPEWIRE_CORE_ENVIRONMENT_KEY, PIPEWIRE_REMOTE_ENVIRONMENT_KEY, PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY}; + +pub struct Container { + name: String, + tag: String, + container_file_path: PathBuf, + container: Option>, + socket_id: Uuid, + pipewire_pid: Option, + wireplumber_pid: Option, + pulse_pid: Option, +} + +impl Container { + pub fn new( + name: String, + container_file_path: PathBuf, + ) -> Self { + Self { + name, + tag: "latest".to_string(), + container_file_path, + container: None, + socket_id: Uuid::new_v4(), + pipewire_pid: None, + wireplumber_pid: None, + pulse_pid: None, + } + } + + fn socket_location(&self) -> PathBuf { + Path::new("/run/pipewire-sockets").join(self.socket_id.to_string()).to_path_buf() + } + + fn socket_name(&self) -> String { + format!("{}", self.socket_id) + } + + fn build(&self) { + const DOCKER_HOST_ENVIRONMENT_KEY: &str = "DOCKER_HOST"; + const CONTAINER_HOST_ENVIRONMENT_KEY: &str = "CONTAINER_HOST"; + let docker_host = std::env::var(DOCKER_HOST_ENVIRONMENT_KEY); + let container_host = std::env::var(CONTAINER_HOST_ENVIRONMENT_KEY); + let uri = match (docker_host, container_host) { + (Ok(value), Ok(_)) => value, + (Ok(value), Err(_)) => value, + (Err(_), Ok(value)) => { + // TestContainer does not recognize CONTAINER_HOST. + // Instead, with set DOCKET_HOST env var with the same value + std::env::set_var(DOCKER_HOST_ENVIRONMENT_KEY, value.clone()); + value + }, + (Err(_), Err(_)) => panic!( + "${} or ${} should be set.", + DOCKER_HOST_ENVIRONMENT_KEY, CONTAINER_HOST_ENVIRONMENT_KEY + ), + }; + let api = Docker::new(uri).unwrap(); + let images = api.images(); + let build_image_options= ImageBuildOpts::builder(self.container_file_path.parent().unwrap().to_str().unwrap()) + .tag(format!("{}:{}", self.name, self.tag)) + .dockerfile(self.container_file_path.file_name().unwrap().to_str().unwrap()) + .build(); + let mut stream = images.build(&build_image_options); + let runtime = tokio::runtime::Runtime::new().unwrap(); + while let Some(build_result) = runtime.block_on(stream.next()) { + match build_result { + Ok(output) => { + let output = match output { + ImageBuildChunk::Update { stream } => stream, + ImageBuildChunk::Error { error, error_detail } => { + panic!("Error {}: {}", error, error_detail.message); + } + ImageBuildChunk::Digest { aux } => aux.id, + ImageBuildChunk::PullStatus { .. } => { + return + } + }; + print!("{}", output); + }, + Err(e) => panic!("Error: {e}"), + } + } + } + + fn run(&mut self) { + let socket_location = self.socket_location(); + let socket_name = self.socket_name(); + let container = GenericImage::new(self.name.clone(), self.tag.clone()) + .with_env_var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, socket_location.to_str().unwrap()) + .with_env_var(PIPEWIRE_CORE_ENVIRONMENT_KEY, socket_name.clone()) + .with_env_var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, socket_name.clone()) + .with_env_var("PULSE_RUNTIME_PATH", socket_location.join("pulse").to_str().unwrap()) + .with_mount(Mount::volume_mount( + "pipewire-sockets", + socket_location.parent().unwrap().to_str().unwrap(), + )); + let runtime = tokio::runtime::Runtime::new().unwrap(); + let container = runtime.block_on(container.start()).unwrap(); + self.container = Some(container); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "mkdir", + "--parent", + socket_location.to_str().unwrap(), + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + } + + fn run_process(&mut self, process_name: &str) -> u32 { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + process_name + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + let mut result = runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "pidof", + process_name, + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + let mut pid = String::new(); + runtime.block_on(result.stdout().read_to_string(&mut pid)).unwrap(); + pid = pid.trim_end().to_string(); + u32::parse_value(pid.as_str()).unwrap() + } + + fn kill_process(&mut self, process_id: u32) { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "kill", + "-s", "SIGKILL", + format!("{}", process_id).as_str() + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + } + + fn start_pipewire(&mut self) { + let pid = self.run_process("pipewire"); + self.pipewire_pid = Some(pid); + } + + fn stop_pipewire(&mut self) { + self.kill_process(self.pipewire_pid.unwrap()) + } + + fn start_wireplumber(&mut self) { + let pid = self.run_process("wireplumber"); + self.wireplumber_pid = Some(pid); + } + + fn stop_wireplumber(&mut self) { + if self.wireplumber_pid.is_none() { + return; + } + self.kill_process(self.wireplumber_pid.unwrap()); + } + + fn start_pulse(&mut self) { + let pid = self.run_process("pipewire-pulse"); + self.pulse_pid = Some(pid); + } + + fn stop_pulse(&mut self) { + if self.pulse_pid.is_none() { + return; + } + self.kill_process(self.pulse_pid.unwrap()); + } + + fn load_null_sink_module(&self) { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "pactl", + "load-module", + "module-null-sink" + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + } + + fn set_virtual_nodes_configuration(&self) { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "mkdir", + "--parent", + "/etc/pipewire/pipewire.conf.d/", + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "cp", + "/root/pipewire.nodes.conf", + "/etc/pipewire/pipewire.conf.d/pipewire.nodes.conf", + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + } + + fn set_default_nodes(&self) { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "pactl", + "set-default-sink", + "test-sink", + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + runtime.block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(vec![ + "pactl", + "set-default-source", + "test-source", + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + } +} + +impl Drop for Container { + fn drop(&mut self) { + if self.container.is_none() { + return; + } + self.stop_pulse(); + self.stop_wireplumber(); + self.stop_pipewire(); + let socket_location = self.socket_location(); + let container = self.container.take().unwrap(); + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(container.exec( + ExecCommand::new(vec![ + "rm", + "--force", + "--recursive", + socket_location.join("*").to_str().unwrap(), + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )).unwrap(); + runtime.block_on(container.stop()).unwrap(); + runtime.block_on(container.rm()).unwrap(); + } +} + +#[fixture] +pub fn server_with_default_configuration() -> Container { + let mut container = Container::new( + "pipewire-default".to_string(), + PathBuf::from(".containers/pipewire.test.container"), + ); + container.build(); + container.run(); + container.set_virtual_nodes_configuration(); + container.start_pipewire(); + container.start_wireplumber(); + container.start_pulse(); + container.set_default_nodes(); + //container.load_null_sink_module(); + container +} + +#[fixture] +pub fn server_without_session_manager() -> Container { + let mut container = Container::new( + "pipewire-without-session-manager".to_string(), + PathBuf::from(".containers/pipewire.test.container"), + ); + container.build(); + container.run(); + container.start_pipewire(); + container +} + +#[fixture] +pub fn server_without_node() -> Container { + let mut container = Container::new( + "pipewire-without-node".to_string(), + PathBuf::from(".containers/pipewire.test.container"), + ); + container.build(); + container.run(); + container.start_pipewire(); + container.start_wireplumber(); + container +} + +pub fn set_socket_env_vars(server: &Container) { + std::env::set_var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, server.socket_location()); + std::env::set_var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, server.socket_name()); +} \ No newline at end of file From e6838d3e1973638374b9b7753c7c4a669a7fb4bb Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Sun, 19 Jan 2025 22:29:03 +0000 Subject: [PATCH 08/17] Optimize imports --- pipewire-client/src/client/api/core_test.rs | 2 +- pipewire-client/src/client/api/mod.rs | 2 +- pipewire-client/src/client/api/node_test.rs | 4 ++-- pipewire-client/src/client/api/stream_test.rs | 4 ++-- pipewire-client/src/client/implementation_test.rs | 7 +++---- pipewire-client/src/test_utils/fixtures.rs | 6 +++--- pipewire-client/src/test_utils/server.rs | 8 ++++---- 7 files changed, 16 insertions(+), 17 deletions(-) diff --git a/pipewire-client/src/client/api/core_test.rs b/pipewire-client/src/client/api/core_test.rs index 2c230be23..2a6612257 100644 --- a/pipewire-client/src/client/api/core_test.rs +++ b/pipewire-client/src/client/api/core_test.rs @@ -1,6 +1,6 @@ +use crate::test_utils::fixtures::{client, PipewireTestClient}; use rstest::rstest; use serial_test::serial; -use crate::test_utils::fixtures::{client, PipewireTestClient}; #[rstest] #[serial] diff --git a/pipewire-client/src/client/api/mod.rs b/pipewire-client/src/client/api/mod.rs index 57c4db349..9989d8358 100644 --- a/pipewire-client/src/client/api/mod.rs +++ b/pipewire-client/src/client/api/mod.rs @@ -17,4 +17,4 @@ pub(super) use stream::StreamApi; mod stream_test; mod internal; -pub(super) use internal::InternalApi; \ No newline at end of file +pub(super) use internal::InternalApi; diff --git a/pipewire-client/src/client/api/node_test.rs b/pipewire-client/src/client/api/node_test.rs index 36b098ce1..818411901 100644 --- a/pipewire-client/src/client/api/node_test.rs +++ b/pipewire-client/src/client/api/node_test.rs @@ -1,8 +1,8 @@ -use crate::{Direction}; use crate::test_utils::fixtures::client; +use crate::test_utils::fixtures::PipewireTestClient; +use crate::Direction; use rstest::rstest; use serial_test::serial; -use crate::test_utils::fixtures::PipewireTestClient; fn internal_enumerate(client: &PipewireTestClient, direction: Direction) { let nodes = client.node().enumerate(direction).unwrap(); diff --git a/pipewire-client/src/client/api/stream_test.rs b/pipewire-client/src/client/api/stream_test.rs index b35c88477..fe5f0cf6b 100644 --- a/pipewire-client/src/client/api/stream_test.rs +++ b/pipewire-client/src/client/api/stream_test.rs @@ -1,10 +1,10 @@ -use crate::{Direction, NodeInfo}; use crate::test_utils::fixtures::client; use crate::test_utils::fixtures::default_input_node; use crate::test_utils::fixtures::default_output_node; +use crate::test_utils::fixtures::PipewireTestClient; +use crate::{Direction, NodeInfo}; use rstest::rstest; use serial_test::serial; -use crate::test_utils::fixtures::PipewireTestClient; fn internal_create( client: &PipewireTestClient, diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs index 64dbb110b..bd3e268bb 100644 --- a/pipewire-client/src/client/implementation_test.rs +++ b/pipewire-client/src/client/implementation_test.rs @@ -1,10 +1,9 @@ -use crate::client::implementation::{CLIENT_INDEX, CLIENT_NAME_PREFIX}; +use crate::client::implementation::CLIENT_NAME_PREFIX; +use crate::test_utils::fixtures::{client2, PipewireTestClient}; +use crate::test_utils::server::{server_with_default_configuration, server_without_node, server_without_session_manager, set_socket_env_vars, Container}; use crate::PipewireClient; use rstest::rstest; use serial_test::serial; -use std::sync::atomic::Ordering; -use crate::test_utils::fixtures::{client, client2, PipewireTestClient}; -use crate::test_utils::server::{server_with_default_configuration, server_without_node, server_without_session_manager, set_socket_env_vars, Container}; #[rstest] #[serial] diff --git a/pipewire-client/src/test_utils/fixtures.rs b/pipewire-client/src/test_utils/fixtures.rs index eae8acd22..929f6c7b0 100644 --- a/pipewire-client/src/test_utils/fixtures.rs +++ b/pipewire-client/src/test_utils/fixtures.rs @@ -1,9 +1,9 @@ +use crate::test_utils::server::{server_with_default_configuration, set_socket_env_vars, Container}; +use crate::{Direction, NodeInfo, PipewireClient}; +use rstest::fixture; use std::cell::RefCell; use std::ops::Deref; use std::rc::Rc; -use crate::{Direction, NodeInfo, PipewireClient}; -use rstest::fixture; -use crate::test_utils::server::{server_with_default_configuration, set_socket_env_vars, Container}; pub struct PipewireTestClient { server: Rc>, diff --git a/pipewire-client/src/test_utils/server.rs b/pipewire-client/src/test_utils/server.rs index 5868e1330..cf2e973ad 100644 --- a/pipewire-client/src/test_utils/server.rs +++ b/pipewire-client/src/test_utils/server.rs @@ -1,16 +1,16 @@ -use std::path::{Path, PathBuf}; -use docker_api::Docker; +use crate::constants::{PIPEWIRE_CORE_ENVIRONMENT_KEY, PIPEWIRE_REMOTE_ENVIRONMENT_KEY, PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY}; use docker_api::models::ImageBuildChunk; use docker_api::opts::ImageBuildOpts; +use docker_api::Docker; use futures::StreamExt; use pipewire::spa::utils::dict::ParsableValue; use rstest::fixture; -use testcontainers::{ContainerAsync, GenericImage, ImageExt}; +use std::path::{Path, PathBuf}; use testcontainers::core::{CmdWaitFor, ExecCommand, Mount}; use testcontainers::runners::AsyncRunner; +use testcontainers::{ContainerAsync, GenericImage, ImageExt}; use tokio::io::AsyncReadExt; use uuid::Uuid; -use crate::constants::{PIPEWIRE_CORE_ENVIRONMENT_KEY, PIPEWIRE_REMOTE_ENVIRONMENT_KEY, PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY}; pub struct Container { name: String, From 2b7258acf12743627cc647e5ea4ddfad58d31fdc Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Mon, 20 Jan 2025 13:23:22 +0000 Subject: [PATCH 09/17] Replace listener trigger policy by control flow object. That give more control when to release a listener instance. --- pipewire-client/src/client/api/stream.rs | 3 +- pipewire-client/src/client/api/stream_test.rs | 22 ++-- pipewire-client/src/client/handlers/event.rs | 13 +- .../src/client/handlers/request.rs | 28 ++--- pipewire-client/src/client/handlers/thread.rs | 4 +- pipewire-client/src/listeners.rs | 37 ++++-- pipewire-client/src/messages.rs | 9 +- pipewire-client/src/states.rs | 112 +++++++++++------- pipewire-client/src/utils.rs | 19 +-- 9 files changed, 147 insertions(+), 100 deletions(-) diff --git a/pipewire-client/src/client/api/stream.rs b/pipewire-client/src/client/api/stream.rs index f3be8d22e..543ba4ffb 100644 --- a/pipewire-client/src/client/api/stream.rs +++ b/pipewire-client/src/client/api/stream.rs @@ -4,6 +4,7 @@ use crate::messages::{MessageRequest, MessageResponse, StreamCallback}; use crate::states::GlobalId; use crate::{AudioStreamInfo, Direction}; use std::sync::Arc; +use crate::listeners::ListenerControlFlow; pub struct StreamApi { api: Arc, @@ -24,7 +25,7 @@ impl StreamApi { callback: F, ) -> Result where - F: FnMut(pipewire::buffer::Buffer) + Send + 'static + F: FnMut(&mut ListenerControlFlow, pipewire::buffer::Buffer) + Send + 'static { let request = MessageRequest::CreateStream { node_id: GlobalId::from(node_id), diff --git a/pipewire-client/src/client/api/stream_test.rs b/pipewire-client/src/client/api/stream_test.rs index fe5f0cf6b..a9066cdf5 100644 --- a/pipewire-client/src/client/api/stream_test.rs +++ b/pipewire-client/src/client/api/stream_test.rs @@ -5,13 +5,16 @@ use crate::test_utils::fixtures::PipewireTestClient; use crate::{Direction, NodeInfo}; use rstest::rstest; use serial_test::serial; +use crate::listeners::ListenerControlFlow; -fn internal_create( +fn internal_create( client: &PipewireTestClient, node: NodeInfo, direction: Direction, callback: F, -) -> String { +) -> String where + F: FnMut(&mut ListenerControlFlow, pipewire::buffer::Buffer) + Send + 'static +{ client.stream() .create( node.id, @@ -31,12 +34,14 @@ fn internal_delete( .unwrap() } -fn internal_create_connected( +fn internal_create_connected( client: &PipewireTestClient, node: NodeInfo, direction: Direction, callback: F, -) -> String { +) -> String where + F: FnMut(&mut ListenerControlFlow, pipewire::buffer::Buffer) + Send + 'static +{ let stream = client.stream() .create( node.id, @@ -62,8 +67,9 @@ fn abstract_create( Direction::Output => default_output_node.clone() }, direction.clone(), - move |_| { + move |control_flow, _| { assert!(true); + control_flow.release(); } ); match direction { @@ -132,11 +138,12 @@ fn abstract_connect( Direction::Output => default_output_node.clone() }, direction.clone(), - move |mut buffer| { + move |control_flow, mut buffer| { let data = buffer.datas_mut(); let data = &mut data[0]; let data = data.data().unwrap(); assert_eq!(true, data.len() > 0); + control_flow.release(); } ); client.stream().connect(stream.clone()).ok().unwrap(); @@ -179,8 +186,9 @@ fn abstract_disconnect( Direction::Output => default_output_node.clone() }, direction.clone(), - move |_| { + move |control_flow, _| { assert!(true); + control_flow.release(); } ); client.stream().disconnect(stream.clone()).unwrap(); diff --git a/pipewire-client/src/client/handlers/event.rs b/pipewire-client/src/client/handlers/event.rs index a2f645a40..d6e5c7ed4 100644 --- a/pipewire-client/src/client/handlers/event.rs +++ b/pipewire-client/src/client/handlers/event.rs @@ -1,6 +1,5 @@ use crate::constants::{METADATA_NAME_PROPERTY_VALUE_DEFAULT, METADATA_NAME_PROPERTY_VALUE_SETTINGS}; use crate::error::Error; -use crate::listeners::ListenerTriggerPolicy; use crate::messages::{EventMessage, MessageResponse}; use crate::states::{DefaultAudioNodesState, GlobalId, GlobalState, SettingsState}; use pipewire_spa_utils::audio::raw::AudioInfoRaw; @@ -76,13 +75,11 @@ fn handle_set_metadata_listeners( match metadata.name.as_str() { METADATA_NAME_PROPERTY_VALUE_SETTINGS => { metadata.add_property_listener( - ListenerTriggerPolicy::Keep, SettingsState::listener(listener_state) ) }, METADATA_NAME_PROPERTY_VALUE_DEFAULT => { metadata.add_property_listener( - ListenerTriggerPolicy::Keep, DefaultAudioNodesState::listener(listener_state) ) }, @@ -132,8 +129,7 @@ fn handle_set_node_properties_listener( }; let event_sender = event_sender.clone(); node.add_properties_listener( - ListenerTriggerPolicy::Keep, - move |properties| { + move |control_flow, properties| { // "object.register" property when set to "false", indicate we should not // register this object // Some bluez nodes don't have sample rate information in their @@ -167,6 +163,7 @@ fn handle_set_node_properties_listener( }) .unwrap(); } + control_flow.release(); } ); } @@ -190,8 +187,7 @@ fn handle_set_node_format_listener( let main_sender = main_sender.clone(); let event_sender = event_sender.clone(); node.add_format_listener( - ListenerTriggerPolicy::Keep, - move |format| { + move |control_flow, format| { match format { Ok(value) => { event_sender @@ -204,7 +200,8 @@ fn handle_set_node_format_listener( Err(value) => { main_sender.send(MessageResponse::Error(value)).unwrap(); } - } + }; + control_flow.release(); } ) } diff --git a/pipewire-client/src/client/handlers/request.rs b/pipewire-client/src/client/handlers/request.rs index 186c41615..54db74403 100644 --- a/pipewire-client/src/client/handlers/request.rs +++ b/pipewire-client/src/client/handlers/request.rs @@ -1,6 +1,5 @@ use crate::constants::*; use crate::error::Error; -use crate::listeners::ListenerTriggerPolicy; use crate::messages::{MessageRequest, MessageResponse, StreamCallback}; use crate::states::{GlobalId, GlobalObjectState, GlobalState, OrphanState, StreamState}; use crate::utils::PipewireCoreSync; @@ -216,9 +215,8 @@ fn handle_create_node( let listener_main_sender = main_sender.clone(); let listener_state = state.clone(); core_sync.register( - false, PIPEWIRE_CORE_SYNC_CREATE_DEVICE_SEQ, - move || { + move |control_flow| { let state = listener_state.borrow(); let nodes = match state.get_nodes() { Ok(value) => value, @@ -226,6 +224,7 @@ fn handle_create_node( listener_main_sender .send(MessageResponse::Error(value)) .unwrap(); + control_flow.release(); return; } }; @@ -239,14 +238,16 @@ fn handle_create_node( description: "Created node not found".to_string(), })) .unwrap(); - return; - }; - let node_id = node.unwrap().0; - listener_main_sender - .send(MessageResponse::CreateNode { - id: (*node_id).clone(), - }) - .unwrap(); + } + else { + let node_id = node.unwrap().0; + listener_main_sender + .send(MessageResponse::CreateNode { + id: (*node_id).clone(), + }) + .unwrap(); + } + control_flow.release(); } ); let mut state = state.borrow_mut(); @@ -383,10 +384,7 @@ fn handle_create_stream( direction.into(), stream ); - stream.add_process_listener( - ListenerTriggerPolicy::Keep, - callback - ); + stream.add_process_listener(callback); if let Err(value) = state.insert_stream(stream_name.clone(), stream) { main_sender .send(MessageResponse::Error(value)) diff --git a/pipewire-client/src/client/handlers/thread.rs b/pipewire-client/src/client/handlers/thread.rs index fee0fb207..9d1fed15f 100644 --- a/pipewire-client/src/client/handlers/thread.rs +++ b/pipewire-client/src/client/handlers/thread.rs @@ -89,12 +89,12 @@ pub fn pw_thread( let listener_main_sender = main_sender.clone(); core_sync.register( - false, PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ, - move || { + move |control_flow| { listener_main_sender .send(MessageResponse::Initialized) .unwrap(); + control_flow.release(); } ); diff --git a/pipewire-client/src/listeners.rs b/pipewire-client/src/listeners.rs index 9591a8ee7..253b598c6 100644 --- a/pipewire-client/src/listeners.rs +++ b/pipewire-client/src/listeners.rs @@ -1,24 +1,42 @@ use std::cell::RefCell; use std::collections::HashMap; +use std::ops::ControlFlow; use std::rc::Rc; -#[derive(Debug, Clone, PartialEq)] -pub(super) enum ListenerTriggerPolicy { - Keep, - Remove +pub(super) struct ListenerControlFlow { + is_released: bool +} + +impl ListenerControlFlow { + pub fn new() -> Self { + Self { + is_released: false, + } + } + + pub fn is_released(&self) -> bool { + self.is_released + } + + pub fn release(&mut self) { + if self.is_released { + return; + } + self.is_released = true; + } } pub(super) struct Listener { inner: T, - trigger_policy: ListenerTriggerPolicy, + control_flow: Rc>, } impl Listener { - pub fn new(inner: T, policy: ListenerTriggerPolicy) -> Self + pub fn new(inner: T, control_flow: Rc>) -> Self { Self { inner, - trigger_policy: policy, + control_flow, } } } @@ -42,8 +60,9 @@ impl Listeners { pub fn triggered(&mut self, name: &String) { let mut listeners = self.listeners.borrow_mut(); let listener = listeners.get_mut(name).unwrap(); - if listener.trigger_policy == ListenerTriggerPolicy::Remove { - listeners.remove(name); + if listener.control_flow.borrow().is_released == false { + return; } + listeners.remove(name); } } \ No newline at end of file diff --git a/pipewire-client/src/messages.rs b/pipewire-client/src/messages.rs index 2887ba12e..1bb00a424 100644 --- a/pipewire-client/src/messages.rs +++ b/pipewire-client/src/messages.rs @@ -6,21 +6,22 @@ use pipewire_spa_utils::audio::raw::AudioInfoRaw; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use std::sync::{Arc, Mutex}; +use crate::listeners::ListenerControlFlow; pub(super) struct StreamCallback { - callback: Arc>> + callback: Arc>> } -impl From for StreamCallback { +impl From for StreamCallback { fn from(value: F) -> Self { Self { callback: Arc::new(Mutex::new(Box::new(value))) } } } impl StreamCallback { - pub fn call(&mut self, buffer: pipewire::buffer::Buffer) { + pub fn call(&mut self, control_flow: &mut ListenerControlFlow, buffer: pipewire::buffer::Buffer) { let mut callback = self.callback.lock().unwrap(); - callback(buffer); + callback(control_flow, buffer); } } diff --git a/pipewire-client/src/states.rs b/pipewire-client/src/states.rs index 4ece63109..b3d718b2f 100644 --- a/pipewire-client/src/states.rs +++ b/pipewire-client/src/states.rs @@ -1,6 +1,6 @@ use super::constants::*; use crate::error::Error; -use crate::listeners::{Listener, ListenerTriggerPolicy, Listeners}; +use crate::listeners::{Listener, ListenerControlFlow, Listeners}; use crate::messages::StreamCallback; use crate::utils::dict_ref_to_hashmap; use crate::Direction; @@ -75,9 +75,9 @@ impl GlobalState { let index = std::ptr::addr_of!(state) as usize; let listener_orphans = self.orphans.clone(); state.add_removed_listener( - ListenerTriggerPolicy::Remove, - move || { + move |control_flow| { listener_orphans.borrow_mut().remove(&index); + control_flow.release() } ); self.orphans.borrow_mut().insert(index, state); @@ -234,21 +234,26 @@ impl OrphanState { } } - pub fn add_removed_listener(&mut self, policy: ListenerTriggerPolicy, callback: F) + pub fn add_removed_listener(&mut self, callback: F) where - F: Fn() + 'static + F: Fn(&mut ListenerControlFlow) + 'static { const LISTENER_NAME: &str = "removed"; let listeners = self.listeners.clone(); + let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); + let listener_control_flow = control_flow.clone(); let listener = self.proxy.add_listener_local() .removed(move || { - callback(); + if listener_control_flow.borrow().is_released() { + return; + } + callback(&mut listener_control_flow.borrow_mut()); listeners.borrow_mut().triggered(&LISTENER_NAME.to_string()); }) .register(); self.listeners.borrow_mut().add( LISTENER_NAME.to_string(), - Listener::new(listener, policy) + Listener::new(listener, control_flow) ); } } @@ -312,35 +317,39 @@ impl NodeState { self.properties.borrow().get(*pipewire::keys::NODE_NAME).unwrap().clone() } - fn add_info_listener(&mut self, name: String, policy: ListenerTriggerPolicy, listener: F) + fn add_info_listener(&mut self, name: String, listener: F) where - F: Fn(&pipewire::node::NodeInfoRef) + 'static + F: Fn(&mut ListenerControlFlow, &pipewire::node::NodeInfoRef) + 'static { let listeners = self.listeners.clone(); let listener_name = name.clone(); + let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); + let listener_control_flow = control_flow.clone(); let listener = self.proxy.add_listener_local() .info(move |info| { - listener(info); + if listener_control_flow.borrow().is_released() { + return; + } + listener(&mut listener_control_flow.borrow_mut(), info); listeners.borrow_mut().triggered(&listener_name); }) .register(); - self.listeners.borrow_mut().add(name, Listener::new(listener, policy)); + self.listeners.borrow_mut().add(name, Listener::new(listener, control_flow)); } - pub fn add_properties_listener(&mut self, policy: ListenerTriggerPolicy, callback: F) + pub fn add_properties_listener(&mut self, callback: F) where - F: Fn(HashMap) + 'static, + F: Fn(&mut ListenerControlFlow, HashMap) + 'static, { self.add_info_listener( "properties".to_string(), - policy, - move |info| { + move |control_flow, info| { if info.props().is_none() { return; } let properties = info.props().unwrap(); let properties = dict_ref_to_hashmap(properties); - callback(properties); + callback(control_flow, properties); } ); } @@ -349,39 +358,43 @@ impl NodeState { &mut self, name: String, expected_kind: pipewire::spa::param::ParamType, - policy: ListenerTriggerPolicy, listener: F ) where - F: Fn(u32, u32, &pipewire::spa::pod::Pod) + 'static, + F: Fn(&mut ListenerControlFlow, &pipewire::spa::pod::Pod) + 'static, { let listeners = self.listeners.clone(); let listener_name = name.clone(); + let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); + let listener_control_flow = control_flow.clone(); self.proxy.subscribe_params(&[expected_kind]); let listener = self.proxy.add_listener_local() - .param(move |_, kind, id, next_id, parameter| { + // parameters: seq, kind, id, next_id, parameter + .param(move |_, kind, _, _, parameter| { + if listener_control_flow.borrow().is_released() { + return; + } if kind != expected_kind { return; } let Some(parameter) = parameter else { return; }; - listener(id, next_id, parameter); + listener(&mut listener_control_flow.borrow_mut(), parameter); listeners.borrow_mut().triggered(&listener_name); }) .register(); - self.listeners.borrow_mut().add(name, Listener::new(listener, policy)); + self.listeners.borrow_mut().add(name, Listener::new(listener, control_flow)); } - pub fn add_format_listener(&mut self, policy: ListenerTriggerPolicy, callback: F) + pub fn add_format_listener(&mut self, callback: F) where - F: Fn(Result) + 'static, + F: Fn(&mut ListenerControlFlow, Result) + 'static, { self.add_parameter_listener( "format".to_string(), pipewire::spa::param::ParamType::EnumFormat, - policy, - move |_, _, parameter| { + move |control_flow, parameter| { let (media_type, media_subtype): (MediaType, MediaSubtype) = match pipewire::spa::param::format_utils::parse_format(parameter) { Ok((media_type, media_subtype)) => (media_type.0.into(), media_subtype.0.into()), @@ -423,7 +436,7 @@ impl NodeState { }, _ => return }; - callback(parameter); + callback(control_flow, parameter); } ); } @@ -458,22 +471,33 @@ impl MetadataState { } } - pub fn add_property_listener(&mut self, policy: ListenerTriggerPolicy, listener: F) + pub fn add_property_listener(&mut self, listener: F) where - F: Fn(u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + Sized + 'static + F: Fn(&mut ListenerControlFlow, u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + Sized + 'static { const LISTENER_NAME: &str = "property"; let listeners = self.listeners.clone(); + let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); + let listener_control_flow = control_flow.clone(); let listener = self.proxy.add_listener_local() .property(move |subject , key, kind, value| { - let result = listener(subject, key, kind, value); + if listener_control_flow.borrow().is_released() { + return 0; + } + let result = listener( + &mut listener_control_flow.borrow_mut(), + subject, + key, + kind, + value + ); listeners.borrow_mut().triggered(&LISTENER_NAME.to_string()); result }) .register(); self.listeners.borrow_mut().add( LISTENER_NAME.to_string(), - Listener::new(listener, policy) + Listener::new(listener, control_flow) ); } } @@ -517,13 +541,11 @@ impl StreamState { description: format!("Stream {} is already connected", self.name) }); } - let object = pipewire::spa::pod::Value::Object(pipewire::spa::pod::Object { type_: pipewire::spa::sys::SPA_TYPE_OBJECT_Format, id: pipewire::spa::sys::SPA_PARAM_EnumFormat, properties: self.format.into(), }); - let values: Vec = pipewire::spa::pod::serialize::PodSerializer::serialize( Cursor::new(Vec::new()), &object, @@ -531,10 +553,8 @@ impl StreamState { .unwrap() .0 .into_inner(); - let mut params = [pipewire::spa::pod::Pod::from_bytes(&values).unwrap()]; let flags = pipewire::stream::StreamFlags::AUTOCONNECT | pipewire::stream::StreamFlags::MAP_BUFFERS; - self.proxy .connect( self.direction, @@ -543,9 +563,7 @@ impl StreamState { &mut params, ) .map_err(move |error| Error { description: error.to_string() })?; - self.is_connected = true; - Ok(()) } @@ -558,29 +576,31 @@ impl StreamState { self.proxy .disconnect() .map_err(move |error| Error { description: error.to_string() })?; - self.is_connected = false; - Ok(()) } pub fn add_process_listener( &mut self, - policy: ListenerTriggerPolicy, mut callback: StreamCallback ) { const LISTENER_NAME: &str = "process"; let listeners = self.listeners.clone(); + let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); + let listener_control_flow = control_flow.clone(); let listener = self.proxy.add_local_listener() .process(move |stream, _| { + if listener_control_flow.borrow().is_released() { + return; + } let buffer = stream.dequeue_buffer().unwrap(); - callback.call(buffer); + callback.call(&mut listener_control_flow.borrow_mut(), buffer); listeners.borrow_mut().triggered(&LISTENER_NAME.to_string()); }) .register() .unwrap(); - self.listeners.borrow_mut().add(LISTENER_NAME.to_string(), Listener::new(listener, policy)); + self.listeners.borrow_mut().add(LISTENER_NAME.to_string(), Listener::new(listener, control_flow)); } } @@ -672,11 +692,11 @@ impl Default for SettingsState { } impl SettingsState { - pub(super) fn listener(state: Rc>) -> impl Fn(u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static + pub(super) fn listener(state: Rc>) -> impl Fn(&mut ListenerControlFlow, u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static { const EXPECTED_PROPERTY: u32 = 5; let property_count: Rc> = Rc::new(Cell::new(0)); - move |_: u32, key: Option<&str>, _: Option<&str>, value: Option<&str>| { + move |control_flow, _, key, _, value| { let settings = &mut state.borrow_mut().settings; let key = key.unwrap(); let value = value.unwrap(); @@ -709,6 +729,7 @@ impl SettingsState { }; if let (GlobalObjectState::Pending, EXPECTED_PROPERTY) = (settings.state.clone(), property_count.get()) { settings.state = GlobalObjectState::Initialized; + control_flow.release(); } 0 } @@ -733,11 +754,11 @@ impl Default for DefaultAudioNodesState { } impl DefaultAudioNodesState { - pub(super) fn listener(state: Rc>) -> impl Fn(u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static + pub(super) fn listener(state: Rc>) -> impl Fn(&mut ListenerControlFlow, u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static { const EXPECTED_PROPERTY: u32 = 2; let property_count: Rc> = Rc::new(Cell::new(0)); - move |_: u32, key: Option<&str>, _: Option<&str>, value: Option<&str>| { + move |control_flow, _, key, _, value| { let default_audio_devices = &mut state.borrow_mut().default_audio_nodes; let key = key.unwrap(); if value.is_none() { @@ -771,6 +792,7 @@ impl DefaultAudioNodesState { }; if let (GlobalObjectState::Pending, EXPECTED_PROPERTY) = (default_audio_devices.state.clone(), property_count.get()) { default_audio_devices.state = GlobalObjectState::Initialized; + control_flow.release(); } 0 } diff --git a/pipewire-client/src/utils.rs b/pipewire-client/src/utils.rs index eab7b3c01..af333bd49 100644 --- a/pipewire-client/src/utils.rs +++ b/pipewire-client/src/utils.rs @@ -1,4 +1,4 @@ -use crate::listeners::{Listener, ListenerTriggerPolicy, Listeners}; +use crate::listeners::{Listener, ListenerControlFlow, Listeners}; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; @@ -49,18 +49,16 @@ impl PipewireCoreSync { } } - pub fn register(&self, keep: bool, seq: u32, callback: F) + pub fn register(&self, seq: u32, callback: F) where - F: Fn() + 'static, + F: Fn(&mut ListenerControlFlow) + 'static, { let sync_id = self.core.borrow_mut().sync(seq as i32).unwrap(); let name = format!("sync-{}", sync_id.raw()); - let policy = match keep { - true => ListenerTriggerPolicy::Keep, - false => ListenerTriggerPolicy::Remove, - }; let listeners = self.listeners.clone(); let listener_name = name.clone(); + let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); + let listener_control_flow = control_flow.clone(); let listener = self .core .borrow_mut() @@ -69,13 +67,16 @@ impl PipewireCoreSync { if seq != sync_id { return; } - callback(); + if listener_control_flow.borrow().is_released() { + return; + } + callback(&mut listener_control_flow.borrow_mut()); listeners.borrow_mut().triggered(&listener_name); }) .register(); self.listeners .borrow_mut() - .add(name, Listener::new(listener, policy)); + .add(name, Listener::new(listener, control_flow)); } } From 101fef55c9c0ea18d877c27d8908bb5d400206a9 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Mon, 20 Jan 2025 14:47:21 +0000 Subject: [PATCH 10/17] Testing if listeners are properly released. Avoid server fixture created in cascade (improved tests execution time) --- pipewire-client/src/client/api/core.rs | 2 +- pipewire-client/src/client/api/mod.rs | 8 +- pipewire-client/src/client/api/node_test.rs | 12 ++ pipewire-client/src/client/api/stream_test.rs | 69 ++++------ .../src/client/handlers/request.rs | 46 +++++++ .../src/client/implementation_test.rs | 20 ++- pipewire-client/src/client/mod.rs | 1 + pipewire-client/src/listeners.rs | 7 +- pipewire-client/src/messages.rs | 12 +- pipewire-client/src/states.rs | 36 +++++ pipewire-client/src/test_utils/api.rs | 33 +++++ pipewire-client/src/test_utils/fixtures.rs | 52 ++++---- pipewire-client/src/test_utils/mod.rs | 3 +- pipewire-client/src/test_utils/server.rs | 126 +++++++----------- pipewire-client/src/utils.rs | 4 + 15 files changed, 277 insertions(+), 154 deletions(-) create mode 100644 pipewire-client/src/test_utils/api.rs diff --git a/pipewire-client/src/client/api/core.rs b/pipewire-client/src/client/api/core.rs index f35f8c68a..63f3b7d1d 100644 --- a/pipewire-client/src/client/api/core.rs +++ b/pipewire-client/src/client/api/core.rs @@ -5,7 +5,7 @@ use crate::states::{DefaultAudioNodesState, SettingsState}; use std::sync::Arc; pub struct CoreApi { - api: Arc, + pub(crate) api: Arc, } impl CoreApi { diff --git a/pipewire-client/src/client/api/mod.rs b/pipewire-client/src/client/api/mod.rs index 9989d8358..ecc0268a5 100644 --- a/pipewire-client/src/client/api/mod.rs +++ b/pipewire-client/src/client/api/mod.rs @@ -1,20 +1,20 @@ mod core; -pub(super) use core::CoreApi; +pub(crate) use core::CoreApi; #[cfg(test)] #[path = "core_test.rs"] mod core_test; mod node; -pub(super) use node::NodeApi; +pub(crate) use node::NodeApi; #[cfg(test)] #[path = "node_test.rs"] mod node_test; mod stream; -pub(super) use stream::StreamApi; +pub(crate) use stream::StreamApi; #[cfg(test)] #[path = "stream_test.rs"] mod stream_test; mod internal; -pub(super) use internal::InternalApi; +pub(crate) use internal::InternalApi; diff --git a/pipewire-client/src/client/api/node_test.rs b/pipewire-client/src/client/api/node_test.rs index 818411901..94ec9374d 100644 --- a/pipewire-client/src/client/api/node_test.rs +++ b/pipewire-client/src/client/api/node_test.rs @@ -1,8 +1,10 @@ +use std::any::TypeId; use crate::test_utils::fixtures::client; use crate::test_utils::fixtures::PipewireTestClient; use crate::Direction; use rstest::rstest; use serial_test::serial; +use crate::states::{NodeState, StreamState}; fn internal_enumerate(client: &PipewireTestClient, direction: Direction) { let nodes = client.node().enumerate(direction).unwrap(); @@ -11,6 +13,11 @@ fn internal_enumerate(client: &PipewireTestClient, direction: Direction) { .filter(|node| node.is_default) .last(); assert_eq!(true, default_node.is_some()); + let listeners = client.core().get_listeners().unwrap(); + let node_listeners = listeners.get(&TypeId::of::()).unwrap(); + for (_, listeners) in node_listeners { + assert_eq!(0, listeners.len()); + } } fn internal_create(client: &PipewireTestClient, direction: Direction) { @@ -22,6 +29,11 @@ fn internal_create(client: &PipewireTestClient, direction: Direction) { direction, 2 ).unwrap(); + let listeners = client.core().get_listeners().unwrap(); + let node_listeners = listeners.get(&TypeId::of::()).unwrap(); + for (_, listeners) in node_listeners { + assert_eq!(0, listeners.len()); + } } #[rstest] diff --git a/pipewire-client/src/client/api/stream_test.rs b/pipewire-client/src/client/api/stream_test.rs index a9066cdf5..7bf6c8945 100644 --- a/pipewire-client/src/client/api/stream_test.rs +++ b/pipewire-client/src/client/api/stream_test.rs @@ -1,11 +1,11 @@ +use crate::listeners::ListenerControlFlow; +use crate::states::StreamState; use crate::test_utils::fixtures::client; -use crate::test_utils::fixtures::default_input_node; -use crate::test_utils::fixtures::default_output_node; use crate::test_utils::fixtures::PipewireTestClient; use crate::{Direction, NodeInfo}; use rstest::rstest; use serial_test::serial; -use crate::listeners::ListenerControlFlow; +use std::any::TypeId; fn internal_create( client: &PipewireTestClient, @@ -56,15 +56,13 @@ fn internal_create_connected( fn abstract_create( client: &PipewireTestClient, - default_input_node: NodeInfo, - default_output_node: NodeInfo, direction: Direction ) -> String { let stream = internal_create( &client, match direction { - Direction::Input => default_input_node.clone(), - Direction::Output => default_output_node.clone() + Direction::Input => client.default_input_node().clone(), + Direction::Output => client.default_output_node().clone() }, direction.clone(), move |control_flow, _| { @@ -76,6 +74,12 @@ fn abstract_create( Direction::Input => assert_eq!(true, stream.ends_with(".stream_input")), Direction::Output => assert_eq!(true, stream.ends_with(".stream_output")) }; + let listeners = client.core().get_listeners().unwrap(); + let stream_listeners = listeners.get(&TypeId::of::()).unwrap(); + for (_, listeners) in stream_listeners { + // Expect one listener since we created a stream object with our callback set + assert_eq!(1, listeners.len()); + } stream } @@ -83,59 +87,54 @@ fn abstract_create( #[serial] fn create_input( client: PipewireTestClient, - default_input_node: NodeInfo, - default_output_node: NodeInfo, ) { let direction = Direction::Input; - abstract_create(&client, default_input_node, default_output_node, direction); + abstract_create(&client, direction); } #[rstest] #[serial] fn create_output( client: PipewireTestClient, - default_input_node: NodeInfo, - default_output_node: NodeInfo, ) { let direction = Direction::Output; - abstract_create(&client, default_input_node, default_output_node, direction); + abstract_create(&client, direction); } #[rstest] #[serial] fn delete_input( client: PipewireTestClient, - default_input_node: NodeInfo, - default_output_node: NodeInfo, ) { let direction = Direction::Input; - let stream = abstract_create(&client, default_input_node, default_output_node, direction); - client.stream().delete(stream).unwrap() + let stream = abstract_create(&client, direction); + client.stream().delete(stream).unwrap(); + let listeners = client.core().get_listeners().unwrap(); + let stream_listeners = listeners.get(&TypeId::of::()).unwrap(); + for (_, listeners) in stream_listeners { + assert_eq!(0, listeners.len()); + } } #[rstest] #[serial] fn delete_output( client: PipewireTestClient, - default_input_node: NodeInfo, - default_output_node: NodeInfo, ) { let direction = Direction::Output; - let stream = abstract_create(&client, default_input_node, default_output_node, direction); + let stream = abstract_create(&client, direction); client.stream().delete(stream).unwrap() } fn abstract_connect( client: &PipewireTestClient, - default_input_node: NodeInfo, - default_output_node: NodeInfo, direction: Direction ) { let stream = internal_create( &client, match direction { - Direction::Input => default_input_node.clone(), - Direction::Output => default_output_node.clone() + Direction::Input => client.default_input_node().clone(), + Direction::Output => client.default_output_node().clone() }, direction.clone(), move |control_flow, mut buffer| { @@ -155,35 +154,29 @@ fn abstract_connect( #[serial] fn connect_input( client: PipewireTestClient, - default_input_node: NodeInfo, - default_output_node: NodeInfo, ) { let direction = Direction::Input; - abstract_connect(&client, default_input_node, default_output_node, direction); + abstract_connect(&client, direction); } #[rstest] #[serial] fn connect_output( client: PipewireTestClient, - default_input_node: NodeInfo, - default_output_node: NodeInfo, ) { let direction = Direction::Output; - abstract_connect(&client, default_input_node, default_output_node, direction); + abstract_connect(&client, direction); } fn abstract_disconnect( client: &PipewireTestClient, - default_input_node: NodeInfo, - default_output_node: NodeInfo, direction: Direction ) { let stream = internal_create_connected( &client, match direction { - Direction::Input => default_input_node.clone(), - Direction::Output => default_output_node.clone() + Direction::Input => client.default_input_node().clone(), + Direction::Output => client.default_output_node().clone() }, direction.clone(), move |control_flow, _| { @@ -198,20 +191,16 @@ fn abstract_disconnect( #[serial] fn disconnect_input( client: PipewireTestClient, - default_input_node: NodeInfo, - default_output_node: NodeInfo, ) { let direction = Direction::Input; - abstract_disconnect(&client, default_input_node, default_output_node, direction); + abstract_disconnect(&client, direction); } #[rstest] #[serial] fn disconnect_output( client: PipewireTestClient, - default_input_node: NodeInfo, - default_output_node: NodeInfo, ) { let direction = Direction::Output; - abstract_disconnect(&client, default_input_node, default_output_node, direction); + abstract_disconnect(&client, direction); } \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/request.rs b/pipewire-client/src/client/handlers/request.rs index 54db74403..2b7804948 100644 --- a/pipewire-client/src/client/handlers/request.rs +++ b/pipewire-client/src/client/handlers/request.rs @@ -6,6 +6,7 @@ use crate::utils::PipewireCoreSync; use crate::{AudioStreamInfo, Direction, NodeInfo}; use pipewire::proxy::ProxyT; use std::cell::RefCell; +use std::collections::HashMap; use std::rc::Rc; pub(super) fn request_handler( @@ -131,6 +132,13 @@ pub(super) fn request_handler( main_sender.clone() ) } + MessageRequest::Listeners => { + handle_listeners( + state.clone(), + main_sender.clone(), + core_sync.clone() + ) + } } } @@ -616,4 +624,42 @@ fn handle_node_count( .unwrap(); } }; +} +fn handle_listeners( + state: Rc>, + main_sender: crossbeam_channel::Sender, + core_sync: Rc +) +{ + let mut core = HashMap::new(); + core.insert("0".to_string(), core_sync.get_listener_names()); + let metadata = state.borrow().get_metadatas() + .unwrap_or_default() + .iter() + .map(move |(id, metadata)| { + (id.to_string(), metadata.get_listener_names()) + }) + .collect::>(); + let nodes = state.borrow().get_nodes() + .unwrap_or_default() + .iter() + .map(move |(id, node)| { + (id.to_string(), node.get_listener_names()) + }) + .collect::>(); + let streams = state.borrow().get_streams() + .unwrap_or_default() + .iter() + .map(move |(name, stream)| { + ((*name).clone(), stream.get_listener_names()) + }) + .collect::>(); + main_sender + .send(MessageResponse::Listeners { + core, + metadata, + nodes, + streams, + }) + .unwrap(); } \ No newline at end of file diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs index bd3e268bb..bed2bbed4 100644 --- a/pipewire-client/src/client/implementation_test.rs +++ b/pipewire-client/src/client/implementation_test.rs @@ -1,9 +1,13 @@ +use std::any::TypeId; +use std::fs::metadata; use crate::client::implementation::CLIENT_NAME_PREFIX; use crate::test_utils::fixtures::{client2, PipewireTestClient}; use crate::test_utils::server::{server_with_default_configuration, server_without_node, server_without_session_manager, set_socket_env_vars, Container}; use crate::PipewireClient; use rstest::rstest; use serial_test::serial; +use crate::states::{MetadataState, NodeState}; +use crate::utils::PipewireCoreSync; #[rstest] #[serial] @@ -21,7 +25,21 @@ pub fn names( #[serial] pub fn with_default_configuration(server_with_default_configuration: Container) { set_socket_env_vars(&server_with_default_configuration); - let _ = PipewireClient::new().unwrap(); + let client = PipewireClient::new().unwrap(); + let listeners = client.core().get_listeners().unwrap(); + let core_listeners = listeners.get(&TypeId::of::()).unwrap(); + let metadata_listeners = listeners.get(&TypeId::of::()).unwrap(); + let nodes_listeners = listeners.get(&TypeId::of::()).unwrap(); + // No need to check stream listeners since we had to create them in first place (i.e. after client init phases). + for (_, listeners) in core_listeners { + assert_eq!(0, listeners.len()); + } + for (_, listeners) in metadata_listeners { + assert_eq!(0, listeners.len()); + } + for (_, listeners) in nodes_listeners { + assert_eq!(0, listeners.len()); + } } #[rstest] diff --git a/pipewire-client/src/client/mod.rs b/pipewire-client/src/client/mod.rs index 43b6fdac2..b51a38ca4 100644 --- a/pipewire-client/src/client/mod.rs +++ b/pipewire-client/src/client/mod.rs @@ -3,6 +3,7 @@ pub use implementation::PipewireClient; mod connection_string; mod handlers; mod api; +pub(super) use api::CoreApi; #[cfg(test)] #[path = "./implementation_test.rs"] diff --git a/pipewire-client/src/listeners.rs b/pipewire-client/src/listeners.rs index 253b598c6..a5051832b 100644 --- a/pipewire-client/src/listeners.rs +++ b/pipewire-client/src/listeners.rs @@ -1,9 +1,8 @@ use std::cell::RefCell; use std::collections::HashMap; -use std::ops::ControlFlow; use std::rc::Rc; -pub(super) struct ListenerControlFlow { +pub struct ListenerControlFlow { is_released: bool } @@ -51,6 +50,10 @@ impl Listeners { listeners: Rc::new(RefCell::new(HashMap::new())), } } + + pub fn get_names(&self) -> Vec { + self.listeners.borrow().keys().cloned().collect() + } pub fn add(&mut self, name: String, listener: Listener) { let mut listeners = self.listeners.borrow_mut(); diff --git a/pipewire-client/src/messages.rs b/pipewire-client/src/messages.rs index 1bb00a424..a621c1710 100644 --- a/pipewire-client/src/messages.rs +++ b/pipewire-client/src/messages.rs @@ -71,7 +71,8 @@ pub(super) enum MessageRequest { DefaultAudioNodesState, NodeState(GlobalId), NodeStates, - NodeCount + NodeCount, + Listeners } #[derive(Debug, Clone)] @@ -100,6 +101,13 @@ pub(super) enum MessageResponse { NodeState(GlobalObjectState), NodeStates(Vec), NodeCount(u32), + // For testing purpose only + Listeners { + core: HashMap>, + metadata: HashMap>, + nodes: HashMap>, + streams: HashMap>, + } } #[derive(Debug, Clone)] @@ -123,5 +131,5 @@ pub(super) enum EventMessage { SetNodeFormat { id: GlobalId, format: AudioInfoRaw, - } + }, } \ No newline at end of file diff --git a/pipewire-client/src/states.rs b/pipewire-client/src/states.rs index b3d718b2f..e0fe20752 100644 --- a/pipewire-client/src/states.rs +++ b/pipewire-client/src/states.rs @@ -127,6 +127,18 @@ impl GlobalState { }) } + pub fn get_metadatas(&self) -> Result, Error> { + let metadatas = self.metadata.iter() + .map(|(id, state)| (id, state)) + .collect::>(); + if metadatas.is_empty() { + return Err(Error { + description: "Zero metadata registered".to_string(), + }) + } + Ok(metadatas) + } + pub fn insert_node(&mut self, id: GlobalId, state: NodeState) -> Result<(), Error> { if self.nodes.contains_key(&id) { return Err(Error { @@ -193,6 +205,18 @@ impl GlobalState { }) } + pub fn get_streams(&self) -> Result, Error> { + let streams = self.streams.iter() + .map(|(id, state)| (id, state)) + .collect::>(); + if streams.is_empty() { + return Err(Error { + description: "Zero stream registered".to_string(), + }) + } + Ok(streams) + } + pub fn get_settings(&self) -> SettingsState { self.settings.clone() } @@ -276,6 +300,10 @@ impl NodeState { listeners: Rc::new(RefCell::new(Listeners::new())), } } + + pub(super) fn get_listener_names(&self) -> Vec { + self.listeners.borrow().get_names() + } pub fn state(&self) -> GlobalObjectState { self.state.borrow().clone() @@ -470,6 +498,10 @@ impl MetadataState { listeners: Rc::new(RefCell::new(Listeners::new())), } } + + pub(super) fn get_listener_names(&self) -> Vec { + self.listeners.borrow().get_names() + } pub fn add_property_listener(&mut self, listener: F) where @@ -531,6 +563,10 @@ impl StreamState { } } + pub(super) fn get_listener_names(&self) -> Vec { + self.listeners.borrow().get_names() + } + pub fn is_connected(&self) -> bool { self.is_connected } diff --git a/pipewire-client/src/test_utils/api.rs b/pipewire-client/src/test_utils/api.rs new file mode 100644 index 000000000..4c245774d --- /dev/null +++ b/pipewire-client/src/test_utils/api.rs @@ -0,0 +1,33 @@ +use std::any::TypeId; +use std::collections::HashMap; +use crate::client::CoreApi; +use crate::error::Error; +use crate::messages::{MessageRequest, MessageResponse}; +use crate::states::{MetadataState, NodeState, StreamState}; +use crate::utils::PipewireCoreSync; + +impl CoreApi { + pub(crate) fn get_listeners(&self) -> Result>>, Error> { + let request = MessageRequest::Listeners; + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::Listeners { + core, + metadata, + nodes, + streams + }) => { + let mut map = HashMap::new(); + map.insert(TypeId::of::(), core); + map.insert(TypeId::of::(), metadata); + map.insert(TypeId::of::(), nodes); + map.insert(TypeId::of::(), streams); + Ok(map) + }, + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } +} \ No newline at end of file diff --git a/pipewire-client/src/test_utils/fixtures.rs b/pipewire-client/src/test_utils/fixtures.rs index 929f6c7b0..6f2483e31 100644 --- a/pipewire-client/src/test_utils/fixtures.rs +++ b/pipewire-client/src/test_utils/fixtures.rs @@ -17,6 +17,30 @@ impl PipewireTestClient { client, } } + + pub fn input_nodes(&self) -> Vec { + self.node().enumerate(Direction::Input).unwrap() + } + + pub fn output_nodes(&self) -> Vec { + self.node().enumerate(Direction::Output).unwrap() + } + + pub fn default_input_node(&self) -> NodeInfo { + self.input_nodes().iter() + .filter(|node| node.is_default) + .last() + .cloned() + .unwrap() + } + + pub fn default_output_node(&self) -> NodeInfo { + self.output_nodes().iter() + .filter(|node| node.is_default) + .last() + .cloned() + .unwrap() + } } impl Deref for PipewireTestClient { @@ -52,32 +76,4 @@ pub fn client2(server_with_default_configuration: Container) -> (PipewireTestCli client_2 ) ) -} - -#[fixture] -pub fn input_nodes(client: PipewireTestClient) -> Vec { - client.node().enumerate(Direction::Input).unwrap() -} - -#[fixture] -pub fn output_nodes(client: PipewireTestClient) -> Vec { - client.node().enumerate(Direction::Output).unwrap() -} - -#[fixture] -pub fn default_input_node(input_nodes: Vec) -> NodeInfo { - input_nodes.iter() - .filter(|node| node.is_default) - .last() - .cloned() - .unwrap() -} - -#[fixture] -pub fn default_output_node(output_nodes: Vec) -> NodeInfo { - output_nodes.iter() - .filter(|node| node.is_default) - .last() - .cloned() - .unwrap() } \ No newline at end of file diff --git a/pipewire-client/src/test_utils/mod.rs b/pipewire-client/src/test_utils/mod.rs index 27aeb978c..9a13c9d1a 100644 --- a/pipewire-client/src/test_utils/mod.rs +++ b/pipewire-client/src/test_utils/mod.rs @@ -1,2 +1,3 @@ pub mod fixtures; -pub mod server; \ No newline at end of file +pub mod server; +mod api; \ No newline at end of file diff --git a/pipewire-client/src/test_utils/server.rs b/pipewire-client/src/test_utils/server.rs index cf2e973ad..d01be7d01 100644 --- a/pipewire-client/src/test_utils/server.rs +++ b/pipewire-client/src/test_utils/server.rs @@ -6,7 +6,7 @@ use futures::StreamExt; use pipewire::spa::utils::dict::ParsableValue; use rstest::fixture; use std::path::{Path, PathBuf}; -use testcontainers::core::{CmdWaitFor, ExecCommand, Mount}; +use testcontainers::core::{CmdWaitFor, ExecCommand, ExecResult, Mount}; use testcontainers::runners::AsyncRunner; use testcontainers::{ContainerAsync, GenericImage, ImageExt}; use tokio::io::AsyncReadExt; @@ -110,47 +110,41 @@ impl Container { let runtime = tokio::runtime::Runtime::new().unwrap(); let container = runtime.block_on(container.start()).unwrap(); self.container = Some(container); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "mkdir", - "--parent", - socket_location.to_str().unwrap(), - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); + self.exec(vec![ + "mkdir", + "--parent", + socket_location.to_str().unwrap(), + ]); + } + + fn exec(&self, command: Vec<&str>) -> ExecResult { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime + .block_on(self.container.as_ref().unwrap().exec( + ExecCommand::new(command).with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + )) + .unwrap() } fn run_process(&mut self, process_name: &str) -> u32 { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - process_name - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); - let mut result = runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "pidof", - process_name, - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); + self.exec(vec![process_name]); + let mut result = self.exec(vec![ + "pidof", + process_name, + ]); let mut pid = String::new(); + let runtime = tokio::runtime::Runtime::new().unwrap(); runtime.block_on(result.stdout().read_to_string(&mut pid)).unwrap(); pid = pid.trim_end().to_string(); u32::parse_value(pid.as_str()).unwrap() } fn kill_process(&mut self, process_id: u32) { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "kill", - "-s", "SIGKILL", - format!("{}", process_id).as_str() - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); + self.exec(vec![ + "kill", + "-s", "SIGKILL", + format!("{}", process_id).as_str() + ]); } fn start_pipewire(&mut self) { @@ -187,55 +181,37 @@ impl Container { } fn load_null_sink_module(&self) { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "pactl", - "load-module", - "module-null-sink" - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); + self.exec(vec![ + "pactl", + "load-module", + "module-null-sink" + ]); } fn set_virtual_nodes_configuration(&self) { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "mkdir", - "--parent", - "/etc/pipewire/pipewire.conf.d/", - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "cp", - "/root/pipewire.nodes.conf", - "/etc/pipewire/pipewire.conf.d/pipewire.nodes.conf", - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); + self.exec(vec![ + "mkdir", + "--parent", + "/etc/pipewire/pipewire.conf.d/", + ]); + self.exec(vec![ + "cp", + "/root/pipewire.nodes.conf", + "/etc/pipewire/pipewire.conf.d/pipewire.nodes.conf", + ]); } fn set_default_nodes(&self) { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "pactl", - "set-default-sink", - "test-sink", - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); - runtime.block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(vec![ - "pactl", - "set-default-source", - "test-source", - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); + self.exec(vec![ + "pactl", + "set-default-sink", + "test-sink", + ]); + self.exec(vec![ + "pactl", + "set-default-source", + "test-source", + ]); } } diff --git a/pipewire-client/src/utils.rs b/pipewire-client/src/utils.rs index af333bd49..5a4b3381c 100644 --- a/pipewire-client/src/utils.rs +++ b/pipewire-client/src/utils.rs @@ -49,6 +49,10 @@ impl PipewireCoreSync { } } + pub(super) fn get_listener_names(&self) -> Vec { + self.listeners.borrow().get_names() + } + pub fn register(&self, seq: u32, callback: F) where F: Fn(&mut ListenerControlFlow) + 'static, From a9212f8e6ff0ac1cddb77dc0dcb2aae79c9d0215 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Mon, 20 Jan 2025 14:54:23 +0000 Subject: [PATCH 11/17] Remove serial_test dependency. All tests a containerized, no need to run them sequentially anymore. --- pipewire-client/Cargo.toml | 1 - pipewire-client/src/client/api/core_test.rs | 4 ---- pipewire-client/src/client/api/node_test.rs | 7 ------- pipewire-client/src/client/api/stream_test.rs | 9 --------- pipewire-client/src/client/implementation_test.rs | 5 ----- 5 files changed, 26 deletions(-) diff --git a/pipewire-client/Cargo.toml b/pipewire-client/Cargo.toml index d5e1e8a83..4382971f0 100644 --- a/pipewire-client/Cargo.toml +++ b/pipewire-client/Cargo.toml @@ -17,7 +17,6 @@ crossbeam-channel = "0.5" [dev-dependencies] rstest = "0.24" -serial_test = "3.2" testcontainers = "0.23" docker-api = "0.14" futures = "0.3" diff --git a/pipewire-client/src/client/api/core_test.rs b/pipewire-client/src/client/api/core_test.rs index 2a6612257..4ae1519a7 100644 --- a/pipewire-client/src/client/api/core_test.rs +++ b/pipewire-client/src/client/api/core_test.rs @@ -1,15 +1,12 @@ use crate::test_utils::fixtures::{client, PipewireTestClient}; use rstest::rstest; -use serial_test::serial; #[rstest] -#[serial] fn quit(client: PipewireTestClient) { client.core().quit(); } #[rstest] -#[serial] pub fn settings(client: PipewireTestClient) { let settings = client.core().get_settings().unwrap(); assert_eq!(true, settings.sample_rate > u32::default()); @@ -20,7 +17,6 @@ pub fn settings(client: PipewireTestClient) { } #[rstest] -#[serial] pub fn default_audio_nodes(client: PipewireTestClient) { let default_audio_nodes = client.core().get_default_audio_nodes().unwrap(); assert_eq!(false, default_audio_nodes.sink.is_empty()); diff --git a/pipewire-client/src/client/api/node_test.rs b/pipewire-client/src/client/api/node_test.rs index 94ec9374d..0ad71ce12 100644 --- a/pipewire-client/src/client/api/node_test.rs +++ b/pipewire-client/src/client/api/node_test.rs @@ -3,7 +3,6 @@ use crate::test_utils::fixtures::client; use crate::test_utils::fixtures::PipewireTestClient; use crate::Direction; use rstest::rstest; -use serial_test::serial; use crate::states::{NodeState, StreamState}; fn internal_enumerate(client: &PipewireTestClient, direction: Direction) { @@ -37,7 +36,6 @@ fn internal_create(client: &PipewireTestClient, direction: Direction) { } #[rstest] -#[serial] fn enumerate_input( client: PipewireTestClient, ) { @@ -45,7 +43,6 @@ fn enumerate_input( } #[rstest] -#[serial] fn enumerate_output( client: PipewireTestClient, ) { @@ -53,7 +50,6 @@ fn enumerate_output( } #[rstest] -#[serial] fn create_input( client: PipewireTestClient, ) { @@ -61,7 +57,6 @@ fn create_input( } #[rstest] -#[serial] fn create_output( client: PipewireTestClient, ) { @@ -69,7 +64,6 @@ fn create_output( } #[rstest] -#[serial] fn create_then_enumerate_input( client: PipewireTestClient, ) { @@ -79,7 +73,6 @@ fn create_then_enumerate_input( } #[rstest] -#[serial] fn create_then_enumerate_output( client: PipewireTestClient, ) { diff --git a/pipewire-client/src/client/api/stream_test.rs b/pipewire-client/src/client/api/stream_test.rs index 7bf6c8945..896f62589 100644 --- a/pipewire-client/src/client/api/stream_test.rs +++ b/pipewire-client/src/client/api/stream_test.rs @@ -4,7 +4,6 @@ use crate::test_utils::fixtures::client; use crate::test_utils::fixtures::PipewireTestClient; use crate::{Direction, NodeInfo}; use rstest::rstest; -use serial_test::serial; use std::any::TypeId; fn internal_create( @@ -84,7 +83,6 @@ fn abstract_create( } #[rstest] -#[serial] fn create_input( client: PipewireTestClient, ) { @@ -93,7 +91,6 @@ fn create_input( } #[rstest] -#[serial] fn create_output( client: PipewireTestClient, ) { @@ -102,7 +99,6 @@ fn create_output( } #[rstest] -#[serial] fn delete_input( client: PipewireTestClient, ) { @@ -117,7 +113,6 @@ fn delete_input( } #[rstest] -#[serial] fn delete_output( client: PipewireTestClient, ) { @@ -151,7 +146,6 @@ fn abstract_connect( } #[rstest] -#[serial] fn connect_input( client: PipewireTestClient, ) { @@ -160,7 +154,6 @@ fn connect_input( } #[rstest] -#[serial] fn connect_output( client: PipewireTestClient, ) { @@ -188,7 +181,6 @@ fn abstract_disconnect( } #[rstest] -#[serial] fn disconnect_input( client: PipewireTestClient, ) { @@ -197,7 +189,6 @@ fn disconnect_input( } #[rstest] -#[serial] fn disconnect_output( client: PipewireTestClient, ) { diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs index bed2bbed4..680fde20c 100644 --- a/pipewire-client/src/client/implementation_test.rs +++ b/pipewire-client/src/client/implementation_test.rs @@ -5,12 +5,10 @@ use crate::test_utils::fixtures::{client2, PipewireTestClient}; use crate::test_utils::server::{server_with_default_configuration, server_without_node, server_without_session_manager, set_socket_env_vars, Container}; use crate::PipewireClient; use rstest::rstest; -use serial_test::serial; use crate::states::{MetadataState, NodeState}; use crate::utils::PipewireCoreSync; #[rstest] -#[serial] pub fn names( #[from(client2)] (client_1, client_2): (PipewireTestClient, PipewireTestClient) ) { @@ -22,7 +20,6 @@ pub fn names( } #[rstest] -#[serial] pub fn with_default_configuration(server_with_default_configuration: Container) { set_socket_env_vars(&server_with_default_configuration); let client = PipewireClient::new().unwrap(); @@ -43,7 +40,6 @@ pub fn with_default_configuration(server_with_default_configuration: Container) } #[rstest] -#[serial] pub fn without_session_manager(server_without_session_manager: Container) { set_socket_env_vars(&server_without_session_manager); let error = PipewireClient::new().unwrap_err(); @@ -51,7 +47,6 @@ pub fn without_session_manager(server_without_session_manager: Container) { } #[rstest] -#[serial] pub fn without_node(server_without_node: Container) { set_socket_env_vars(&server_without_node); let error = PipewireClient::new().unwrap_err(); From ac9f8257cb2cbeee290df8cf7a43be03dab4806d Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Mon, 20 Jan 2025 14:55:22 +0000 Subject: [PATCH 12/17] Optimize imports --- pipewire-client/src/client/api/node_test.rs | 4 ++-- pipewire-client/src/client/api/stream.rs | 2 +- pipewire-client/src/client/implementation_test.rs | 7 +++---- pipewire-client/src/messages.rs | 2 +- pipewire-client/src/test_utils/api.rs | 4 ++-- pipewire-spa-utils/build.rs | 6 +++--- pipewire-spa-utils/src/format/mod.rs | 6 +++--- pipewire-spa-utils/src/utils/mod.rs | 2 +- 8 files changed, 16 insertions(+), 17 deletions(-) diff --git a/pipewire-client/src/client/api/node_test.rs b/pipewire-client/src/client/api/node_test.rs index 0ad71ce12..6b043ff1d 100644 --- a/pipewire-client/src/client/api/node_test.rs +++ b/pipewire-client/src/client/api/node_test.rs @@ -1,9 +1,9 @@ -use std::any::TypeId; +use crate::states::NodeState; use crate::test_utils::fixtures::client; use crate::test_utils::fixtures::PipewireTestClient; use crate::Direction; use rstest::rstest; -use crate::states::{NodeState, StreamState}; +use std::any::TypeId; fn internal_enumerate(client: &PipewireTestClient, direction: Direction) { let nodes = client.node().enumerate(direction).unwrap(); diff --git a/pipewire-client/src/client/api/stream.rs b/pipewire-client/src/client/api/stream.rs index 543ba4ffb..ce09f816f 100644 --- a/pipewire-client/src/client/api/stream.rs +++ b/pipewire-client/src/client/api/stream.rs @@ -1,10 +1,10 @@ use crate::client::api::internal::InternalApi; use crate::error::Error; +use crate::listeners::ListenerControlFlow; use crate::messages::{MessageRequest, MessageResponse, StreamCallback}; use crate::states::GlobalId; use crate::{AudioStreamInfo, Direction}; use std::sync::Arc; -use crate::listeners::ListenerControlFlow; pub struct StreamApi { api: Arc, diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs index 680fde20c..cd2a73402 100644 --- a/pipewire-client/src/client/implementation_test.rs +++ b/pipewire-client/src/client/implementation_test.rs @@ -1,12 +1,11 @@ -use std::any::TypeId; -use std::fs::metadata; use crate::client::implementation::CLIENT_NAME_PREFIX; +use crate::states::{MetadataState, NodeState}; use crate::test_utils::fixtures::{client2, PipewireTestClient}; use crate::test_utils::server::{server_with_default_configuration, server_without_node, server_without_session_manager, set_socket_env_vars, Container}; +use crate::utils::PipewireCoreSync; use crate::PipewireClient; use rstest::rstest; -use crate::states::{MetadataState, NodeState}; -use crate::utils::PipewireCoreSync; +use std::any::TypeId; #[rstest] pub fn names( diff --git a/pipewire-client/src/messages.rs b/pipewire-client/src/messages.rs index a621c1710..5ae9407a9 100644 --- a/pipewire-client/src/messages.rs +++ b/pipewire-client/src/messages.rs @@ -1,12 +1,12 @@ use crate::error::Error; use crate::info::{AudioStreamInfo, NodeInfo}; +use crate::listeners::ListenerControlFlow; use crate::states::{DefaultAudioNodesState, GlobalId, GlobalObjectState, SettingsState}; use crate::utils::Direction; use pipewire_spa_utils::audio::raw::AudioInfoRaw; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use std::sync::{Arc, Mutex}; -use crate::listeners::ListenerControlFlow; pub(super) struct StreamCallback { callback: Arc>> diff --git a/pipewire-client/src/test_utils/api.rs b/pipewire-client/src/test_utils/api.rs index 4c245774d..5578257b8 100644 --- a/pipewire-client/src/test_utils/api.rs +++ b/pipewire-client/src/test_utils/api.rs @@ -1,10 +1,10 @@ -use std::any::TypeId; -use std::collections::HashMap; use crate::client::CoreApi; use crate::error::Error; use crate::messages::{MessageRequest, MessageResponse}; use crate::states::{MetadataState, NodeState, StreamState}; use crate::utils::PipewireCoreSync; +use std::any::TypeId; +use std::collections::HashMap; impl CoreApi { pub(crate) fn get_listeners(&self) -> Result>>, Error> { diff --git a/pipewire-spa-utils/build.rs b/pipewire-spa-utils/build.rs index 4a656618f..7c385f63d 100644 --- a/pipewire-spa-utils/build.rs +++ b/pipewire-spa-utils/build.rs @@ -1,9 +1,9 @@ extern crate cargo; -extern crate syn; -extern crate itertools; -extern crate indexmap; extern crate cargo_metadata; +extern crate indexmap; +extern crate itertools; extern crate quote; +extern crate syn; mod build_modules; diff --git a/pipewire-spa-utils/src/format/mod.rs b/pipewire-spa-utils/src/format/mod.rs index a561546db..a79445434 100644 --- a/pipewire-spa-utils/src/format/mod.rs +++ b/pipewire-spa-utils/src/format/mod.rs @@ -1,10 +1,10 @@ -use libspa::utils::Id; use libspa::pod::deserialize::DeserializeError; use libspa::pod::deserialize::DeserializeSuccess; +use libspa::pod::deserialize::IdVisitor; use libspa::pod::deserialize::PodDeserialize; use libspa::pod::deserialize::PodDeserializer; -use libspa::pod::deserialize::IdVisitor; -use ::{impl_id_deserializer}; +use libspa::utils::Id; +use ::impl_id_deserializer; include!(concat!(env!("OUT_DIR"), "/format.rs")); diff --git a/pipewire-spa-utils/src/utils/mod.rs b/pipewire-spa-utils/src/utils/mod.rs index 18b44bedd..2b30c9dfb 100644 --- a/pipewire-spa-utils/src/utils/mod.rs +++ b/pipewire-spa-utils/src/utils/mod.rs @@ -1,3 +1,4 @@ +use ::impl_any_deserializer; use libspa::pod::deserialize::DeserializeError; use libspa::pod::deserialize::DeserializeSuccess; use libspa::pod::deserialize::PodDeserialize; @@ -6,7 +7,6 @@ use libspa::pod::deserialize::{ChoiceIdVisitor, ChoiceIntVisitor}; use libspa::pod::{ChoiceValue, Value}; use libspa::utils::{Choice, ChoiceEnum, Id}; use std::ops::Deref; -use ::impl_any_deserializer; use impl_choice_int_deserializer; #[derive(Debug, Clone)] From e97ec083204f0013f7d374b8794eac31e3493521 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Mon, 20 Jan 2025 15:06:26 +0000 Subject: [PATCH 13/17] Install libpipewire in jobs workflow --- .github/workflows/cpal.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/cpal.yml b/.github/workflows/cpal.yml index db0b51387..6f160a5dd 100644 --- a/.github/workflows/cpal.yml +++ b/.github/workflows/cpal.yml @@ -14,6 +14,8 @@ jobs: run: sudo apt-get install libasound2-dev - name: Install libjack run: sudo apt-get install libjack-jackd2-dev libjack-jackd2-0 + - name: Install libpipewire + run: sudo apt-get install libpipewire-0.3-dev - name: Install stable uses: dtolnay/rust-toolchain@stable with: @@ -67,6 +69,8 @@ jobs: run: sudo apt-get install libasound2-dev - name: Install libjack run: sudo apt-get install libjack-jackd2-dev libjack-jackd2-0 + - name: Install libpipewire + run: sudo apt-get install libpipewire-0.3-dev - name: Install stable uses: dtolnay/rust-toolchain@stable - name: Run without features From faf68c6e5f02220265c076ee9510fb5b973b22b4 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Mon, 20 Jan 2025 15:15:01 +0000 Subject: [PATCH 14/17] Fix device stream callback --- src/host/pipewire/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index dbbe95ef9..ee05cebb1 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -95,7 +95,7 @@ impl Device { self.id, direction, format, - move |buffer| { + move |_, buffer| { let mut buffer = AudioBuffer::from( buffer, sample_format, From a2f95fa40fd1a6ae3b5a2861740167a8eca7c83b Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Tue, 28 Jan 2025 19:16:11 +0000 Subject: [PATCH 15/17] Better handling how to run tests (local and containers). Better handling requests/responses in async context. Fix instabilities when running all tests. --- .../.containers/pipewire.test.container | 23 - pipewire-client/Cargo.toml | 13 +- pipewire-client/src/client/api/core.rs | 24 +- pipewire-client/src/client/api/core_test.rs | 12 +- pipewire-client/src/client/api/internal.rs | 52 +- pipewire-client/src/client/api/node.rs | 104 +-- pipewire-client/src/client/api/node_test.rs | 100 ++- pipewire-client/src/client/api/stream.rs | 14 +- pipewire-client/src/client/api/stream_test.rs | 262 ++++--- pipewire-client/src/client/channel.rs | 338 ++++++++ pipewire-client/src/client/channel_test.rs | 88 +++ pipewire-client/src/client/handlers/event.rs | 104 +-- .../src/client/handlers/registry.rs | 69 +- .../src/client/handlers/request.rs | 685 +++++++++------- pipewire-client/src/client/handlers/thread.rs | 62 +- pipewire-client/src/client/implementation.rs | 141 ++-- .../src/client/implementation_test.rs | 53 +- pipewire-client/src/client/mod.rs | 9 +- pipewire-client/src/constants.rs | 33 - pipewire-client/src/lib.rs | 13 +- pipewire-client/src/listeners.rs | 57 ++ pipewire-client/src/messages.rs | 33 +- pipewire-client/src/states.rs | 95 ++- pipewire-client/src/test_utils/api.rs | 2 +- pipewire-client/src/test_utils/fixtures.rs | 405 ++++++++-- pipewire-client/src/test_utils/mod.rs | 1 - pipewire-client/src/test_utils/server.rs | 288 ------- pipewire-client/src/utils.rs | 144 ---- pipewire-common/Cargo.toml | 13 + pipewire-common/src/constants.rs | 34 + .../src/error.rs | 0 pipewire-common/src/lib.rs | 4 + pipewire-common/src/macros.rs | 85 ++ pipewire-common/src/utils.rs | 110 +++ pipewire-test-utils/.containers/.digests | 3 + .../.containers/.tmp/entrypoint.bash | 6 + .../.containers/.tmp/healthcheck.bash | 14 + .../.containers/.tmp/supervisor.conf | 21 + .../.containers/pipewire.test.container | 48 ++ .../.containers/virtual.nodes.conf | 0 pipewire-test-utils/Cargo.toml | 31 + .../src/containers/container.rs | 591 ++++++++++++++ pipewire-test-utils/src/containers/mod.rs | 3 + pipewire-test-utils/src/containers/options.rs | 269 +++++++ .../src/containers/sync_api.rs | 414 ++++++++++ pipewire-test-utils/src/environment.rs | 231 ++++++ pipewire-test-utils/src/lib.rs | 17 + pipewire-test-utils/src/server.rs | 730 ++++++++++++++++++ 48 files changed, 4549 insertions(+), 1299 deletions(-) delete mode 100644 pipewire-client/.containers/pipewire.test.container create mode 100644 pipewire-client/src/client/channel.rs create mode 100644 pipewire-client/src/client/channel_test.rs delete mode 100644 pipewire-client/src/constants.rs delete mode 100644 pipewire-client/src/test_utils/server.rs delete mode 100644 pipewire-client/src/utils.rs create mode 100644 pipewire-common/Cargo.toml create mode 100644 pipewire-common/src/constants.rs rename {pipewire-client => pipewire-common}/src/error.rs (100%) create mode 100644 pipewire-common/src/lib.rs create mode 100644 pipewire-common/src/macros.rs create mode 100644 pipewire-common/src/utils.rs create mode 100644 pipewire-test-utils/.containers/.digests create mode 100644 pipewire-test-utils/.containers/.tmp/entrypoint.bash create mode 100644 pipewire-test-utils/.containers/.tmp/healthcheck.bash create mode 100644 pipewire-test-utils/.containers/.tmp/supervisor.conf create mode 100644 pipewire-test-utils/.containers/pipewire.test.container rename pipewire-client/.containers/pipewire.nodes.conf => pipewire-test-utils/.containers/virtual.nodes.conf (100%) create mode 100644 pipewire-test-utils/Cargo.toml create mode 100644 pipewire-test-utils/src/containers/container.rs create mode 100644 pipewire-test-utils/src/containers/mod.rs create mode 100644 pipewire-test-utils/src/containers/options.rs create mode 100644 pipewire-test-utils/src/containers/sync_api.rs create mode 100644 pipewire-test-utils/src/environment.rs create mode 100644 pipewire-test-utils/src/lib.rs create mode 100644 pipewire-test-utils/src/server.rs diff --git a/pipewire-client/.containers/pipewire.test.container b/pipewire-client/.containers/pipewire.test.container deleted file mode 100644 index 7db129c2f..000000000 --- a/pipewire-client/.containers/pipewire.test.container +++ /dev/null @@ -1,23 +0,0 @@ -from fedora:41 as base - -run dnf update --assumeyes -run dnf install --assumeyes \ - tini \ - procps-ng \ - pipewire \ - pipewire-utils \ - pipewire-alsa \ - pipewire-pulse \ - pipewire-devel \ - wireplumber \ - pulseaudio-utils - -env PIPEWIRE_CORE="pipewire-0" -env PIPEWIRE_REMOTE="pipewire-0" -env PIPEWIRE_RUNTIME_DIR="/run" - -copy pipewire.nodes.conf /root/pipewire.nodes.conf - -# For handling signal properly. Avoid to hangout before container runtime send SIGKILL signal. -entrypoint ["tini", "--"] -cmd ["bash", "-c", "sleep 999d"] \ No newline at end of file diff --git a/pipewire-client/Cargo.toml b/pipewire-client/Cargo.toml index 4382971f0..1fe5b4535 100644 --- a/pipewire-client/Cargo.toml +++ b/pipewire-client/Cargo.toml @@ -12,13 +12,16 @@ keywords = ["pipewire", "client"] [dependencies] pipewire = { version = "0.8" } pipewire-spa-utils = { version = "0.1", path = "../pipewire-spa-utils"} +pipewire-common = { version = "0.1", path = "../pipewire-common" } serde_json = "1.0" crossbeam-channel = "0.5" +uuid = { version = "1.12", features = ["v4"] } +tokio = { version = "1", features = ["full"] } +tokio-util = "0.7" +libc = "0.2" [dev-dependencies] rstest = "0.24" -testcontainers = "0.23" -docker-api = "0.14" -futures = "0.3" -tokio = { version = "1", features = ["full"] } -uuid = { version = "1.12", features = ["v4"] } \ No newline at end of file +serial_test = "3.2" +ctor = "0.2" +pipewire-test-utils = { version = "0.1", path = "../pipewire-test-utils" } \ No newline at end of file diff --git a/pipewire-client/src/client/api/core.rs b/pipewire-client/src/client/api/core.rs index 63f3b7d1d..1970c5a71 100644 --- a/pipewire-client/src/client/api/core.rs +++ b/pipewire-client/src/client/api/core.rs @@ -1,7 +1,7 @@ use crate::client::api::internal::InternalApi; use crate::error::Error; use crate::messages::{MessageRequest, MessageResponse}; -use crate::states::{DefaultAudioNodesState, SettingsState}; +use crate::states::{DefaultAudioNodesState, GlobalObjectState, SettingsState}; use std::sync::Arc; pub struct CoreApi { @@ -52,9 +52,16 @@ impl CoreApi { } } - pub(crate) fn get_settings_state(&self) -> Result<(), Error> { + pub(crate) fn get_settings_state(&self) -> Result { let request = MessageRequest::SettingsState; - self.api.send_request_without_response(&request) + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::SettingsState(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } } pub fn get_default_audio_nodes(&self) -> Result { @@ -69,8 +76,15 @@ impl CoreApi { } } - pub(crate) fn get_default_audio_nodes_state(&self) -> Result<(), Error> { + pub(crate) fn get_default_audio_nodes_state(&self) -> Result { let request = MessageRequest::DefaultAudioNodesState; - self.api.send_request_without_response(&request) + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::DefaultAudioNodesState(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } } } \ No newline at end of file diff --git a/pipewire-client/src/client/api/core_test.rs b/pipewire-client/src/client/api/core_test.rs index 4ae1519a7..4a1e47ebd 100644 --- a/pipewire-client/src/client/api/core_test.rs +++ b/pipewire-client/src/client/api/core_test.rs @@ -1,13 +1,16 @@ -use crate::test_utils::fixtures::{client, PipewireTestClient}; +use crate::test_utils::fixtures::{isolated_client, shared_client, PipewireTestClient}; use rstest::rstest; +use serial_test::serial; #[rstest] -fn quit(client: PipewireTestClient) { +#[serial] +fn quit(#[from(isolated_client)] client: PipewireTestClient) { client.core().quit(); } #[rstest] -pub fn settings(client: PipewireTestClient) { +#[serial] +pub fn settings(#[from(shared_client)] client: PipewireTestClient) { let settings = client.core().get_settings().unwrap(); assert_eq!(true, settings.sample_rate > u32::default()); assert_eq!(true, settings.default_buffer_size > u32::default()); @@ -17,7 +20,8 @@ pub fn settings(client: PipewireTestClient) { } #[rstest] -pub fn default_audio_nodes(client: PipewireTestClient) { +#[serial] +pub fn default_audio_nodes(#[from(shared_client)] client: PipewireTestClient) { let default_audio_nodes = client.core().get_default_audio_nodes().unwrap(); assert_eq!(false, default_audio_nodes.sink.is_empty()); assert_eq!(false, default_audio_nodes.source.is_empty()); diff --git a/pipewire-client/src/client/api/internal.rs b/pipewire-client/src/client/api/internal.rs index f4336eb3b..79808edab 100644 --- a/pipewire-client/src/client/api/internal.rs +++ b/pipewire-client/src/client/api/internal.rs @@ -1,66 +1,42 @@ +use crate::client::channel::ClientChannel; use crate::error::Error; use crate::messages::{MessageRequest, MessageResponse}; -use crossbeam_channel::{RecvError, RecvTimeoutError}; use std::time::Duration; pub(crate) struct InternalApi { - sender: pipewire::channel::Sender, - receiver: crossbeam_channel::Receiver, + pub(crate) channel: ClientChannel, + pub(crate) timeout: Duration } impl InternalApi { pub(crate) fn new( - sender: pipewire::channel::Sender, - receiver: crossbeam_channel::Receiver, + channel: ClientChannel, + timeout: Duration ) -> Self { InternalApi { - sender, - receiver, + channel, + timeout, } } - pub(crate) fn wait_response(&self) -> Result { - self.receiver.recv() - } - - pub(crate) fn wait_response_with_timeout(&self, timeout: Duration) -> Result { - self.receiver.recv_timeout(timeout) + pub(crate) fn wait_response_with_timeout(&self, timeout: Duration) -> Result { + self.channel.receive_timeout(timeout) } pub(crate) fn send_request(&self, request: &MessageRequest) -> Result { - let response = self.sender.send(request.clone()); - let response = match response { - Ok(_) => self.receiver.recv(), - Err(_) => return Err(Error { - description: format!("Failed to send request: {:?}", request), - }), - }; + let response = self.channel.send(request.clone()); match response { Ok(value) => { match value { MessageResponse::Error(value) => Err(value), - _ => Ok(value), + _ => Ok(value) } - }, - Err(value) => Err(Error { - description: format!( - "Failed to execute request ({:?}): {:?}", - request, value - ), - }), + } + Err(value) => Err(value) } } pub(crate) fn send_request_without_response(&self, request: &MessageRequest) -> Result<(), Error> { - let response = self.sender.send(request.clone()); - match response { - Ok(_) => Ok(()), - Err(value) => Err(Error { - description: format!( - "Failed to execute request ({:?}): {:?}", - request, value - ), - }), - } + self.channel.fire(request.clone()).map(move |_| ()) } } \ No newline at end of file diff --git a/pipewire-client/src/client/api/node.rs b/pipewire-client/src/client/api/node.rs index 3d52ccd70..8f3415f28 100644 --- a/pipewire-client/src/client/api/node.rs +++ b/pipewire-client/src/client/api/node.rs @@ -17,22 +17,36 @@ impl NodeApi { } } - pub(crate) fn get_state( + pub(crate) fn state( &self, id: &GlobalId, - ) -> Result<(), Error> { + ) -> Result { let request = MessageRequest::NodeState(id.clone()); - self.api.send_request_without_response(&request) + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::NodeState(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } } - pub(crate) fn get_states( + pub(crate) fn states( &self, - ) -> Result<(), Error> { + ) -> Result, Error> { let request = MessageRequest::NodeStates; - self.api.send_request_without_response(&request) + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::NodeStates(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } } - pub(crate) fn get_count( + pub(crate) fn count( &self, ) -> Result { let request = MessageRequest::NodeCount; @@ -46,6 +60,25 @@ impl NodeApi { } } + pub fn get( + &self, + name: String, + direction: Direction, + ) -> Result { + let request = MessageRequest::GetNode { + name, + direction, + }; + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::GetNode(value)) => Ok(value), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + pub fn create( &self, name: String, @@ -63,45 +96,18 @@ impl NodeApi { }; let response = self.api.send_request(&request); match response { - Ok(MessageResponse::CreateNode { - id - }) => { - #[cfg(debug_assertions)] - let timeout_duration = std::time::Duration::from_secs(u64::MAX); - #[cfg(not(debug_assertions))] - let timeout_duration = std::time::Duration::from_millis(500); - self.get_state(&id)?; + Ok(MessageResponse::CreateNode(id)) => { let operation = move || { - let response = self.api.wait_response_with_timeout(timeout_duration); - return match response { - Ok(value) => match value { - MessageResponse::NodeState(state) => { - match state == GlobalObjectState::Initialized { - true => { - Ok(()) - }, - false => { - self.get_state(&id)?; - Err(Error { - description: "Created node should be initialized at this point".to_string(), - }) - } - } - } - _ => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - }, - Err(value) => Err(Error { - description: format!("Failed during post initialization: {:?}", value), + let state = self.state(&id)?; + return if state == GlobalObjectState::Initialized { + Ok(()) + } else { + Err(Error { + description: "Created node not yet initialized".to_string(), }) - }; + } }; - let mut backoff = Backoff::new( - 10, - std::time::Duration::from_millis(10), - std::time::Duration::from_millis(100), - ); + let mut backoff = Backoff::constant(self.api.timeout.as_millis()); backoff.retry(operation) }, Ok(MessageResponse::Error(value)) => Err(value), @@ -112,6 +118,18 @@ impl NodeApi { } } + pub fn delete(&self, id: u32) -> Result<(), Error> { + let request = MessageRequest::DeleteNode(GlobalId::from(id)); + let response = self.api.send_request(&request); + match response { + Ok(MessageResponse::DeleteNode) => Ok(()), + Err(value) => Err(value), + Ok(value) => Err(Error { + description: format!("Received unexpected response: {:?}", value), + }), + } + } + pub fn enumerate( &self, direction: Direction, diff --git a/pipewire-client/src/client/api/node_test.rs b/pipewire-client/src/client/api/node_test.rs index 6b043ff1d..ac5bb9f8f 100644 --- a/pipewire-client/src/client/api/node_test.rs +++ b/pipewire-client/src/client/api/node_test.rs @@ -1,11 +1,12 @@ use crate::states::NodeState; -use crate::test_utils::fixtures::client; -use crate::test_utils::fixtures::PipewireTestClient; +use crate::test_utils::fixtures::{shared_client, PipewireTestClient}; use crate::Direction; use rstest::rstest; +use serial_test::serial; use std::any::TypeId; +use uuid::Uuid; -fn internal_enumerate(client: &PipewireTestClient, direction: Direction) { +fn internal_enumerate(client: &PipewireTestClient, direction: Direction) -> Vec { let nodes = client.node().enumerate(direction).unwrap(); assert_eq!(false, nodes.is_empty()); let default_node = nodes.iter() @@ -17,14 +18,18 @@ fn internal_enumerate(client: &PipewireTestClient, direction: Direction) { for (_, listeners) in node_listeners { assert_eq!(0, listeners.len()); } + nodes.iter() + .map(move |node| node.name.clone()) + .collect() } -fn internal_create(client: &PipewireTestClient, direction: Direction) { +fn internal_create(client: &PipewireTestClient, direction: Direction) -> String { + let node_name = Uuid::new_v4().to_string(); client.node() .create( - "test".to_string(), - "test".to_string(), - "test".to_string(), + node_name.clone(), + node_name.clone(), + node_name.clone(), direction, 2 ).unwrap(); @@ -33,50 +38,111 @@ fn internal_create(client: &PipewireTestClient, direction: Direction) { for (_, listeners) in node_listeners { assert_eq!(0, listeners.len()); } + node_name } #[rstest] +#[serial] fn enumerate_input( - client: PipewireTestClient, + #[from(shared_client)] client: PipewireTestClient, ) { internal_enumerate(&client, Direction::Input); } #[rstest] +#[serial] fn enumerate_output( - client: PipewireTestClient, + #[from(shared_client)] client: PipewireTestClient, ) { internal_enumerate(&client, Direction::Output); } #[rstest] +#[serial] fn create_input( - client: PipewireTestClient, + #[from(shared_client)] client: PipewireTestClient, ) { internal_create(&client, Direction::Input); } #[rstest] +#[serial] fn create_output( - client: PipewireTestClient, + #[from(shared_client)] client: PipewireTestClient, ) { internal_create(&client, Direction::Output); } #[rstest] +#[serial] +fn create_twice_same_direction( + #[from(shared_client)] client: PipewireTestClient, +) { + let node_name = Uuid::new_v4().to_string(); + client.node() + .create( + node_name.clone(), + node_name.clone(), + node_name.clone(), + Direction::Output, + 2 + ).unwrap(); + let error = client.node() + .create( + node_name.clone(), + node_name.clone(), + node_name.clone(), + Direction::Output, + 2 + ).unwrap_err(); + assert_eq!( + format!("Node with name({}) already exists", node_name), + error.description + ) +} + +#[rstest] +#[serial] +fn create_twice_different_direction( + #[from(shared_client)] client: PipewireTestClient, +) { + let node_name = Uuid::new_v4().to_string(); + client.node() + .create( + node_name.clone(), + node_name.clone(), + node_name.clone(), + Direction::Input, + 2 + ).unwrap(); + client.node() + .create( + node_name.clone(), + node_name.clone(), + node_name.clone(), + Direction::Output, + 2 + ).unwrap(); +} + +#[rstest] +#[serial] fn create_then_enumerate_input( - client: PipewireTestClient, + #[from(shared_client)] client: PipewireTestClient, ) { let direction = Direction::Input; - internal_create(&client, direction.clone()); - internal_enumerate(&client, direction.clone()); + let node = internal_create(&client, direction.clone()); + let nodes = internal_enumerate(&client, direction.clone()); + assert_eq!(true, nodes.contains(&node)) } #[rstest] +#[serial] fn create_then_enumerate_output( - client: PipewireTestClient, + #[from(shared_client)] client: PipewireTestClient, ) { let direction = Direction::Output; - internal_create(&client, direction.clone()); - internal_enumerate(&client, direction.clone()); + let node = internal_create(&client, direction.clone()); + let nodes = internal_enumerate(&client, direction.clone()); + assert_eq!(true, nodes.contains(&node)) } \ No newline at end of file diff --git a/pipewire-client/src/client/api/stream.rs b/pipewire-client/src/client/api/stream.rs index ce09f816f..ae6ad9cbd 100644 --- a/pipewire-client/src/client/api/stream.rs +++ b/pipewire-client/src/client/api/stream.rs @@ -35,7 +35,7 @@ impl StreamApi { }; let response = self.api.send_request(&request); match response { - Ok(MessageResponse::CreateStream{name}) => Ok(name), + Ok(MessageResponse::CreateStream(name)) => Ok(name), Err(value) => Err(value), Ok(value) => Err(Error { description: format!("Received unexpected response: {:?}", value), @@ -47,9 +47,7 @@ impl StreamApi { &self, name: String ) -> Result<(), Error> { - let request = MessageRequest::DeleteStream { - name, - }; + let request = MessageRequest::DeleteStream(name); let response = self.api.send_request(&request); match response { Ok(MessageResponse::DeleteStream) => Ok(()), @@ -64,9 +62,7 @@ impl StreamApi { &self, name: String ) -> Result<(), Error> { - let request = MessageRequest::ConnectStream { - name, - }; + let request = MessageRequest::ConnectStream(name); let response = self.api.send_request(&request); match response { Ok(MessageResponse::ConnectStream) => Ok(()), @@ -81,9 +77,7 @@ impl StreamApi { &self, name: String ) -> Result<(), Error> { - let request = MessageRequest::DisconnectStream { - name, - }; + let request = MessageRequest::DisconnectStream(name); let response = self.api.send_request(&request); match response { Ok(MessageResponse::DisconnectStream) => Ok(()), diff --git a/pipewire-client/src/client/api/stream_test.rs b/pipewire-client/src/client/api/stream_test.rs index 896f62589..79c2a3dc1 100644 --- a/pipewire-client/src/client/api/stream_test.rs +++ b/pipewire-client/src/client/api/stream_test.rs @@ -1,47 +1,40 @@ use crate::listeners::ListenerControlFlow; use crate::states::StreamState; -use crate::test_utils::fixtures::client; -use crate::test_utils::fixtures::PipewireTestClient; -use crate::{Direction, NodeInfo}; +use crate::test_utils::fixtures::{input_connected_stream, input_node, input_stream, output_connected_stream, output_node, output_stream, shared_client, ConnectedStreamFixture, NodeInfoFixture, PipewireTestClient, StreamFixture}; +use crate::{Direction, PipewireClient}; use rstest::rstest; +use serial_test::serial; use std::any::TypeId; +use std::fmt::{Display, Formatter}; +use std::ops::Deref; +use crate::client::api::StreamApi; +use crate::client::CoreApi; -fn internal_create( - client: &PipewireTestClient, - node: NodeInfo, - direction: Direction, - callback: F, -) -> String where - F: FnMut(&mut ListenerControlFlow, pipewire::buffer::Buffer) + Send + 'static -{ - client.stream() - .create( - node.id, - direction, - node.format.clone().into(), - callback - ) - .unwrap() -} - -fn internal_delete( - client: &PipewireTestClient, - stream: &String -) { - client.stream() - .delete(stream.clone()) - .unwrap() +fn assert_listeners(client: &CoreApi, stream_name: &String, expected_listener: u32) { + let listeners = client.get_listeners().unwrap(); + let stream_listeners = listeners.get(&TypeId::of::()).unwrap().iter() + .find_map(move |(key, listeners)| { + if key == stream_name { + Some(listeners) + } + else { + None + } + }) + .unwrap(); + assert_eq!(expected_listener as usize, stream_listeners.len()); } -fn internal_create_connected( - client: &PipewireTestClient, - node: NodeInfo, +fn internal_create( + client: &StreamApi, + node: &NodeInfoFixture, direction: Direction, callback: F, -) -> String where +) -> String +where F: FnMut(&mut ListenerControlFlow, pipewire::buffer::Buffer) + Send + 'static { - let stream = client.stream() + let stream_name = client .create( node.id, direction, @@ -49,20 +42,17 @@ fn internal_create_connected( callback ) .unwrap(); - client.stream().connect(stream.clone()).unwrap(); - stream + stream_name } fn abstract_create( - client: &PipewireTestClient, + client: &PipewireClient, + node: &NodeInfoFixture, direction: Direction -) -> String { +) { let stream = internal_create( - &client, - match direction { - Direction::Input => client.default_input_node().clone(), - Direction::Output => client.default_output_node().clone() - }, + &client.stream(), + node, direction.clone(), move |control_flow, _| { assert!(true); @@ -73,125 +63,167 @@ fn abstract_create( Direction::Input => assert_eq!(true, stream.ends_with(".stream_input")), Direction::Output => assert_eq!(true, stream.ends_with(".stream_output")) }; - let listeners = client.core().get_listeners().unwrap(); - let stream_listeners = listeners.get(&TypeId::of::()).unwrap(); - for (_, listeners) in stream_listeners { - // Expect one listener since we created a stream object with our callback set - assert_eq!(1, listeners.len()); - } - stream + assert_listeners(client.core(), &stream, 1); } #[rstest] +#[serial] fn create_input( - client: PipewireTestClient, + #[from(input_node)] node: NodeInfoFixture ) { let direction = Direction::Input; - abstract_create(&client, direction); + abstract_create(&node.client(), &node, direction); } #[rstest] +#[serial] fn create_output( - client: PipewireTestClient, + #[from(output_node)] node: NodeInfoFixture ) { let direction = Direction::Output; - abstract_create(&client, direction); + abstract_create(&node.client(), &node, direction); } #[rstest] +#[serial] +fn create_twice( + #[from(output_node)] node: NodeInfoFixture +) { + let direction = Direction::Output; + let stream = node.client().stream() + .create( + node.id, + direction.clone(), + node.format.clone().into(), + move |_, _| {} + ) + .unwrap(); + let error = node.client().stream() + .create( + node.id, + direction.clone(), + node.format.clone().into(), + move |_, _| {} + ) + .unwrap_err(); + assert_eq!( + format!("Stream with name({}) already exists", stream), + error.description + ); + assert_listeners(node.client().core(), &stream, 1); +} + +#[rstest] +#[serial] fn delete_input( - client: PipewireTestClient, + #[from(input_stream)] stream: StreamFixture ) { - let direction = Direction::Input; - let stream = abstract_create(&client, direction); - client.stream().delete(stream).unwrap(); - let listeners = client.core().get_listeners().unwrap(); - let stream_listeners = listeners.get(&TypeId::of::()).unwrap(); - for (_, listeners) in stream_listeners { - assert_eq!(0, listeners.len()); - } + stream.delete().unwrap(); } #[rstest] +#[serial] fn delete_output( - client: PipewireTestClient, + #[from(output_stream)] stream: StreamFixture ) { - let direction = Direction::Output; - let stream = abstract_create(&client, direction); - client.stream().delete(stream).unwrap() + stream.delete().unwrap(); } -fn abstract_connect( - client: &PipewireTestClient, - direction: Direction +#[rstest] +#[serial] +fn delete_when_not_exists( + #[from(shared_client)] client: PipewireTestClient, ) { - let stream = internal_create( - &client, - match direction { - Direction::Input => client.default_input_node().clone(), - Direction::Output => client.default_output_node().clone() - }, - direction.clone(), - move |control_flow, mut buffer| { - let data = buffer.datas_mut(); - let data = &mut data[0]; - let data = data.data().unwrap(); - assert_eq!(true, data.len() > 0); - control_flow.release(); - } - ); - client.stream().connect(stream.clone()).ok().unwrap(); - // Wait a bit to test if stream callback will panic - std::thread::sleep(std::time::Duration::from_millis(1 * 1000)); + let stream = "not_existing_stream".to_string(); + let error = client.stream().delete(stream.clone()).unwrap_err(); + assert_eq!( + format!("Stream with name({}) not found", stream), + error.description + ) } #[rstest] +#[serial] +fn delete_twice( + #[from(output_stream)] stream: StreamFixture +) { + stream.delete().unwrap(); + let error = stream.delete().unwrap_err(); + assert_eq!( + format!("Stream with name({}) not found", stream), + error.description + ) +} + +#[rstest] +#[serial] fn connect_input( - client: PipewireTestClient, + #[from(input_stream)] stream: StreamFixture ) { - let direction = Direction::Input; - abstract_connect(&client, direction); + stream.connect().unwrap(); + assert_listeners(stream.client().core(), &stream, 1); } #[rstest] +#[serial] fn connect_output( - client: PipewireTestClient, + #[from(output_stream)] stream: StreamFixture ) { - let direction = Direction::Output; - abstract_connect(&client, direction); + stream.connect().unwrap(); + assert_listeners(stream.client().core(), &stream, 1); } -fn abstract_disconnect( - client: &PipewireTestClient, - direction: Direction +#[rstest] +#[serial] +fn connect_twice( + #[from(output_connected_stream)] stream: ConnectedStreamFixture ) { - let stream = internal_create_connected( - &client, - match direction { - Direction::Input => client.default_input_node().clone(), - Direction::Output => client.default_output_node().clone() - }, - direction.clone(), - move |control_flow, _| { - assert!(true); - control_flow.release(); - } - ); - client.stream().disconnect(stream.clone()).unwrap(); + let error = stream.connect().unwrap_err(); + assert_eq!( + format!("Stream {} is already connected", stream), + error.description + ) } #[rstest] +#[serial] fn disconnect_input( - client: PipewireTestClient, + #[from(input_connected_stream)] stream: ConnectedStreamFixture ) { - let direction = Direction::Input; - abstract_disconnect(&client, direction); + stream.disconnect().unwrap(); + assert_listeners(stream.client().core(), &stream, 1); } #[rstest] +#[serial] fn disconnect_output( - client: PipewireTestClient, + #[from(output_connected_stream)] stream: ConnectedStreamFixture ) { - let direction = Direction::Output; - abstract_disconnect(&client, direction); + stream.disconnect().unwrap(); + assert_listeners(stream.client().core(), &stream, 1); +} + +#[rstest] +#[serial] +fn disconnect_when_not_connected( + #[from(output_stream)] stream: StreamFixture +) { + let error = stream.disconnect().unwrap_err(); + assert_eq!( + format!("Stream {} is not connected", stream), + error.description + ) +} + +#[rstest] +#[serial] +fn disconnect_twice( + #[from(output_connected_stream)] stream: ConnectedStreamFixture +) { + stream.disconnect().unwrap(); + let error = stream.disconnect().unwrap_err(); + assert_eq!( + format!("Stream {} is not connected", stream), + error.description + ) } \ No newline at end of file diff --git a/pipewire-client/src/client/channel.rs b/pipewire-client/src/client/channel.rs new file mode 100644 index 000000000..6bbcfbd9c --- /dev/null +++ b/pipewire-client/src/client/channel.rs @@ -0,0 +1,338 @@ +use ControlFlow::Break; +use crate::error::Error; +use crossbeam_channel::{unbounded, SendError, TryRecvError}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::ops::ControlFlow; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tokio::runtime::Runtime; +use tokio::select; +use tokio::time::Instant; +use tokio_util::sync::CancellationToken; +use uuid::Uuid; + +pub(crate) struct Request { + pub id: Uuid, + pub message: T, +} + +impl Request { + pub(super) fn new(message: T) -> Self { + Self { + id: Uuid::new_v4(), + message, + } + } +} + +impl From for Request { + fn from(value: T) -> Self { + Self::new(value) + } +} + +pub(crate) struct Response { + pub id: Uuid, + pub message: T, +} + +impl Response { + pub fn from(request: &Request, message: T) -> Self { + Self { + id: request.id.clone(), + message, + } + } +} + +type PendingMessages = Arc>>>; +type GlobalMessages = Arc>>>; + +const GLOBAL_MESSAGE_ID: Uuid = Uuid::nil(); + +pub(crate) struct ClientChannel { + sender: pipewire::channel::Sender>, + receiver: crossbeam_channel::Receiver>, + pub(super) global_messages: GlobalMessages, + pub(super) pending_messages: PendingMessages, + runtime: Arc +} + +impl ClientChannel { + pub(self) fn new( + sender: pipewire::channel::Sender>, + receiver: crossbeam_channel::Receiver>, + runtime: Arc + ) -> Self { + Self { + sender, + receiver, + global_messages: Arc::new(Mutex::new(Vec::new())), + pending_messages: Arc::new(Mutex::new(HashMap::new())), + runtime, + } + } + + pub fn fire(&self, request: Q) -> Result { + let id = Uuid::new_v4(); + let request = Request { + id: id.clone(), + message: request, + }; + let response = self.sender.send(request); + match response { + Ok(_) => Ok(id), + Err(value) => Err(Error { + description: format!("Failed to send request: {:?}", value.message), + }), + } + } + + pub fn send(&self, request: Q) -> Result { + let id = Uuid::new_v4(); + let request = Request { + id: id.clone(), + message: request, + }; + let response = self.sender.send(request); + let response = match response { + Ok(_) => self.receive( + id, + self.global_messages.clone(), + self.pending_messages.clone(), + self.receiver.clone(), + CancellationToken::default() + ), + Err(value) => return Err(Error { + description: format!("Failed to send request: {:?}", value.message), + }), + }; + match response { + Ok(value) => Ok(value.message), + Err(value) => Err(Error { + description: format!( + "Failed to execute request ({:?})", value + ), + }), + } + } + + pub fn send_timeout(&self, request: Q, timeout: Duration) -> Result { + let request_id = match self.fire(request) { + Ok(value) => value, + Err(value) => return Err(value) + }; + self.internal_receive_timeout(request_id, timeout) + } + + pub fn receive_timeout(&self, timeout: Duration) -> Result { + self.internal_receive_timeout(GLOBAL_MESSAGE_ID, timeout) + } + + fn internal_receive_timeout(&self, id: Uuid, timeout: Duration) -> Result { + let global_messages = self.global_messages.clone(); + let pending_messages = self.pending_messages.clone(); + let receiver = self.receiver.clone(); + let handle = self.runtime.spawn(async move { + let start_time = Instant::now(); + loop { + let control_flow = Self::internal_receive( + id, + global_messages.clone(), + pending_messages.clone(), + receiver.clone() + ); + match control_flow.await { + Break(value) => { + return match value { + Ok(value ) => Ok(value.message), + Err(value) => Err(value) + }; + }, + _ => { + let now_time = Instant::now(); + let delta_time = now_time - start_time; + if delta_time >= timeout { + return Err(Error { + description: "Timeout".to_string(), + }); + } + continue + }, + } + } + }); + self.runtime.block_on(handle).unwrap() + } + + fn receive( + &self, + id: Uuid, + global_messages: GlobalMessages, + pending_messages: PendingMessages, + receiver: crossbeam_channel::Receiver>, + cancellation_token: CancellationToken + ) -> Result, Error> { + let handle = self.runtime.spawn(async move { + loop { + select! { + _ = cancellation_token.cancelled() => (), + control_flow = Self::internal_receive( + id, + global_messages.clone(), + pending_messages.clone(), + receiver.clone() + ) => { + match control_flow { + Break(value) => return value, + _ => continue, + } + } + } + } + }); + self.runtime.block_on(handle).unwrap() + } + + async fn internal_receive( + id: Uuid, + global_messages: GlobalMessages, + pending_messages: PendingMessages, + receiver: crossbeam_channel::Receiver>, + ) -> ControlFlow, Error>, ()> + { + let response = receiver.try_recv(); + match response { + Ok(value) => { + // When request id is equal Uuid::nil, + // message is sent when unrecoverable error occurred outside of request context. + // But it's not necessary an error message, initialized message is global too. + // + // Those errors are sent in event handler, registry + // and during server thread init phase. + // Might a good idea to find a better solution, because any request could fail + // but not because request is malformed or request result cannot be computed, but + // because somewhere else something bad happen. + // + // Maybe putting error messages into a vec which will be regularly watched by an async + // periodic task ? But that involve to create a thread/task in tokio and that task will + // live during client lifetime (maybe for a long time). + // + // Maybe add a separate channel but same issue occur here. An async periodic task will + // watch if any error message spawned + // + // For now, solution is simple: + // 1.1: Requested id is equal to response id + // in that case we break the loop because that's the requested id + // 1.2: Requested id is not equal to response id but to global id + // we store that response to further process + if value.id == id { + Break(Ok(value)) + } + else if value.id == GLOBAL_MESSAGE_ID { + global_messages.lock().unwrap().push(value); + ControlFlow::Continue(()) + } + else { + pending_messages.lock().unwrap().insert(value.id.clone(), value); + ControlFlow::Continue(()) + } + } + Err(value) => { + match value { + TryRecvError::Empty => { + match pending_messages.lock().unwrap().remove(&id) { + Some(value) => Break(Ok(value)), + None => ControlFlow::Continue(()), + } + } + TryRecvError::Disconnected => { + Break(Err(Error { + description: "Channel disconnected".to_string(), + })) + } + } + } + } + } +} + +impl Clone for ClientChannel { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + receiver: self.receiver.clone(), + global_messages: self.global_messages.clone(), + pending_messages: self.pending_messages.clone(), + runtime: self.runtime.clone(), + } + } +} + +pub(crate) struct ServerChannel { + sender: crossbeam_channel::Sender>, + receiver: Option>> +} + +impl ServerChannel { + pub(self) fn new( + sender: crossbeam_channel::Sender>, + receiver: pipewire::channel::Receiver>, + ) -> Self { + Self { + sender, + receiver: Some(receiver), + } + } + + pub fn attach<'a, F>(&mut self, loop_: &'a pipewire::loop_::LoopRef, callback: F) -> pipewire::channel::AttachedReceiver<'a, Request> + where + F: Fn(Request) + 'static, + { + let receiver = self.receiver.take().unwrap(); + let attached_receiver = receiver.attach(loop_, callback); + attached_receiver + } + + pub fn fire(&self, response: R) -> Result<(), SendError>> { + let response = Response { + id: GLOBAL_MESSAGE_ID, + message: response, + }; + self.sender.send(response) + } + + pub fn send(&self, request: &Request, response: R) -> Result<(), SendError>> { + let response = Response::from(request, response); + self.sender.send(response) + } +} + +impl Clone for ServerChannel { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + receiver: None // pipewire receiver cannot be cloned + } + } +} + +pub(crate) fn channels(runtime: Arc) -> (ClientChannel, ServerChannel) +where + Q: Debug + Send, + R: Send + 'static +{ + let (pw_sender, pw_receiver) = pipewire::channel::channel(); + let (main_sender, main_receiver) = unbounded(); + let client_channel = ClientChannel::::new( + pw_sender, + main_receiver, + runtime + ); + let server_channel = ServerChannel::::new( + main_sender, + pw_receiver + ); + (client_channel, server_channel) +} \ No newline at end of file diff --git a/pipewire-client/src/client/channel_test.rs b/pipewire-client/src/client/channel_test.rs new file mode 100644 index 000000000..634686897 --- /dev/null +++ b/pipewire-client/src/client/channel_test.rs @@ -0,0 +1,88 @@ +use crate::client::channel::{channels, Request}; +use rstest::rstest; +use std::thread; +use std::time::Duration; +use pipewire_test_utils::environment::TEST_ENVIRONMENT; + +#[derive(Debug, Clone, Copy)] +enum MessageRequest { + Quit, + Test1, + Test2 +} + +#[derive(Debug, PartialEq)] +enum MessageResponse { + Test1, + Test2, + GlobalMessage +} + +#[rstest] +fn request_context() { + let (client_channel, mut server_channel) = channels(TEST_ENVIRONMENT.lock().unwrap().runtime.clone()); + let handle_main = thread::spawn(move || { + let sender = server_channel.clone(); + let main_loop = pipewire::main_loop::MainLoop::new(None).unwrap(); + let attached_main_loop = main_loop.clone(); + let _attached_channel = server_channel.attach( + main_loop.loop_(), + move |message| { + let request = message.message; + match request { + MessageRequest::Test1 => { + sender + .send(&message, MessageResponse::Test1) + .unwrap() + } + MessageRequest::Test2 => { + sender + .send(&message, MessageResponse::Test2) + .unwrap() + } + _ => attached_main_loop.quit() + } + } + ); + main_loop.run(); + }); + let request_1 = MessageRequest::Test1; + let request_2 = MessageRequest::Test2; + let response = client_channel.send(request_1).unwrap(); + assert_eq!(MessageResponse::Test1, response); + let response = client_channel.send(request_2).unwrap(); + assert_eq!(MessageResponse::Test2, response); + client_channel.fire(MessageRequest::Quit).unwrap(); + assert_eq!(0, client_channel.global_messages.lock().unwrap().len()); + assert_eq!(0, client_channel.pending_messages.lock().unwrap().len()); + handle_main.join().unwrap(); +} + +#[rstest] +fn global_message() { + let (client_channel, server_channel) = channels::(TEST_ENVIRONMENT.lock().unwrap().runtime.clone()); + server_channel.fire(MessageResponse::GlobalMessage).unwrap(); + client_channel + .send_timeout( + MessageRequest::Test2, + Duration::from_millis(200), + ) + .unwrap_err(); + assert_eq!(1, client_channel.global_messages.lock().unwrap().len()); + assert_eq!(0, client_channel.pending_messages.lock().unwrap().len()); +} + +#[rstest] +fn pending_message() { + let (client_channel, server_channel) = channels::(TEST_ENVIRONMENT.lock().unwrap().runtime.clone()); + let request = Request::new(MessageRequest::Test1); + server_channel.send(&request ,MessageResponse::Test1).unwrap(); + client_channel + .send_timeout( + MessageRequest::Test2, + Duration::from_millis(200), + ) + .unwrap_err(); + assert_eq!(0, client_channel.global_messages.lock().unwrap().len()); + assert_eq!(1, client_channel.pending_messages.lock().unwrap().len()); +} diff --git a/pipewire-client/src/client/handlers/event.rs b/pipewire-client/src/client/handlers/event.rs index d6e5c7ed4..8501f3caa 100644 --- a/pipewire-client/src/client/handlers/event.rs +++ b/pipewire-client/src/client/handlers/event.rs @@ -1,15 +1,17 @@ use crate::constants::{METADATA_NAME_PROPERTY_VALUE_DEFAULT, METADATA_NAME_PROPERTY_VALUE_SETTINGS}; use crate::error::Error; -use crate::messages::{EventMessage, MessageResponse}; +use crate::messages::{EventMessage, MessageRequest, MessageResponse}; use crate::states::{DefaultAudioNodesState, GlobalId, GlobalState, SettingsState}; use pipewire_spa_utils::audio::raw::AudioInfoRaw; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use crate::client::channel::ServerChannel; pub(super) fn event_handler( - state: Rc>, - main_sender: crossbeam_channel::Sender, + state: Arc>, + server_channel: ServerChannel, event_sender: pipewire::channel::Sender, ) -> impl Fn(EventMessage) + 'static { @@ -17,23 +19,23 @@ pub(super) fn event_handler( EventMessage::SetMetadataListeners { id } => handle_set_metadata_listeners( id, state.clone(), - main_sender.clone(), + server_channel.clone(), ), EventMessage::RemoveNode { id } => handle_remove_node( id, - state.clone(), - main_sender.clone() + state.clone(), + server_channel.clone() ), EventMessage::SetNodePropertiesListener { id } => handle_set_node_properties_listener( id, - state.clone(), - main_sender.clone(), + state.clone(), + server_channel.clone(), event_sender.clone() ), EventMessage::SetNodeFormatListener { id } => handle_set_node_format_listener( id, - state.clone(), - main_sender.clone(), + state.clone(), + server_channel.clone(), event_sender.clone() ), EventMessage::SetNodeProperties { @@ -42,36 +44,36 @@ pub(super) fn event_handler( } => handle_set_node_properties( id, properties, - state.clone(), - main_sender.clone() + state.clone(), + server_channel.clone() ), EventMessage::SetNodeFormat { id, format } => handle_set_node_format( id, format, - state.clone(), - main_sender.clone() + state.clone(), + server_channel.clone() ), } } fn handle_set_metadata_listeners( id: GlobalId, - state: Rc>, - main_sender: crossbeam_channel::Sender, + state: Arc>, + server_channel: ServerChannel, ) { let listener_state = state.clone(); - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); let metadata = match state.get_metadata_mut(&id) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + server_channel + .fire(MessageResponse::Error(value)) .unwrap(); return; } }; - let main_sender = main_sender.clone(); + let server_channel = server_channel.clone(); match metadata.name.as_str() { METADATA_NAME_PROPERTY_VALUE_SETTINGS => { metadata.add_property_listener( @@ -84,8 +86,8 @@ fn handle_set_metadata_listeners( ) }, _ => { - main_sender - .send(MessageResponse::Error(Error { + server_channel + .fire(MessageResponse::Error(Error { description: format!("Unexpected metadata with name: {}", metadata.name) })) .unwrap(); @@ -94,16 +96,16 @@ fn handle_set_metadata_listeners( } fn handle_remove_node( id: GlobalId, - state: Rc>, - main_sender: crossbeam_channel::Sender, + state: Arc>, + server_channel: ServerChannel, ) { - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); let _ = match state.get_node(&id) { Ok(_) => {}, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + server_channel + .fire(MessageResponse::Error(value)) .unwrap(); return; } @@ -112,17 +114,17 @@ fn handle_remove_node( } fn handle_set_node_properties_listener( id: GlobalId, - state: Rc>, - main_sender: crossbeam_channel::Sender, + state: Arc>, + server_channel: ServerChannel, event_sender: pipewire::channel::Sender, ) { - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); let node = match state.get_node_mut(&id) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + server_channel + .fire(MessageResponse::Error(value)) .unwrap(); return; } @@ -169,22 +171,22 @@ fn handle_set_node_properties_listener( } fn handle_set_node_format_listener( id: GlobalId, - state: Rc>, - main_sender: crossbeam_channel::Sender, + state: Arc>, + server_channel: ServerChannel, event_sender: pipewire::channel::Sender, ) { - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); let node = match state.get_node_mut(&id) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + server_channel + .fire(MessageResponse::Error(value)) .unwrap(); return; } }; - let main_sender = main_sender.clone(); + let server_channel = server_channel.clone(); let event_sender = event_sender.clone(); node.add_format_listener( move |control_flow, format| { @@ -198,7 +200,9 @@ fn handle_set_node_format_listener( .unwrap(); } Err(value) => { - main_sender.send(MessageResponse::Error(value)).unwrap(); + server_channel + .fire(MessageResponse::Error(value)) + .unwrap(); } }; control_flow.release(); @@ -208,16 +212,16 @@ fn handle_set_node_format_listener( fn handle_set_node_properties( id: GlobalId, properties: HashMap, - state: Rc>, - main_sender: crossbeam_channel::Sender, + state: Arc>, + server_channel: ServerChannel, ) { - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); let node = match state.get_node_mut(&id) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + server_channel + .fire(MessageResponse::Error(value)) .unwrap(); return; } @@ -227,19 +231,19 @@ fn handle_set_node_properties( fn handle_set_node_format( id: GlobalId, format: AudioInfoRaw, - state: Rc>, - main_sender: crossbeam_channel::Sender, + state: Arc>, + server_channel: ServerChannel, ) { - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); let node = match state.get_node_mut(&id) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + server_channel + .fire(MessageResponse::Error(value)) .unwrap(); return; } }; - node.set_format(format) + node.set_format(format); } \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/registry.rs b/pipewire-client/src/client/handlers/registry.rs index abdeaf0e1..454db8b91 100644 --- a/pipewire-client/src/client/handlers/registry.rs +++ b/pipewire-client/src/client/handlers/registry.rs @@ -1,51 +1,53 @@ +use crate::client::channel::ServerChannel; use crate::constants::{APPLICATION_NAME_PROPERTY_KEY, APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION, APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER, MEDIA_CLASS_PROPERTY_KEY, MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK, MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE, METADATA_NAME_PROPERTY_KEY, METADATA_NAME_PROPERTY_VALUE_DEFAULT, METADATA_NAME_PROPERTY_VALUE_SETTINGS}; -use crate::messages::{EventMessage, MessageResponse}; +use crate::messages::{EventMessage, MessageRequest, MessageResponse}; use crate::states::{ClientState, GlobalId, GlobalObjectState, GlobalState, MetadataState, NodeState}; -use crate::utils::debug_dict_ref; use pipewire::registry::GlobalObject; use pipewire::spa; use std::cell::RefCell; use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use pipewire_common::utils::dict_ref_to_hashmap; pub(super) fn registry_global_handler( - state: Rc>, + state: Arc>, registry: Rc, - main_sender: crossbeam_channel::Sender, + server_channel: ServerChannel, event_sender: pipewire::channel::Sender, ) -> impl Fn(&GlobalObject<&spa::utils::dict::DictRef>) + 'static { move |global: &GlobalObject<&spa::utils::dict::DictRef>| match global.type_ { pipewire::types::ObjectType::Client => handle_client( global, - state.clone(), - main_sender.clone() + state.clone(), + server_channel.clone() ), pipewire::types::ObjectType::Metadata => handle_metadata( global, state.clone(), - registry.clone(), - main_sender.clone(), + registry.clone(), + server_channel.clone(), event_sender.clone() ), pipewire::types::ObjectType::Node => handle_node( global, state.clone(), registry.clone(), - main_sender.clone(), + server_channel.clone(), event_sender.clone() ), pipewire::types::ObjectType::Port => handle_port( global, state.clone(), registry.clone(), - main_sender.clone(), + server_channel.clone(), event_sender.clone() ), pipewire::types::ObjectType::Link => handle_link( global, state.clone(), registry.clone(), - main_sender.clone(), + server_channel.clone(), event_sender.clone() ), _ => {} @@ -54,8 +56,8 @@ pub(super) fn registry_global_handler( fn handle_client( global: &GlobalObject<&spa::utils::dict::DictRef>, - state: Rc>, - main_sender: crossbeam_channel::Sender, + state: Arc>, + server_channel: ServerChannel, ) { if global.props.is_none() { @@ -76,10 +78,10 @@ fn handle_client( } _ => return, }; - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); if let Err(value) = state.insert_client(global.id.into(), client) { - main_sender - .send(MessageResponse::Error(value)) + server_channel + .fire(MessageResponse::Error(value)) .unwrap(); return; }; @@ -87,9 +89,9 @@ fn handle_client( fn handle_metadata( global: &GlobalObject<&spa::utils::dict::DictRef>, - state: Rc>, + state: Arc>, registry: Rc, - main_sender: crossbeam_channel::Sender, + server_channel: ServerChannel, event_sender: pipewire::channel::Sender, ) { @@ -109,10 +111,10 @@ fn handle_metadata( } _ => return, }; - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); if let Err(value) = state.insert_metadata(global.id.into(), metadata) { - main_sender - .send(MessageResponse::Error(value)) + server_channel + .fire(MessageResponse::Error(value)) .unwrap(); return; }; @@ -126,9 +128,9 @@ fn handle_metadata( fn handle_node( global: &GlobalObject<&spa::utils::dict::DictRef>, - state: Rc>, + state: Arc>, registry: Rc, - main_sender: crossbeam_channel::Sender, + server_channel: ServerChannel, event_sender: pipewire::channel::Sender, ) { @@ -136,7 +138,7 @@ fn handle_node( return; } let properties = global.props.unwrap(); - let node = match properties.get(MEDIA_CLASS_PROPERTY_KEY) { + let mut node = match properties.get(MEDIA_CLASS_PROPERTY_KEY) { Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE) | Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK) => { let node: pipewire::node::Node = registry.bind(global).unwrap(); @@ -144,10 +146,11 @@ fn handle_node( } _ => return, }; - let mut state = state.borrow_mut(); + node.set_properties(dict_ref_to_hashmap(properties)); + let mut state = state.lock().unwrap(); if let Err(value) = state.insert_node(global.id.into(), node) { - main_sender - .send(MessageResponse::Error(value)) + server_channel + .fire(MessageResponse::Error(value)) .unwrap(); return; }; @@ -161,9 +164,9 @@ fn handle_node( fn handle_port( global: &GlobalObject<&spa::utils::dict::DictRef>, - state: Rc>, + state: Arc>, registry: Rc, - main_sender: crossbeam_channel::Sender, + server_channel: ServerChannel, event_sender: pipewire::channel::Sender, ) { @@ -171,7 +174,7 @@ fn handle_port( } let properties = global.props.unwrap(); - debug_dict_ref(properties); + // debug_dict_ref(properties); let port: pipewire::port::Port = registry.bind(global).unwrap(); @@ -200,9 +203,9 @@ fn handle_port( fn handle_link( global: &GlobalObject<&spa::utils::dict::DictRef>, - state: Rc>, + state: Arc>, registry: Rc, - main_sender: crossbeam_channel::Sender, + server_channel: ServerChannel, event_sender: pipewire::channel::Sender, ) { @@ -210,7 +213,7 @@ fn handle_link( } let properties = global.props.unwrap(); - debug_dict_ref(properties); + // debug_dict_ref(properties); let link: pipewire::link::Link = registry.bind(global).unwrap(); // link. diff --git a/pipewire-client/src/client/handlers/request.rs b/pipewire-client/src/client/handlers/request.rs index 2b7804948..109946fbd 100644 --- a/pipewire-client/src/client/handlers/request.rs +++ b/pipewire-client/src/client/handlers/request.rs @@ -1,177 +1,296 @@ +use crate::client::channel::{Request, ServerChannel}; use crate::constants::*; use crate::error::Error; +use crate::listeners::PipewireCoreSync; use crate::messages::{MessageRequest, MessageResponse, StreamCallback}; -use crate::states::{GlobalId, GlobalObjectState, GlobalState, OrphanState, StreamState}; -use crate::utils::PipewireCoreSync; +use crate::states::{GlobalId, GlobalObjectState, GlobalState, NodeState, OrphanState, StreamState}; use crate::{AudioStreamInfo, Direction, NodeInfo}; use pipewire::proxy::ProxyT; use std::cell::RefCell; -use std::collections::HashMap; use std::rc::Rc; +#[cfg(test)] +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use pipewire_common::utils::dict_ref_to_hashmap; + +struct Context { + request: Request, + core: Rc, + core_sync: Rc, + main_loop: pipewire::main_loop::MainLoop, + state: Arc>, + server_channel: ServerChannel, +} + pub(super) fn request_handler( core: Rc, core_sync: Rc, main_loop: pipewire::main_loop::MainLoop, - state: Rc>, - main_sender: crossbeam_channel::Sender, -) -> impl Fn(MessageRequest) + 'static + state: Arc>, + server_channel: ServerChannel, +) -> impl Fn(Request) + 'static { - move |message_request: MessageRequest| match message_request { - MessageRequest::Quit => main_loop.quit(), - MessageRequest::Settings => { - handle_settings( - state.clone(), - main_sender.clone(), - ) - } - MessageRequest::DefaultAudioNodes => { - handle_default_audio_nodes( - state.clone(), - main_sender.clone() - ) - }, - MessageRequest::CreateNode { - name, - description, - nickname, - direction, - channels, - } => { - handle_create_node( + move |request| { + let message_request = request.message.clone(); + let context = Context { + request, + core: core.clone(), + core_sync: core_sync.clone(), + main_loop: main_loop.clone(), + state: state.clone(), + server_channel: server_channel.clone(), + }; + match message_request { + MessageRequest::Quit => main_loop.quit(), + MessageRequest::Settings => handle_settings( + context, + ), + MessageRequest::DefaultAudioNodes => handle_default_audio_nodes( + context, + ), + MessageRequest::GetNode { + name, + direction + } => handle_get_node(context, name, direction), + MessageRequest::CreateNode { name, description, nickname, direction, channels, - core.clone(), - core_sync.clone(), - state.clone(), - main_sender.clone(), - ) - } - MessageRequest::EnumerateNodes(direction) => { - handle_enumerate_node( + } => handle_create_node( + context, + name, + description, + nickname, direction, - state.clone(), - main_sender.clone(), - ) - }, - MessageRequest::CreateStream { - node_id, - direction, - format, - callback - } => { - handle_create_stream( + channels, + ), + MessageRequest::DeleteNode(id) => handle_delete_node(context, id), + MessageRequest::EnumerateNodes(direction) => handle_enumerate_node( + context, + direction, + ), + MessageRequest::CreateStream { + node_id, + direction, + format, + callback + } => handle_create_stream( + context, node_id, direction, format, callback, - core.clone(), - state.clone(), - main_sender.clone(), - ) - } - MessageRequest::DeleteStream { name } => { - handle_delete_stream( + ), + MessageRequest::DeleteStream(name) => handle_delete_stream( + context, name, - state.clone(), - main_sender.clone() - ) - } - MessageRequest::ConnectStream { name } => { - handle_connect_stream( + ), + MessageRequest::ConnectStream(name) => handle_connect_stream( + context, name, - state.clone(), - main_sender.clone() - ) - } - MessageRequest::DisconnectStream { name } => { - handle_disconnect_stream( + ), + MessageRequest::DisconnectStream(name) => handle_disconnect_stream( + context, name, - state.clone(), - main_sender.clone() - ) - } - // Internal requests - MessageRequest::CheckSessionManagerRegistered => { - handle_check_session_manager_registered( - state.clone(), - main_sender.clone() - ) - } - MessageRequest::SettingsState => { - handle_settings_state( - state.clone(), - main_sender.clone() - ) - } - MessageRequest::DefaultAudioNodesState => { - handle_default_audio_nodes_state( - state.clone(), - main_sender.clone() - ) - } - MessageRequest::NodeState(id) => { - handle_node_state( + ), + // Internal requests + MessageRequest::CheckSessionManagerRegistered => handle_check_session_manager_registered( + context, + ), + MessageRequest::SettingsState => handle_settings_state( + context, + ), + MessageRequest::DefaultAudioNodesState => handle_default_audio_nodes_state( + context, + ), + MessageRequest::NodeState(id) => handle_node_state( + context, id, - state.clone(), - main_sender.clone() - ) - } - MessageRequest::NodeStates => { - handle_node_states( - state.clone(), - main_sender.clone() - ) - } - MessageRequest::NodeCount => { - handle_node_count( - state.clone(), - main_sender.clone() - ) - } - MessageRequest::Listeners => { - handle_listeners( - state.clone(), - main_sender.clone(), - core_sync.clone() - ) + ), + MessageRequest::NodeStates => handle_node_states( + context, + ), + MessageRequest::NodeCount => handle_node_count( + context, + ), + #[cfg(test)] + MessageRequest::Listeners => handle_listeners( + context, + ), } } } fn handle_settings( - state: Rc>, - main_sender: crossbeam_channel::Sender, + context: Context, ) { - let state = state.borrow(); + let state = context.state.lock().unwrap(); let settings = state.get_settings(); - main_sender.send(MessageResponse::Settings(settings)).unwrap(); + context.server_channel + .send(&context.request, MessageResponse::Settings(settings)) + .unwrap(); } fn handle_default_audio_nodes( - state: Rc>, - main_sender: crossbeam_channel::Sender, + context: Context, ) { - let state = state.borrow(); + let state = context.state.lock().unwrap(); let default_audio_devices = state.get_default_audio_nodes(); - main_sender.send(MessageResponse::DefaultAudioNodes(default_audio_devices)).unwrap(); + context.server_channel + .send(&context.request, MessageResponse::DefaultAudioNodes(default_audio_devices)) + .unwrap(); +} +fn handle_get_node( + context: Context, + name: String, + direction: Direction, +) +{ + let control_flow = RefCell::new(false); + let state = context.state.lock().unwrap(); + let default_audio_nodes = state.get_default_audio_nodes(); + let default_audio_node = match direction { + Direction::Input => default_audio_nodes.source.clone(), + Direction::Output => default_audio_nodes.sink.clone() + }; + let nodes = match state.get_nodes() { + Ok(value) => value, + Err(value) => { + context.server_channel + .send(&context.request, MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let node = nodes.iter() + .find_map(|(id, node)| { + let properties = node.properties().unwrap(); + let format = node.format().unwrap(); + let name_to_compare = match node.name() { + Ok(value) => value, + Err(value) => { + control_flow.replace(true); + context.server_channel + .send(&context.request, MessageResponse::Error(value)) + .unwrap(); + return None; + } + }; + let direction_to_compare = match node.direction() { + Ok(value) => value, + Err(value) => { + control_flow.replace(true); + context.server_channel + .send(&context.request, MessageResponse::Error(value)) + .unwrap(); + return None; + } + }; + if name_to_compare == name && direction_to_compare == direction { + Some((id, properties, format)) + } else { + None + } + }) + .iter() + .find_map(|(id, properties, format)| { + if *control_flow.borrow() == true { + return None; + } + let name = properties.get(*pipewire::keys::NODE_NAME).unwrap().clone(); + let description = properties + .get(*pipewire::keys::NODE_DESCRIPTION) + .unwrap() + .clone(); + let nickname = match properties.contains_key(*pipewire::keys::NODE_NICK) { + true => properties.get(*pipewire::keys::NODE_NICK).unwrap().clone(), + false => name.clone(), + }; + let is_default = name == default_audio_node; + Some(NodeInfo { + id: (**id).clone().into(), + name, + description, + nickname, + direction: direction.clone(), + is_default, + format: format.clone() + }) + }); + match node { + Some(value) => context.server_channel + .send(&context.request, MessageResponse::GetNode(value)) + .unwrap(), + None => context.server_channel + .send(&context.request, MessageResponse::Error(Error { + description: format!("Node with name({}) not found", name), + })) + .unwrap() + } + } fn handle_create_node( + context: Context, name: String, description: String, nickname: String, direction: Direction, channels: u16, - core: Rc, - core_sync: Rc, - state: Rc>, - main_sender: crossbeam_channel::Sender, ) { + { + let control_flow = RefCell::new(false); + let state = context.state.lock().unwrap(); + let nodes = match state.get_nodes() { + Ok(value) => value, + Err(value) => { + context.server_channel + .send(&context.request, MessageResponse::Error(value)) + .unwrap(); + return; + } + }; + let is_exists = nodes.iter().any(|(_, node)| { + if *control_flow.borrow() == true { + return false; + } + let name_to_compare = match node.name() { + Ok(value) => value, + Err(value) => { + control_flow.replace(true); + context.server_channel + .send(&context.request, MessageResponse::Error(value)) + .unwrap(); + return false; + } + }; + let direction_to_compare = match node.direction() { + Ok(value) => value, + Err(value) => { + control_flow.replace(true); + context.server_channel + .send(&context.request, MessageResponse::Error(value)) + .unwrap(); + return false; + } + }; + name_to_compare == name && direction_to_compare == direction + }); + if is_exists { + context.server_channel + .send( + &context.request, + MessageResponse::Error(Error { + description: format!("Node with name({}) already exists", name).to_string(), + } + )) + .unwrap(); + } + } let default_audio_position = format!( "[ {} ]", (1..=channels + 1) @@ -204,7 +323,7 @@ fn handle_create_node( _ => default_audio_position.as_str(), } }; - let node: pipewire::node::Node = match core + let node: pipewire::node::Node = match context.core .create_object("adapter", properties) .map_err(move |error| { Error { @@ -213,52 +332,60 @@ fn handle_create_node( }) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; } }; - let core_sync = core_sync.clone(); - let listener_main_sender = main_sender.clone(); - let listener_state = state.clone(); + let core_sync = context.core_sync.clone(); + let listener_server_channel = context.server_channel.clone(); + let listener_state = context.state.clone(); + let listener_properties = properties.clone(); core_sync.register( PIPEWIRE_CORE_SYNC_CREATE_DEVICE_SEQ, move |control_flow| { - let state = listener_state.borrow(); - let nodes = match state.get_nodes() { + let mut state = listener_state.lock().unwrap(); + let mut nodes = match state.get_nodes_mut() { Ok(value) => value, Err(value) => { - listener_main_sender - .send(MessageResponse::Error(value)) + listener_server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); control_flow.release(); return; } }; - let node = nodes.iter() + let node = nodes.iter_mut() .find(move |(_, node)| { node.state() == GlobalObjectState::Pending }); - if let None = node { - listener_main_sender - .send(MessageResponse::Error(Error { - description: "Created node not found".to_string(), - })) - .unwrap(); - } - else { - let node_id = node.unwrap().0; - listener_main_sender - .send(MessageResponse::CreateNode { - id: (*node_id).clone(), - }) - .unwrap(); + match node { + Some((id, node)) => { + let properties = dict_ref_to_hashmap(listener_properties.dict()); + node.set_properties(properties); + listener_server_channel + .send( + &context.request, + MessageResponse::CreateNode((*id).clone()) + ) + .unwrap(); + } + None => { + listener_server_channel + .send( + &context.request, + MessageResponse::Error(Error { + description: "Created node not found".to_string(), + }) + ) + .unwrap(); + } } control_flow.release(); } ); - let mut state = state.borrow_mut(); + let mut state = context.state.lock().unwrap(); // We need to store created node object as orphan since it had not been // registered by server at this point (does not have an id yet). // @@ -273,13 +400,31 @@ fn handle_create_node( let orphan = OrphanState::new(node.upcast()); state.insert_orphan(orphan); } +fn handle_delete_node( + context: Context, + id: GlobalId, +) +{ + match context.state.lock().unwrap().delete_node(&id) { + Ok(_) => { + context.server_channel + .send(&context.request, MessageResponse::DeleteNode) + .unwrap() + } + Err(value) => { + context.server_channel + .send(&context.request, MessageResponse::Error(value)) + .unwrap() + } + }; +} + fn handle_enumerate_node( + context: Context, direction: Direction, - state: Rc>, - main_sender: crossbeam_channel::Sender, ) { - let state = state.borrow(); + let state = context.state.lock().unwrap(); let default_audio_nodes = state.get_default_audio_nodes(); let default_audio_node = match direction { Direction::Input => default_audio_nodes.source.clone(), @@ -292,8 +437,8 @@ fn handle_enumerate_node( let nodes = match state.get_nodes() { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; } @@ -301,7 +446,7 @@ fn handle_enumerate_node( let nodes: Vec = nodes .iter() .filter_map(|(id, node)| { - let properties = node.properties(); + let properties = node.properties().unwrap(); let format = node.format().unwrap(); if properties.iter().any(|(_, v)| v == filter_value) { Some((id, properties, format)) @@ -331,24 +476,32 @@ fn handle_enumerate_node( } }) .collect(); - main_sender.send(MessageResponse::EnumerateNodes(nodes)).unwrap(); + context.server_channel.send(&context.request, MessageResponse::EnumerateNodes(nodes)).unwrap(); } fn handle_create_stream( + context: Context, node_id: GlobalId, direction: Direction, format: AudioStreamInfo, callback: StreamCallback, - core: Rc, - state: Rc>, - main_sender: crossbeam_channel::Sender, ) { - let mut state = state.borrow_mut(); + let mut state = context.state.lock().unwrap(); let node_name = match state.get_node(&node_id) { - Ok(value) => value.name(), + Ok(value) => { + match value.name() { + Ok(value) => value, + Err(value) => { + context.server_channel + .send(&context.request, MessageResponse::Error(value)) + .unwrap(); + return; + } + } + }, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; } @@ -362,14 +515,14 @@ fn handle_create_stream( } }; let properties = pipewire::properties::properties! { - *pipewire::keys::MEDIA_TYPE => MEDIA_TYPE_PROPERTY_VALUE_AUDIO, - *pipewire::keys::MEDIA_CLASS => match direction { - Direction::Input => MEDIA_CLASS_PROPERTY_VALUE_STREAM_INPUT_AUDIO, - Direction::Output => MEDIA_CLASS_PROPERTY_VALUE_STREAM_OUTPUT_AUDIO, - }, - }; + *pipewire::keys::MEDIA_TYPE => MEDIA_TYPE_PROPERTY_VALUE_AUDIO, + *pipewire::keys::MEDIA_CLASS => match direction { + Direction::Input => MEDIA_CLASS_PROPERTY_VALUE_STREAM_INPUT_AUDIO, + Direction::Output => MEDIA_CLASS_PROPERTY_VALUE_STREAM_OUTPUT_AUDIO, + }, + }; let stream = match pipewire::stream::Stream::new( - &core, + &context.core, stream_name.clone().as_str(), properties, ) @@ -380,8 +533,8 @@ fn handle_create_stream( }) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; } @@ -394,100 +547,97 @@ fn handle_create_stream( ); stream.add_process_listener(callback); if let Err(value) = state.insert_stream(stream_name.clone(), stream) { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; }; - main_sender - .send(MessageResponse::CreateStream { - name: stream_name.clone(), - }) + context.server_channel + .send( + &context.request, + MessageResponse::CreateStream(stream_name.clone()) + ) .unwrap(); } fn handle_delete_stream( + context: Context, name: String, - state: Rc>, - main_sender: crossbeam_channel::Sender, ) { - let mut state = state.borrow_mut(); + let mut state = context.state.lock().unwrap(); let stream = match state.get_stream_mut(&name) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; } }; if stream.is_connected() { if let Err(value) = stream.disconnect() { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; }; } - if let Err(value) = state.remove_stream(&name) { - main_sender - .send(MessageResponse::Error(value)) + if let Err(value) = state.delete_stream(&name) { + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; }; - main_sender.send(MessageResponse::DeleteStream).unwrap(); + context.server_channel.send(&context.request, MessageResponse::DeleteStream).unwrap(); } fn handle_connect_stream( + context: Context, name: String, - state: Rc>, - main_sender: crossbeam_channel::Sender, ) { - let mut state = state.borrow_mut(); + let mut state = context.state.lock().unwrap(); let stream = match state.get_stream_mut(&name) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; } }; if let Err(value) = stream.connect() { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; }; - main_sender.send(MessageResponse::ConnectStream).unwrap(); + context.server_channel.send(&context.request, MessageResponse::ConnectStream).unwrap(); } fn handle_disconnect_stream( + context: Context, name: String, - state: Rc>, - main_sender: crossbeam_channel::Sender, ) { - let mut state = state.borrow_mut(); + let mut state = context.state.lock().unwrap(); let stream = match state.get_stream_mut(&name) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; } }; if let Err(value) = stream.disconnect() { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; }; - main_sender.send(MessageResponse::DisconnectStream).unwrap(); + context.server_channel.send(&context.request, MessageResponse::DisconnectStream).unwrap(); } fn handle_check_session_manager_registered( - state: Rc>, - main_sender: crossbeam_channel::Sender, + context: Context, ) { pub(crate) fn generate_error_message(session_managers: &Vec<&str>) -> String { @@ -515,7 +665,7 @@ fn handle_check_session_manager_registered( APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION ]; let error_description = generate_error_message(&session_managers); - let state = state.borrow_mut(); + let state = context.state.lock().unwrap(); let clients = state.get_clients().map_err(|_| { Error { description: error_description.clone(), @@ -524,8 +674,8 @@ fn handle_check_session_manager_registered( let clients = match clients { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; } @@ -534,67 +684,68 @@ fn handle_check_session_manager_registered( .any(|(_, client)| { session_managers.contains(&client.name.as_str()) }); - main_sender - .send(MessageResponse::CheckSessionManagerRegistered { - session_manager_registered, - error: match session_manager_registered { - true => Some(Error { - description: error_description.clone() - }), - false => None - }, - }) + context.server_channel + .send( + &context.request, + MessageResponse::CheckSessionManagerRegistered { + session_manager_registered, + error: match session_manager_registered { + true => Some(Error { + description: error_description.clone() + }), + false => None + }, + } + ) .unwrap(); } fn handle_settings_state( - state: Rc>, - main_sender: crossbeam_channel::Sender, + context: Context, ) { - let state = state.borrow_mut(); - main_sender - .send(MessageResponse::SettingsState(state.get_settings().state)) + let state = context.state.lock().unwrap(); + context.server_channel + .send(&context.request, MessageResponse::SettingsState(state.get_settings().state)) .unwrap(); } fn handle_default_audio_nodes_state( - state: Rc>, - main_sender: crossbeam_channel::Sender, + context: Context, ) { - let state = state.borrow_mut(); - main_sender - .send(MessageResponse::DefaultAudioNodesState(state.get_default_audio_nodes().state)) + let state = context.state.lock().unwrap(); + context.server_channel + .send(&context.request, MessageResponse::DefaultAudioNodesState(state.get_default_audio_nodes().state)) .unwrap(); } fn handle_node_state( + context: Context, id: GlobalId, - state: Rc>, - main_sender: crossbeam_channel::Sender, ) { - let state = state.borrow(); + let state = context.state.lock().unwrap(); let node = match state.get_node(&id) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; } }; let state = node.state(); - main_sender.send(MessageResponse::NodeState(state)).unwrap(); + context.server_channel + .send(&context.request, MessageResponse::NodeState(state)) + .unwrap(); } fn handle_node_states( - state: Rc>, - main_sender: crossbeam_channel::Sender, + context: Context, ) { - let state = state.borrow_mut(); + let state = context.state.lock().unwrap(); let nodes = match state.get_nodes() { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(value)) + context.server_channel + .send(&context.request, MessageResponse::Error(value)) .unwrap(); return; } @@ -604,62 +755,66 @@ fn handle_node_states( node.state() }) .collect::>(); - main_sender.send(MessageResponse::NodeStates(states)).unwrap(); + context.server_channel + .send(&context.request, MessageResponse::NodeStates(states)) + .unwrap(); } fn handle_node_count( - state: Rc>, - main_sender: crossbeam_channel::Sender, + context: Context, ) { - let state = state.borrow_mut(); + let state = context.state.lock().unwrap(); match state.get_nodes() { Ok(value) => { - main_sender - .send(MessageResponse::NodeCount(value.len() as u32)) + context.server_channel + .send(&context.request, MessageResponse::NodeCount(value.len() as u32)) .unwrap(); }, Err(_) => { - main_sender - .send(MessageResponse::NodeCount(0)) + context.server_channel + .send(&context.request, MessageResponse::NodeCount(0)) .unwrap(); } }; } +#[cfg(test)] fn handle_listeners( - state: Rc>, - main_sender: crossbeam_channel::Sender, - core_sync: Rc + context: Context, ) { + let state = context.state.lock().unwrap(); let mut core = HashMap::new(); - core.insert("0".to_string(), core_sync.get_listener_names()); - let metadata = state.borrow().get_metadatas() + core.insert("0".to_string(), context.core_sync.get_listener_names()); + let metadata = state.get_metadatas() .unwrap_or_default() .iter() .map(move |(id, metadata)| { (id.to_string(), metadata.get_listener_names()) }) .collect::>(); - let nodes = state.borrow().get_nodes() + let nodes = state.get_nodes() .unwrap_or_default() .iter() .map(move |(id, node)| { (id.to_string(), node.get_listener_names()) }) .collect::>(); - let streams = state.borrow().get_streams() + let streams = state.get_streams() .unwrap_or_default() .iter() .map(move |(name, stream)| { ((*name).clone(), stream.get_listener_names()) }) .collect::>(); - main_sender - .send(MessageResponse::Listeners { - core, - metadata, - nodes, - streams, - }) + context.server_channel + .send( + &context.request, + MessageResponse::Listeners { + core, + metadata, + nodes, + streams, + } + ) .unwrap(); } \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/thread.rs b/pipewire-client/src/client/handlers/thread.rs index 9d1fed15f..00b806c2c 100644 --- a/pipewire-client/src/client/handlers/thread.rs +++ b/pipewire-client/src/client/handlers/thread.rs @@ -6,17 +6,33 @@ use crate::constants::{PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ, PIPEWIRE_RUNTIME_D use crate::error::Error; use crate::messages::{EventMessage, MessageRequest, MessageResponse}; use crate::states::GlobalState; -use crate::utils::PipewireCoreSync; use std::cell::RefCell; use std::rc::Rc; +use std::sync::{Arc, Mutex, Once}; +use libc::atexit; +use crate::client::channel::ServerChannel; +use crate::listeners::PipewireCoreSync; + +static AT_EXIT: Once = Once::new(); + +extern "C" fn at_exit_callback() { + unsafe { pipewire::deinit(); } +} pub fn pw_thread( client_info: PipewireClientInfo, - main_sender: crossbeam_channel::Sender, - pw_receiver: pipewire::channel::Receiver, + mut server_channel: ServerChannel, event_sender: pipewire::channel::Sender, event_receiver: pipewire::channel::Receiver, ) { + pipewire::init(); + + AT_EXIT.call_once(|| { + unsafe { + atexit(at_exit_callback); + } + }); + let connection_properties = Some(pipewire::properties::properties! { PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY => client_info.socket_location, *pipewire::keys::REMOTE_NAME => client_info.socket_name, @@ -26,8 +42,8 @@ pub fn pw_thread( let main_loop = match pipewire::main_loop::MainLoop::new(None) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(Error { + server_channel + .fire(MessageResponse::Error(Error { description: format!("Failed to create PipeWire main loop: {}", value), })) .unwrap(); @@ -38,20 +54,20 @@ pub fn pw_thread( let context = match pipewire::context::Context::new(&main_loop) { Ok(value) => Rc::new(value), Err(value) => { - main_sender - .send(MessageResponse::Error(Error { + server_channel + .fire(MessageResponse::Error(Error { description: format!("Failed to create PipeWire context: {}", value), })) .unwrap(); return; } }; - - let core = match context.connect(connection_properties) { + + let core = match context.connect(connection_properties.clone()) { Ok(value) => value, Err(value) => { - main_sender - .send(MessageResponse::Error(Error { + server_channel + .fire(MessageResponse::Error(Error { description: format!("Failed to connect PipeWire server: {}", value), })) .unwrap(); @@ -59,12 +75,12 @@ pub fn pw_thread( } }; - let listener_main_sender = main_sender.clone(); + let listener_main_sender = server_channel.clone(); let _core_listener = core .add_listener_local() .error(move |_, _, _, message| { listener_main_sender - .send(MessageResponse::Error(Error { + .fire(MessageResponse::Error(Error { description: format!("Server error: {}", message), })) .unwrap(); @@ -74,8 +90,8 @@ pub fn pw_thread( let registry = match core.get_registry() { Ok(value) => Rc::new(value), Err(value) => { - main_sender - .send(MessageResponse::Error(Error { + server_channel + .fire(MessageResponse::Error(Error { description: format!("Failed to get Pipewire registry: {}", value), })) .unwrap(); @@ -85,14 +101,14 @@ pub fn pw_thread( let core_sync = Rc::new(PipewireCoreSync::new(Rc::new(RefCell::new(core.clone())))); let core = Rc::new(core); - let state = Rc::new(RefCell::new(GlobalState::default())); + let state = Arc::new(Mutex::new(GlobalState::default())); - let listener_main_sender = main_sender.clone(); + let listener_main_sender = server_channel.clone(); core_sync.register( PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ, move |control_flow| { listener_main_sender - .send(MessageResponse::Initialized) + .fire(MessageResponse::Initialized) .unwrap(); control_flow.release(); } @@ -102,19 +118,19 @@ pub fn pw_thread( main_loop.loop_(), event_handler( state.clone(), - main_sender.clone(), + server_channel.clone(), event_sender.clone() ) ); - let _attached_pw_receiver = pw_receiver.attach( + let _attached_pw_receiver = server_channel.attach( main_loop.loop_(), request_handler( core.clone(), core_sync.clone(), main_loop.clone(), state.clone(), - main_sender.clone() + server_channel.clone() ) ); @@ -123,11 +139,11 @@ pub fn pw_thread( .global(registry_global_handler( state.clone(), registry.clone(), - main_sender.clone(), + server_channel.clone(), event_sender.clone(), )) .global_remove(move |global_id| { - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); state.remove(&global_id.into()) }) .register(); diff --git a/pipewire-client/src/client/implementation.rs b/pipewire-client/src/client/implementation.rs index b51cbe5ea..7acb23f42 100644 --- a/pipewire-client/src/client/implementation.rs +++ b/pipewire-client/src/client/implementation.rs @@ -1,6 +1,8 @@ extern crate pipewire; +use std::thread; use crate::client::api::{CoreApi, InternalApi, NodeApi, StreamApi}; +use crate::client::channel::channels; use crate::client::connection_string::{PipewireClientInfo, PipewireClientSocketPath}; use crate::client::handlers::thread; use crate::error::Error; @@ -12,8 +14,9 @@ use std::path::PathBuf; use std::string::ToString; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; -use std::thread; use std::thread::JoinHandle; +use std::time::Duration; +use tokio::runtime::Runtime; pub(super) static CLIENT_NAME_PREFIX: &str = "pipewire-client"; pub(super) static CLIENT_INDEX: AtomicU32 = AtomicU32::new(0); @@ -22,6 +25,7 @@ pub struct PipewireClient { pub(crate) name: String, socket_path: PathBuf, thread_handle: Option>, + timeout: Duration, internal_api: Arc, core_api: CoreApi, node_api: NodeApi, @@ -29,7 +33,10 @@ pub struct PipewireClient { } impl PipewireClient { - pub fn new() -> Result { + pub fn new( + runtime: Arc, + timeout: Duration, + ) -> Result { let name = format!("{}-{}", CLIENT_NAME_PREFIX, CLIENT_INDEX.load(Ordering::SeqCst)); CLIENT_INDEX.fetch_add(1, Ordering::SeqCst); @@ -40,20 +47,18 @@ impl PipewireClient { socket_location: socket_path.parent().unwrap().to_str().unwrap().to_string(), socket_name: socket_path.file_name().unwrap().to_str().unwrap().to_string(), }; - - let (main_sender, main_receiver) = crossbeam_channel::unbounded(); - let (pw_sender, pw_receiver) = pipewire::channel::channel(); + + let (client_channel, server_channel) = channels(runtime.clone()); let (event_sender, event_receiver) = pipewire::channel::channel::(); let pw_thread = thread::spawn(move || thread( client_info, - main_sender, - pw_receiver, + server_channel, event_sender, event_receiver )); - let internal_api = Arc::new(InternalApi::new(pw_sender, main_receiver)); + let internal_api = Arc::new(InternalApi::new(client_channel, timeout.clone())); let core_api = CoreApi::new(internal_api.clone()); let node_api = NodeApi::new(internal_api.clone()); let stream_api = StreamApi::new(internal_api.clone()); @@ -62,6 +67,7 @@ impl PipewireClient { name, socket_path, thread_handle: Some(pw_thread), + timeout, internal_api, core_api, node_api, @@ -76,26 +82,26 @@ impl PipewireClient { }; match client.wait_post_initialization() { Ok(_) => {} - Err(value) => return Err(Error { - description: format!("Post initialization error: {}", value), - }), + Err(value) => { + let global_messages = &client.internal_api.channel.global_messages; + return Err(Error { + description: format!("Post initialization error: {}", value), + }) + }, }; Ok(client) } fn wait_initialization(&self) -> Result<(), Error> { - let timeout_duration = std::time::Duration::from_millis(10 * 1000); - let response = self.internal_api.wait_response_with_timeout(timeout_duration); + let response = self.internal_api.wait_response_with_timeout(self.timeout); let response = match response { Ok(value) => value, Err(value) => { // Timeout is certainly due to missing session manager - // We need to check if that the case. If session manager is running then we return + // We need to check if that's the case. If session manager is running then we return // timeout error. return match self.core_api.check_session_manager_registered() { - Ok(_) => Err(Error { - description: value.to_string(), - }), + Ok(_) => Err(value), Err(value) => Err(value) }; } @@ -110,11 +116,10 @@ impl PipewireClient { fn wait_post_initialization(&self) -> Result<(), Error> { let mut settings_initialized = false; - let mut default_audio_devices_initialized = false; + let mut default_audio_nodes_initialized = false; let mut nodes_initialized = false; - let timeout_duration = std::time::Duration::from_millis(1); self.core_api.check_session_manager_registered()?; - match self.node_api.get_count() { + match self.node_api.count() { Ok(value) => { if value == 0 { return Err(Error { @@ -124,72 +129,28 @@ impl PipewireClient { } Err(value) => return Err(value), } - self.core_api.get_settings_state()?; - self.core_api.get_default_audio_nodes_state()?; - self.node_api.get_states()?; let operation = move || { - let response = self.internal_api.wait_response_with_timeout(timeout_duration); - match response { - Ok(value) => match value { - MessageResponse::SettingsState(state) => { - match state { - GlobalObjectState::Initialized => { - settings_initialized = true; - } - _ => { - self.core_api.get_settings_state()?; - return Err(Error { - description: "Settings not yet initialized".to_string(), - }) - } - }; - }, - MessageResponse::DefaultAudioNodesState(state) => { - match state { - GlobalObjectState::Initialized => { - default_audio_devices_initialized = true; - } - _ => { - self.core_api.get_default_audio_nodes_state()?; - return Err(Error { - description: "Default audio nodes not yet initialized".to_string(), - }) - } - } - }, - MessageResponse::NodeStates(states) => { - let condition = states.iter() - .all(|state| *state == GlobalObjectState::Initialized); - match condition { - true => { - nodes_initialized = true; - }, - false => { - self.node_api.get_states()?; - return Err(Error { - description: "All nodes should be initialized at this point".to_string(), - }) - } - }; - } - MessageResponse::Error(value) => return Err(value), - _ => return Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), + if settings_initialized == false { + let settings_state = self.core_api.get_settings_state()?; + if settings_state == GlobalObjectState::Initialized { + settings_initialized = true; } - Err(_) => return Err(Error { - description: format!( - r"Timeout: - - settings: {} - - default audio nodes: {} - - nodes: {}", - settings_initialized, - default_audio_devices_initialized, - nodes_initialized - ), - }) - }; - if settings_initialized == false || default_audio_devices_initialized == false || nodes_initialized == false { + } + if default_audio_nodes_initialized == false { + let default_audio_nodes_state = self.core_api.get_default_audio_nodes_state()?; + if default_audio_nodes_state == GlobalObjectState::Initialized { + default_audio_nodes_initialized = true; + } + } + if nodes_initialized == false { + let node_states = self.node_api.states()?; + let condition = node_states.iter() + .all(|state| *state == GlobalObjectState::Initialized); + if condition { + nodes_initialized = true; + } + } + if settings_initialized == false || default_audio_nodes_initialized == false || nodes_initialized == false { return Err(Error { description: format!( r"Conditions not yet initialized: @@ -197,18 +158,14 @@ impl PipewireClient { - default audio nodes: {} - nodes: {}", settings_initialized, - default_audio_devices_initialized, + default_audio_nodes_initialized, nodes_initialized ), }) } return Ok(()); }; - let mut backoff = Backoff::new( - 30, - std::time::Duration::from_millis(10), - std::time::Duration::from_millis(100), - ); + let mut backoff = Backoff::constant(self.timeout.as_millis()); backoff.retry(operation) } @@ -239,9 +196,7 @@ impl Drop for PipewireClient { fn drop(&mut self) { if self.internal_api.send_request_without_response(&MessageRequest::Quit).is_ok() { if let Some(thread_handle) = self.thread_handle.take() { - if let Err(err) = thread_handle.join() { - panic!("Failed to join PipeWire thread: {:?}", err); - } + thread_handle.join().unwrap(); } } else { panic!("Failed to send Quit message to PipeWire thread."); diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs index cd2a73402..95e712a39 100644 --- a/pipewire-client/src/client/implementation_test.rs +++ b/pipewire-client/src/client/implementation_test.rs @@ -1,13 +1,20 @@ -use crate::client::implementation::CLIENT_NAME_PREFIX; +use crate::client::implementation::{CLIENT_INDEX, CLIENT_NAME_PREFIX}; use crate::states::{MetadataState, NodeState}; -use crate::test_utils::fixtures::{client2, PipewireTestClient}; -use crate::test_utils::server::{server_with_default_configuration, server_without_node, server_without_session_manager, set_socket_env_vars, Container}; -use crate::utils::PipewireCoreSync; +use crate::test_utils::fixtures::{client2, shared_client, PipewireTestClient}; use crate::PipewireClient; use rstest::rstest; +use serial_test::serial; use std::any::TypeId; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use tokio::runtime::Runtime; +use pipewire_test_utils::environment::TEST_ENVIRONMENT; +use pipewire_test_utils::server::{server_with_default_configuration, server_without_node, server_without_session_manager, Server}; +use crate::listeners::PipewireCoreSync; #[rstest] +#[serial] pub fn names( #[from(client2)] (client_1, client_2): (PipewireTestClient, PipewireTestClient) ) { @@ -19,9 +26,23 @@ pub fn names( } #[rstest] -pub fn with_default_configuration(server_with_default_configuration: Container) { - set_socket_env_vars(&server_with_default_configuration); - let client = PipewireClient::new().unwrap(); +#[serial] +#[ignore] +fn init100(#[from(server_with_default_configuration)] _server: Arc) { + for index in 0..100 { + thread::sleep(Duration::from_millis(10)); + println!("Init client: {}", index); + let _ = PipewireClient::new( + Arc::new(Runtime::new().unwrap()), + TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), + ).unwrap(); + assert_eq!(index + 1, CLIENT_INDEX.load(std::sync::atomic::Ordering::SeqCst)); + } +} + +#[rstest] +#[serial] +pub fn with_default_configuration(#[from(shared_client)] client: PipewireTestClient) { let listeners = client.core().get_listeners().unwrap(); let core_listeners = listeners.get(&TypeId::of::()).unwrap(); let metadata_listeners = listeners.get(&TypeId::of::()).unwrap(); @@ -39,15 +60,21 @@ pub fn with_default_configuration(server_with_default_configuration: Container) } #[rstest] -pub fn without_session_manager(server_without_session_manager: Container) { - set_socket_env_vars(&server_without_session_manager); - let error = PipewireClient::new().unwrap_err(); +#[serial] +pub fn without_session_manager(#[from(server_without_session_manager)] _server: Arc) { + let error = PipewireClient::new( + Arc::new(Runtime::new().unwrap()), + TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), + ).unwrap_err(); assert_eq!(true, error.description.contains("No session manager registered")) } #[rstest] -pub fn without_node(server_without_node: Container) { - set_socket_env_vars(&server_without_node); - let error = PipewireClient::new().unwrap_err(); +#[serial] +pub fn without_node(#[from(server_without_node)] _server: Arc) { + let error = PipewireClient::new( + Arc::new(Runtime::new().unwrap()), + TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), + ).unwrap_err(); assert_eq!("Post initialization error: Zero node registered", error.description) } \ No newline at end of file diff --git a/pipewire-client/src/client/mod.rs b/pipewire-client/src/client/mod.rs index b51a38ca4..3b8a656c9 100644 --- a/pipewire-client/src/client/mod.rs +++ b/pipewire-client/src/client/mod.rs @@ -3,8 +3,15 @@ pub use implementation::PipewireClient; mod connection_string; mod handlers; mod api; +mod channel; + +#[cfg(test)] pub(super) use api::CoreApi; #[cfg(test)] #[path = "./implementation_test.rs"] -mod implementation_test; \ No newline at end of file +mod implementation_test; + +#[cfg(test)] +#[path = "./channel_test.rs"] +mod channel_test; \ No newline at end of file diff --git a/pipewire-client/src/constants.rs b/pipewire-client/src/constants.rs deleted file mode 100644 index c7057c400..000000000 --- a/pipewire-client/src/constants.rs +++ /dev/null @@ -1,33 +0,0 @@ -pub(super) const PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY: &str = "PIPEWIRE_RUNTIME_DIR"; -pub(super) const PIPEWIRE_CORE_ENVIRONMENT_KEY: &str = "PIPEWIRE_CORE"; -pub(super) const PIPEWIRE_REMOTE_ENVIRONMENT_KEY: &str = "PIPEWIRE_REMOTE"; -pub(super) const XDG_RUNTIME_DIR_ENVIRONMENT_KEY: &str = "XDG_RUNTIME_DIR"; -pub(super) const PIPEWIRE_REMOTE_ENVIRONMENT_DEFAULT: &str = "pipewire-0"; - -pub(super) const PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ :u32 = 0; -pub(super) const PIPEWIRE_CORE_SYNC_CREATE_DEVICE_SEQ :u32 = 1; - -pub(super) const MEDIA_TYPE_PROPERTY_VALUE_AUDIO: &str = "Audio"; -pub(super) const MEDIA_CLASS_PROPERTY_KEY: &str = "media.class"; -pub(super) const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE: &str = "Audio/Source"; -pub(super) const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK: &str = "Audio/Sink"; -pub(super) const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_DUPLEX: &str = "Audio/Duplex"; -pub(super) const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_DEVICE: &str = "Audio/Device"; -pub(super) const MEDIA_CLASS_PROPERTY_VALUE_STREAM_OUTPUT_AUDIO: &str = "Stream/Output/Audio"; -pub(super) const MEDIA_CLASS_PROPERTY_VALUE_STREAM_INPUT_AUDIO: &str = "Stream/Input/Audio"; -pub(super) const METADATA_NAME_PROPERTY_KEY: &str = "metadata.name"; -pub(super) const METADATA_NAME_PROPERTY_VALUE_SETTINGS: &str = "settings"; -pub(super) const METADATA_NAME_PROPERTY_VALUE_DEFAULT: &str = "default"; -pub(super) const CLOCK_RATE_PROPERTY_KEY: &str = "clock.rate"; -pub(super) const CLOCK_QUANTUM_PROPERTY_KEY: &str = "clock.quantum"; -pub(super) const CLOCK_QUANTUM_MIN_PROPERTY_KEY: &str = "clock.min-quantum"; -pub(super) const CLOCK_QUANTUM_MAX_PROPERTY_KEY: &str = "clock.max-quantum"; -pub(super) const CLOCK_ALLOWED_RATES_PROPERTY_KEY: &str = "clock.allowed-rates"; -pub(super) const MONITOR_CHANNEL_VOLUMES_PROPERTY_KEY: &str = "monitor.channel-volumes"; -pub(super) const MONITOR_PASSTHROUGH_PROPERTY_KEY: &str = "monitor.passthrough"; -pub(super) const DEFAULT_AUDIO_SINK_PROPERTY_KEY: &str = "default.audio.sink"; -pub(super) const DEFAULT_AUDIO_SOURCE_PROPERTY_KEY: &str = "default.audio.source"; -pub(super) const AUDIO_POSITION_PROPERTY_KEY: &str = "audio.position"; -pub(super) const APPLICATION_NAME_PROPERTY_KEY: &str = "application.name"; -pub(super) const APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER: &str = "WirePlumber"; -pub(super) const APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION: &str = "pipewire-media-session"; \ No newline at end of file diff --git a/pipewire-client/src/lib.rs b/pipewire-client/src/lib.rs index e38d942ba..f2b752d54 100644 --- a/pipewire-client/src/lib.rs +++ b/pipewire-client/src/lib.rs @@ -1,20 +1,19 @@ +use pipewire_common::error as error; +use pipewire_common::utils as utils; +pub use pipewire_common::utils::Direction; +pub use pipewire_common::constants as constants; + mod client; pub use client::PipewireClient; -mod constants; mod listeners; mod messages; mod states; -mod utils; -pub use utils::Direction; - -mod error; - mod info; #[cfg(test)] -mod test_utils; +pub mod test_utils; pub use info::AudioStreamInfo; pub use info::NodeInfo; diff --git a/pipewire-client/src/listeners.rs b/pipewire-client/src/listeners.rs index a5051832b..273550aa7 100644 --- a/pipewire-client/src/listeners.rs +++ b/pipewire-client/src/listeners.rs @@ -68,4 +68,61 @@ impl Listeners { } listeners.remove(name); } +} + +pub(super) struct PipewireCoreSync { + core: Rc>, + listeners: Rc>>, +} + +impl PipewireCoreSync { + pub fn new(core: Rc>) -> Self { + Self { + core, + listeners: Rc::new(RefCell::new(Listeners::new())), + } + } + + pub(super) fn get_listener_names(&self) -> Vec { + self.listeners.borrow().get_names() + } + + pub fn register(&self, seq: u32, callback: F) + where + F: Fn(&mut ListenerControlFlow) + 'static, + { + let sync_id = self.core.borrow_mut().sync(seq as i32).unwrap(); + let name = format!("sync-{}", sync_id.raw()); + let listeners = self.listeners.clone(); + let listener_name = name.clone(); + let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); + let listener_control_flow = control_flow.clone(); + let listener = self + .core + .borrow_mut() + .add_listener_local() + .done(move |_, seq| { + if seq != sync_id { + return; + } + if listener_control_flow.borrow().is_released() { + return; + } + callback(&mut listener_control_flow.borrow_mut()); + listeners.borrow_mut().triggered(&listener_name); + }) + .register(); + self.listeners + .borrow_mut() + .add(name, Listener::new(listener, control_flow)); + } +} + +impl Clone for PipewireCoreSync { + fn clone(&self) -> Self { + Self { + core: self.core.clone(), + listeners: self.listeners.clone(), + } + } } \ No newline at end of file diff --git a/pipewire-client/src/messages.rs b/pipewire-client/src/messages.rs index 5ae9407a9..f541d2697 100644 --- a/pipewire-client/src/messages.rs +++ b/pipewire-client/src/messages.rs @@ -42,6 +42,11 @@ pub(super) enum MessageRequest { Quit, Settings, DefaultAudioNodes, + // Node + GetNode { + name: String, + direction: Direction, + }, CreateNode { name: String, description: String, @@ -49,22 +54,18 @@ pub(super) enum MessageRequest { direction: Direction, channels: u16, }, + DeleteNode(GlobalId), EnumerateNodes(Direction), + // Stream CreateStream { node_id: GlobalId, direction: Direction, format: AudioStreamInfo, callback: StreamCallback, }, - DeleteStream { - name: String - }, - ConnectStream { - name: String - }, - DisconnectStream { - name: String - }, + DeleteStream(String), + ConnectStream(String), + DisconnectStream(String), // Internal requests CheckSessionManagerRegistered, SettingsState, @@ -72,6 +73,7 @@ pub(super) enum MessageRequest { NodeState(GlobalId), NodeStates, NodeCount, + #[cfg(test)] Listeners } @@ -81,13 +83,13 @@ pub(super) enum MessageResponse { Initialized, Settings(SettingsState), DefaultAudioNodes(DefaultAudioNodesState), - CreateNode { - id: GlobalId - }, + // Nodes + GetNode(NodeInfo), + CreateNode(GlobalId), + DeleteNode, EnumerateNodes(Vec), - CreateStream { - name: String, - }, + // Streams + CreateStream(String), DeleteStream, ConnectStream, DisconnectStream, @@ -102,6 +104,7 @@ pub(super) enum MessageResponse { NodeStates(Vec), NodeCount(u32), // For testing purpose only + #[cfg(test)] Listeners { core: HashMap>, metadata: HashMap>, diff --git a/pipewire-client/src/states.rs b/pipewire-client/src/states.rs index e0fe20752..308384805 100644 --- a/pipewire-client/src/states.rs +++ b/pipewire-client/src/states.rs @@ -14,6 +14,8 @@ use std::fmt::{Display, Formatter}; use std::io::Cursor; use std::rc::Rc; use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use pipewire::proxy::ProxyT; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub(super) struct GlobalId(u32); @@ -149,6 +151,16 @@ impl GlobalState { Ok(()) } + pub fn delete_node(&mut self, id: &GlobalId) -> Result<(), Error> { + if self.nodes.contains_key(id) == false { + return Err(Error { + description: format!("Node with id({}) not found", id), + }); + } + self.nodes.remove(id); + Ok(()) + } + pub fn get_node(&self, id: &GlobalId) -> Result<&NodeState, Error> { self.nodes.get(id).ok_or(Error { description: format!("Node with id({}) not found", id), @@ -173,6 +185,18 @@ impl GlobalState { Ok(nodes) } + pub fn get_nodes_mut(&mut self) -> Result, Error> { + let nodes = self.nodes.iter_mut() + .map(|(id, state)| (id, state)) + .collect::>(); + if nodes.is_empty() { + return Err(Error { + description: "Zero node registered".to_string(), + }) + } + Ok(nodes) + } + pub fn insert_stream(&mut self, name: String, state: StreamState) -> Result<(), Error> { if self.streams.contains_key(&name) { return Err(Error { @@ -183,7 +207,7 @@ impl GlobalState { Ok(()) } - pub fn remove_stream(&mut self, name: &String) -> Result<(), Error> { + pub fn delete_stream(&mut self, name: &String) -> Result<(), Error> { if self.streams.contains_key(name) == false { return Err(Error { description: format!("Stream with name({}) not found", name), @@ -284,9 +308,9 @@ impl OrphanState { pub(super) struct NodeState { proxy: pipewire::node::Node, - state: Rc>, - properties: Rc>>, - format: Rc>>, + state: GlobalObjectState, + properties: Option>, + format: Option, listeners: Rc>> } @@ -294,9 +318,9 @@ impl NodeState { pub fn new(proxy: pipewire::node::Node) -> Self { Self { proxy, - state: Rc::new(RefCell::new(GlobalObjectState::Pending)), - properties: Rc::new(RefCell::new(HashMap::new())), - format: Rc::new(RefCell::new(None)), + state: GlobalObjectState::Pending, + properties: None, + format: None, listeners: Rc::new(RefCell::new(Listeners::new())), } } @@ -306,43 +330,56 @@ impl NodeState { } pub fn state(&self) -> GlobalObjectState { - self.state.borrow().clone() + self.state.clone() } - fn set_state(&mut self) { - let properties = self.properties.borrow(); - let format = self.format.borrow(); - - let new_state = if properties.is_empty() == false && format.is_some() { - GlobalObjectState::Initialized + fn set_state(&mut self) { + if self.properties.is_some() && self.format.is_some() { + self.state = GlobalObjectState::Initialized } else { - GlobalObjectState::Pending + self.state = GlobalObjectState::Pending }; - - let mut state = self.state.borrow_mut(); - *state = new_state; } - pub fn properties(&self) -> HashMap { - self.properties.borrow().clone() + pub fn properties(&self) -> Option> { + self.properties.clone() } pub fn set_properties(&mut self, properties: HashMap) { - self.properties.borrow_mut().extend(properties); + if self.properties.is_none() { + self.properties = Some(HashMap::new()); + } + self.properties.as_mut().unwrap().extend(properties); self.set_state(); } pub fn format(&self) -> Option { - self.format.borrow().clone() + self.format.clone() } pub fn set_format(&mut self, format: AudioInfoRaw) { - *self.format.borrow_mut() = Some(format); + self.format = Some(format); self.set_state(); } - pub fn name(&self) -> String { - self.properties.borrow().get(*pipewire::keys::NODE_NAME).unwrap().clone() + pub fn name(&self) -> Result { + match self.properties.as_ref().unwrap().get(*pipewire::keys::NODE_NAME) { + Some(value) => Ok(value.clone()), + None => Err(Error { + description: "Node name not found in properties".to_string(), + }) + } + } + + pub fn direction(&self) -> Result { + let media_class = self.properties.as_ref().unwrap().get(*pipewire::keys::MEDIA_CLASS).unwrap().clone(); + match media_class.as_str() { + MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE => Ok(Direction::Input), + MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK => Ok(Direction::Output), + _ => Err(Error { + description: "Media class not an audio sink/source".to_string(), + }) + } } fn add_info_listener(&mut self, name: String, listener: F) @@ -728,12 +765,12 @@ impl Default for SettingsState { } impl SettingsState { - pub(super) fn listener(state: Rc>) -> impl Fn(&mut ListenerControlFlow, u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static + pub(super) fn listener(state: Arc>) -> impl Fn(&mut ListenerControlFlow, u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static { const EXPECTED_PROPERTY: u32 = 5; let property_count: Rc> = Rc::new(Cell::new(0)); move |control_flow, _, key, _, value| { - let settings = &mut state.borrow_mut().settings; + let settings = &mut state.lock().unwrap().settings; let key = key.unwrap(); let value = value.unwrap(); match key { @@ -790,12 +827,12 @@ impl Default for DefaultAudioNodesState { } impl DefaultAudioNodesState { - pub(super) fn listener(state: Rc>) -> impl Fn(&mut ListenerControlFlow, u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static + pub(super) fn listener(state: Arc>) -> impl Fn(&mut ListenerControlFlow, u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static { const EXPECTED_PROPERTY: u32 = 2; let property_count: Rc> = Rc::new(Cell::new(0)); move |control_flow, _, key, _, value| { - let default_audio_devices = &mut state.borrow_mut().default_audio_nodes; + let default_audio_devices = &mut state.lock().unwrap().default_audio_nodes; let key = key.unwrap(); if value.is_none() { return 0; diff --git a/pipewire-client/src/test_utils/api.rs b/pipewire-client/src/test_utils/api.rs index 5578257b8..440d81310 100644 --- a/pipewire-client/src/test_utils/api.rs +++ b/pipewire-client/src/test_utils/api.rs @@ -2,9 +2,9 @@ use crate::client::CoreApi; use crate::error::Error; use crate::messages::{MessageRequest, MessageResponse}; use crate::states::{MetadataState, NodeState, StreamState}; -use crate::utils::PipewireCoreSync; use std::any::TypeId; use std::collections::HashMap; +use crate::listeners::PipewireCoreSync; impl CoreApi { pub(crate) fn get_listeners(&self) -> Result>>, Error> { diff --git a/pipewire-client/src/test_utils/fixtures.rs b/pipewire-client/src/test_utils/fixtures.rs index 6f2483e31..53ad16d59 100644 --- a/pipewire-client/src/test_utils/fixtures.rs +++ b/pipewire-client/src/test_utils/fixtures.rs @@ -1,79 +1,398 @@ -use crate::test_utils::server::{server_with_default_configuration, set_socket_env_vars, Container}; -use crate::{Direction, NodeInfo, PipewireClient}; -use rstest::fixture; -use std::cell::RefCell; -use std::ops::Deref; -use std::rc::Rc; +use std::any::TypeId; +use std::collections::hash_map::Iter; +use std::collections::HashMap; +use crate::{NodeInfo, PipewireClient}; +use pipewire_common::utils::Direction; +use pipewire_test_utils::server::{server_with_default_configuration, server_without_node, server_without_session_manager, Server}; +use rstest::{fixture, Context}; +use std::fmt::{Display, Formatter}; +use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, LazyLock, Mutex, OnceLock}; +use std::{mem, thread}; +use std::ptr::drop_in_place; +use std::time::Duration; +use ctor::{ctor, dtor}; +use libc::{atexit, signal, SIGINT, SIGSEGV, SIGTERM}; +use tokio::runtime::Runtime; +use uuid::Uuid; +use pipewire_common::error::Error; +use pipewire_test_utils::environment::{SHARED_SERVER, TEST_ENVIRONMENT}; +use crate::states::StreamState; + +pub struct NodeInfoFixture { + client: Arc, + node: OnceLock, + direction: Direction +} + +impl NodeInfoFixture { + pub(self) fn new(client: Arc, direction: Direction) -> Self { + Self { + client, + node: OnceLock::new(), + direction, + } + } + + pub fn client(&self) -> Arc { + self.client.clone() + } +} + +impl Deref for NodeInfoFixture { + type Target = NodeInfo; + + fn deref(&self) -> &Self::Target { + let node = self.node.get_or_init(|| { + let node_name = Uuid::new_v4().to_string(); + self.client.node() + .create( + node_name.clone(), + node_name.clone(), + node_name.clone(), + self.direction.clone(), + 2 + ).unwrap(); + let node = self.client.node().get(node_name, self.direction.clone()).unwrap(); + node + }); + node + } +} + +impl Drop for NodeInfoFixture { + fn drop(&mut self) { + self.client.node() + .delete(self.node.get().unwrap().id) + .unwrap() + } +} + +pub struct StreamFixture { + client: Arc, + node: NodeInfoFixture, + stream: OnceLock, + direction: Direction +} + +impl StreamFixture { + pub(self) fn new(client: Arc, node: NodeInfoFixture) -> Self { + let direction = node.direction.clone(); + Self { + client: client.clone(), + node, + stream: OnceLock::new(), + direction, + } + } + + pub fn client(&self) -> Arc { + self.client.clone() + } + + pub fn name(&self) -> String { + self.deref().clone() + } + + pub fn listeners(&self) -> HashMap> { + let listeners = self.client.core().get_listeners().unwrap(); + let stream_listeners = listeners.get(&TypeId::of::()).unwrap(); + stream_listeners.clone() + } + + pub fn connect(&self) -> Result<(), Error> { + let stream = self.deref().clone(); + self.client.stream().connect(stream) + } + + pub fn disconnect(&self) -> Result<(), Error> { + let stream = self.deref().clone(); + self.client.stream().disconnect(stream) + } + + pub fn delete(&self) -> Result<(), Error> { + let stream = self.deref().clone(); + self.client.stream().delete(stream) + } +} + +impl Display for StreamFixture { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.deref().clone()) + } +} + +impl Deref for StreamFixture { + type Target = String; + + fn deref(&self) -> &Self::Target { + let stream = self.stream.get_or_init(|| { + self.client.stream() + .create( + self.node.id, + self.direction.clone(), + self.node.format.clone().into(), + move |control_flow, _| { + assert!(true); + control_flow.release(); + } + ).unwrap() + }); + stream + } +} + +impl Drop for StreamFixture { + fn drop(&mut self) { + let stream = self.stream.get().unwrap().clone(); + let result = self.client.stream().delete(stream.clone()); + match result { + Ok(_) => {} + Err(value) => { + let error_message = format!( + "Stream with name({}) not found", + self.stream.get().unwrap().clone() + ); + if error_message != value.description { + panic!("{}", error_message); + } + // If error is raised, we can assume this stream had been deleted. + // Certainly due to delete tests, we cannot be sure at this point but let just + // show a warning for now. + eprintln!( + "Failed to delete stream: {}. Stream delete occurred during test method ?", + self.stream.get().unwrap() + ); + } + } + } +} + +pub struct ConnectedStreamFixture { + client: Arc, + stream: StreamFixture, +} + +impl ConnectedStreamFixture { + pub(self) fn new(client: Arc, stream: StreamFixture) -> Self { + stream.connect().unwrap(); + Self { + client, + stream, + } + } + + pub fn disconnect(&self) -> Result<(), Error> { + let stream = self.stream.deref().clone(); + self.client.stream().disconnect(stream) + } +} + +impl Display for ConnectedStreamFixture { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.stream.fmt(f) + } +} + +impl Deref for ConnectedStreamFixture { + type Target = StreamFixture; + + fn deref(&self) -> &Self::Target { + &self.stream + } +} + +impl Drop for ConnectedStreamFixture { + fn drop(&mut self) { + let stream = self.stream.deref().clone(); + let result = self.client.stream().disconnect(stream.clone()); + match result { + Ok(_) => {} + Err(value) => { + let error_message = format!( + "Stream {} is not connected", + stream.clone() + ); + if error_message != value.description { + panic!("{}", error_message); + } + // If error is raised, we can assume this stream had been disconnected. + // Certainly due to disconnect tests, we cannot be sure at this point but let just + // show a warning for now. + eprintln!( + "Failed to disconnect stream: {}. Stream disconnect occurred during test method ?", + stream.clone() + ); + } + } + } +} pub struct PipewireTestClient { - server: Rc>, - client: PipewireClient, + name: String, + server: Arc, + client: Arc, } impl PipewireTestClient { - pub(self) fn new(server: Rc>, client: PipewireClient) -> Self { + pub(self) fn new( + name: String, + server: Arc, + client: PipewireClient + ) -> Self { + let client = Arc::new(client); + println!("Create {} client: {}", name.clone(), Arc::strong_count(&client)); Self { + name, server, - client, + client: client.clone(), } } - - pub fn input_nodes(&self) -> Vec { - self.node().enumerate(Direction::Input).unwrap() + + pub(self) fn reference_count(&self) -> usize { + Arc::strong_count(&self.client) } - - pub fn output_nodes(&self) -> Vec { - self.node().enumerate(Direction::Output).unwrap() + + pub(self) fn create_input_node(&self) -> NodeInfoFixture { + NodeInfoFixture::new(self.client.clone(), Direction::Input) } - - pub fn default_input_node(&self) -> NodeInfo { - self.input_nodes().iter() - .filter(|node| node.is_default) - .last() - .cloned() - .unwrap() + + pub(self) fn create_output_node(&self) -> NodeInfoFixture { + NodeInfoFixture::new(self.client.clone(), Direction::Output) } - pub fn default_output_node(&self) -> NodeInfo { - self.output_nodes().iter() - .filter(|node| node.is_default) - .last() - .cloned() - .unwrap() + pub(self) unsafe fn cleanup(&mut self) { + let pointer = std::ptr::addr_of_mut!(self.client); + let reference_count = Arc::strong_count(&self.client); + for _ in 0..reference_count { + drop_in_place(pointer); + } + } +} + +impl Clone for PipewireTestClient { + fn clone(&self) -> Self { + let client = Self { + name: self.name.clone(), + server: self.server.clone(), + client: self.client.clone(), + }; + println!("Clone {} client: {}", self.name.clone(), self.reference_count()); + client } } impl Deref for PipewireTestClient { - type Target = PipewireClient; + type Target = Arc; fn deref(&self) -> &Self::Target { &self.client } } +impl Drop for PipewireTestClient { + fn drop(&mut self) { + println!("Drop {} client: {}", self.name.clone(), self.reference_count() - 1); + } +} + +#[ctor] +static SHARED_CLIENT: Arc> = { + unsafe { libc::printf("Initialize shared client\n\0".as_ptr() as *const i8); }; + let server = SHARED_SERVER.clone(); + let client = PipewireTestClient::new( + "shared".to_string(), + server, + PipewireClient::new( + Arc::new(Runtime::new().unwrap()), + TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), + ).unwrap(), + ); + Arc::new(Mutex::new(client)) +}; + +#[dtor] +unsafe fn cleanup_shared_client() { + libc::printf("Cleaning shared client\n\0".as_ptr() as *const i8); + SHARED_CLIENT.lock().unwrap().cleanup(); +} + #[fixture] -pub fn client(server_with_default_configuration: Container) -> PipewireTestClient { - set_socket_env_vars(&server_with_default_configuration); - PipewireTestClient::new( - Rc::new(RefCell::new(server_with_default_configuration)), - PipewireClient::new().unwrap() - ) +pub fn isolated_client() -> PipewireTestClient { + let server = SHARED_SERVER.clone(); + let client = PipewireTestClient::new( + "isolated".to_string(), + server, + PipewireClient::new( + Arc::new(Runtime::new().unwrap()), + TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), + ).unwrap(), + ); + client } #[fixture] -pub fn client2(server_with_default_configuration: Container) -> (PipewireTestClient, PipewireTestClient) { - set_socket_env_vars(&server_with_default_configuration); - let server = Rc::new(RefCell::new(server_with_default_configuration)); - let client_1 = PipewireClient::new().unwrap(); - let client_2 = PipewireClient::new().unwrap(); +pub fn shared_client() -> PipewireTestClient { + // Its seems that shared client, for some reason, raise + // timeout error during init phase and create node object phase. + // Give a bit of space between tests seem to mitigate that issue. + thread::sleep(Duration::from_millis(10)); + let client = SHARED_CLIENT.lock().unwrap().clone(); + client +} + +#[fixture] +pub fn client2(server_with_default_configuration: Arc) -> (PipewireTestClient, PipewireTestClient) { + let server = server_with_default_configuration.clone(); + let runtime = Arc::new(Runtime::new().unwrap()); + let client_1 = PipewireClient::new( + runtime.clone(), + TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), + ).unwrap(); + let client_2 = PipewireClient::new( + runtime.clone(), + TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), + ).unwrap(); ( PipewireTestClient::new( + "isolated_client_1".to_string(), server.clone(), - client_1 + client_1, ), PipewireTestClient::new( + "isolated_client_2".to_string(), server.clone(), - client_2 + client_2, ) ) +} + +#[fixture] +pub fn input_node(shared_client: PipewireTestClient) -> NodeInfoFixture { + shared_client.create_input_node() +} + +#[fixture] +pub fn output_node(shared_client: PipewireTestClient) -> NodeInfoFixture { + shared_client.create_output_node() +} + +#[fixture] +pub fn input_stream(input_node: NodeInfoFixture) -> StreamFixture { + StreamFixture::new(input_node.client.clone(), input_node) +} + +#[fixture] +pub fn output_stream(output_node: NodeInfoFixture) -> StreamFixture { + StreamFixture::new(output_node.client.clone(), output_node) +} + +#[fixture] +pub fn input_connected_stream(input_stream: StreamFixture) -> ConnectedStreamFixture { + ConnectedStreamFixture::new(input_stream.client.clone(), input_stream) +} + +#[fixture] +pub fn output_connected_stream(output_stream: StreamFixture) -> ConnectedStreamFixture { + ConnectedStreamFixture::new(output_stream.client.clone(), output_stream) } \ No newline at end of file diff --git a/pipewire-client/src/test_utils/mod.rs b/pipewire-client/src/test_utils/mod.rs index 9a13c9d1a..147b5c7da 100644 --- a/pipewire-client/src/test_utils/mod.rs +++ b/pipewire-client/src/test_utils/mod.rs @@ -1,3 +1,2 @@ pub mod fixtures; -pub mod server; mod api; \ No newline at end of file diff --git a/pipewire-client/src/test_utils/server.rs b/pipewire-client/src/test_utils/server.rs deleted file mode 100644 index d01be7d01..000000000 --- a/pipewire-client/src/test_utils/server.rs +++ /dev/null @@ -1,288 +0,0 @@ -use crate::constants::{PIPEWIRE_CORE_ENVIRONMENT_KEY, PIPEWIRE_REMOTE_ENVIRONMENT_KEY, PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY}; -use docker_api::models::ImageBuildChunk; -use docker_api::opts::ImageBuildOpts; -use docker_api::Docker; -use futures::StreamExt; -use pipewire::spa::utils::dict::ParsableValue; -use rstest::fixture; -use std::path::{Path, PathBuf}; -use testcontainers::core::{CmdWaitFor, ExecCommand, ExecResult, Mount}; -use testcontainers::runners::AsyncRunner; -use testcontainers::{ContainerAsync, GenericImage, ImageExt}; -use tokio::io::AsyncReadExt; -use uuid::Uuid; - -pub struct Container { - name: String, - tag: String, - container_file_path: PathBuf, - container: Option>, - socket_id: Uuid, - pipewire_pid: Option, - wireplumber_pid: Option, - pulse_pid: Option, -} - -impl Container { - pub fn new( - name: String, - container_file_path: PathBuf, - ) -> Self { - Self { - name, - tag: "latest".to_string(), - container_file_path, - container: None, - socket_id: Uuid::new_v4(), - pipewire_pid: None, - wireplumber_pid: None, - pulse_pid: None, - } - } - - fn socket_location(&self) -> PathBuf { - Path::new("/run/pipewire-sockets").join(self.socket_id.to_string()).to_path_buf() - } - - fn socket_name(&self) -> String { - format!("{}", self.socket_id) - } - - fn build(&self) { - const DOCKER_HOST_ENVIRONMENT_KEY: &str = "DOCKER_HOST"; - const CONTAINER_HOST_ENVIRONMENT_KEY: &str = "CONTAINER_HOST"; - let docker_host = std::env::var(DOCKER_HOST_ENVIRONMENT_KEY); - let container_host = std::env::var(CONTAINER_HOST_ENVIRONMENT_KEY); - let uri = match (docker_host, container_host) { - (Ok(value), Ok(_)) => value, - (Ok(value), Err(_)) => value, - (Err(_), Ok(value)) => { - // TestContainer does not recognize CONTAINER_HOST. - // Instead, with set DOCKET_HOST env var with the same value - std::env::set_var(DOCKER_HOST_ENVIRONMENT_KEY, value.clone()); - value - }, - (Err(_), Err(_)) => panic!( - "${} or ${} should be set.", - DOCKER_HOST_ENVIRONMENT_KEY, CONTAINER_HOST_ENVIRONMENT_KEY - ), - }; - let api = Docker::new(uri).unwrap(); - let images = api.images(); - let build_image_options= ImageBuildOpts::builder(self.container_file_path.parent().unwrap().to_str().unwrap()) - .tag(format!("{}:{}", self.name, self.tag)) - .dockerfile(self.container_file_path.file_name().unwrap().to_str().unwrap()) - .build(); - let mut stream = images.build(&build_image_options); - let runtime = tokio::runtime::Runtime::new().unwrap(); - while let Some(build_result) = runtime.block_on(stream.next()) { - match build_result { - Ok(output) => { - let output = match output { - ImageBuildChunk::Update { stream } => stream, - ImageBuildChunk::Error { error, error_detail } => { - panic!("Error {}: {}", error, error_detail.message); - } - ImageBuildChunk::Digest { aux } => aux.id, - ImageBuildChunk::PullStatus { .. } => { - return - } - }; - print!("{}", output); - }, - Err(e) => panic!("Error: {e}"), - } - } - } - - fn run(&mut self) { - let socket_location = self.socket_location(); - let socket_name = self.socket_name(); - let container = GenericImage::new(self.name.clone(), self.tag.clone()) - .with_env_var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, socket_location.to_str().unwrap()) - .with_env_var(PIPEWIRE_CORE_ENVIRONMENT_KEY, socket_name.clone()) - .with_env_var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, socket_name.clone()) - .with_env_var("PULSE_RUNTIME_PATH", socket_location.join("pulse").to_str().unwrap()) - .with_mount(Mount::volume_mount( - "pipewire-sockets", - socket_location.parent().unwrap().to_str().unwrap(), - )); - let runtime = tokio::runtime::Runtime::new().unwrap(); - let container = runtime.block_on(container.start()).unwrap(); - self.container = Some(container); - self.exec(vec![ - "mkdir", - "--parent", - socket_location.to_str().unwrap(), - ]); - } - - fn exec(&self, command: Vec<&str>) -> ExecResult { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime - .block_on(self.container.as_ref().unwrap().exec( - ExecCommand::new(command).with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )) - .unwrap() - } - - fn run_process(&mut self, process_name: &str) -> u32 { - self.exec(vec![process_name]); - let mut result = self.exec(vec![ - "pidof", - process_name, - ]); - let mut pid = String::new(); - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(result.stdout().read_to_string(&mut pid)).unwrap(); - pid = pid.trim_end().to_string(); - u32::parse_value(pid.as_str()).unwrap() - } - - fn kill_process(&mut self, process_id: u32) { - self.exec(vec![ - "kill", - "-s", "SIGKILL", - format!("{}", process_id).as_str() - ]); - } - - fn start_pipewire(&mut self) { - let pid = self.run_process("pipewire"); - self.pipewire_pid = Some(pid); - } - - fn stop_pipewire(&mut self) { - self.kill_process(self.pipewire_pid.unwrap()) - } - - fn start_wireplumber(&mut self) { - let pid = self.run_process("wireplumber"); - self.wireplumber_pid = Some(pid); - } - - fn stop_wireplumber(&mut self) { - if self.wireplumber_pid.is_none() { - return; - } - self.kill_process(self.wireplumber_pid.unwrap()); - } - - fn start_pulse(&mut self) { - let pid = self.run_process("pipewire-pulse"); - self.pulse_pid = Some(pid); - } - - fn stop_pulse(&mut self) { - if self.pulse_pid.is_none() { - return; - } - self.kill_process(self.pulse_pid.unwrap()); - } - - fn load_null_sink_module(&self) { - self.exec(vec![ - "pactl", - "load-module", - "module-null-sink" - ]); - } - - fn set_virtual_nodes_configuration(&self) { - self.exec(vec![ - "mkdir", - "--parent", - "/etc/pipewire/pipewire.conf.d/", - ]); - self.exec(vec![ - "cp", - "/root/pipewire.nodes.conf", - "/etc/pipewire/pipewire.conf.d/pipewire.nodes.conf", - ]); - } - - fn set_default_nodes(&self) { - self.exec(vec![ - "pactl", - "set-default-sink", - "test-sink", - ]); - self.exec(vec![ - "pactl", - "set-default-source", - "test-source", - ]); - } -} - -impl Drop for Container { - fn drop(&mut self) { - if self.container.is_none() { - return; - } - self.stop_pulse(); - self.stop_wireplumber(); - self.stop_pipewire(); - let socket_location = self.socket_location(); - let container = self.container.take().unwrap(); - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(container.exec( - ExecCommand::new(vec![ - "rm", - "--force", - "--recursive", - socket_location.join("*").to_str().unwrap(), - ]) - .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), - )).unwrap(); - runtime.block_on(container.stop()).unwrap(); - runtime.block_on(container.rm()).unwrap(); - } -} - -#[fixture] -pub fn server_with_default_configuration() -> Container { - let mut container = Container::new( - "pipewire-default".to_string(), - PathBuf::from(".containers/pipewire.test.container"), - ); - container.build(); - container.run(); - container.set_virtual_nodes_configuration(); - container.start_pipewire(); - container.start_wireplumber(); - container.start_pulse(); - container.set_default_nodes(); - //container.load_null_sink_module(); - container -} - -#[fixture] -pub fn server_without_session_manager() -> Container { - let mut container = Container::new( - "pipewire-without-session-manager".to_string(), - PathBuf::from(".containers/pipewire.test.container"), - ); - container.build(); - container.run(); - container.start_pipewire(); - container -} - -#[fixture] -pub fn server_without_node() -> Container { - let mut container = Container::new( - "pipewire-without-node".to_string(), - PathBuf::from(".containers/pipewire.test.container"), - ); - container.build(); - container.run(); - container.start_pipewire(); - container.start_wireplumber(); - container -} - -pub fn set_socket_env_vars(server: &Container) { - std::env::set_var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, server.socket_location()); - std::env::set_var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, server.socket_name()); -} \ No newline at end of file diff --git a/pipewire-client/src/utils.rs b/pipewire-client/src/utils.rs deleted file mode 100644 index 5a4b3381c..000000000 --- a/pipewire-client/src/utils.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::listeners::{Listener, ListenerControlFlow, Listeners}; -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; - -#[derive(Debug, Clone, PartialEq)] -pub enum Direction { - Input, - Output, -} - -impl From for pipewire::spa::utils::Direction { - fn from(value: Direction) -> Self { - match value { - Direction::Input => pipewire::spa::utils::Direction::Input, - Direction::Output => pipewire::spa::utils::Direction::Output, - } - } -} - -pub(super) fn dict_ref_to_hashmap(dict: &pipewire::spa::utils::dict::DictRef) -> HashMap { - dict - .iter() - .map(move |(k, v)| { - let k = String::from(k).clone(); - let v = String::from(v).clone(); - (k, v) - }) - .collect::>() -} - -pub(super) fn debug_dict_ref(dict: &pipewire::spa::utils::dict::DictRef) { - for (key, value) in dict.iter() { - println!("{} => {}", key ,value); - } - println!("\n"); -} - -pub(super) struct PipewireCoreSync { - core: Rc>, - listeners: Rc>>, -} - -impl PipewireCoreSync { - pub fn new(core: Rc>) -> Self { - Self { - core, - listeners: Rc::new(RefCell::new(Listeners::new())), - } - } - - pub(super) fn get_listener_names(&self) -> Vec { - self.listeners.borrow().get_names() - } - - pub fn register(&self, seq: u32, callback: F) - where - F: Fn(&mut ListenerControlFlow) + 'static, - { - let sync_id = self.core.borrow_mut().sync(seq as i32).unwrap(); - let name = format!("sync-{}", sync_id.raw()); - let listeners = self.listeners.clone(); - let listener_name = name.clone(); - let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); - let listener_control_flow = control_flow.clone(); - let listener = self - .core - .borrow_mut() - .add_listener_local() - .done(move |_, seq| { - if seq != sync_id { - return; - } - if listener_control_flow.borrow().is_released() { - return; - } - callback(&mut listener_control_flow.borrow_mut()); - listeners.borrow_mut().triggered(&listener_name); - }) - .register(); - self.listeners - .borrow_mut() - .add(name, Listener::new(listener, control_flow)); - } -} - -impl Clone for PipewireCoreSync { - fn clone(&self) -> Self { - Self { - core: self.core.clone(), - listeners: self.listeners.clone(), - } - } -} - -pub(super) struct Backoff { - attempts: u32, - maximum_attempts: u32, - wait_duration: std::time::Duration, - initial_wait_duration: std::time::Duration, - maximum_wait_duration: std::time::Duration, -} - -impl Backoff { - pub fn new( - maximum_attempts: u32, - initial_wait_duration: std::time::Duration, - maximum_wait_duration: std::time::Duration - ) -> Self { - Self { - attempts: 0, - maximum_attempts, - wait_duration: initial_wait_duration, - initial_wait_duration, - maximum_wait_duration, - } - } - - pub fn reset(&mut self) { - self.attempts = 0; - self.wait_duration = self.initial_wait_duration; - } - - pub fn retry(&mut self, mut operation: F) -> Result - where - F: FnMut() -> Result, - E: std::error::Error - { - self.reset(); - loop { - let error = match operation() { - Ok(value) => return Ok(value), - Err(value) => value - }; - std::thread::sleep(self.wait_duration); - self.wait_duration = self.maximum_wait_duration.min(self.wait_duration * 2); - self.attempts += 1; - if self.attempts < self.maximum_attempts { - continue; - } - return Err(error) - } - } -} \ No newline at end of file diff --git a/pipewire-common/Cargo.toml b/pipewire-common/Cargo.toml new file mode 100644 index 000000000..3a020f601 --- /dev/null +++ b/pipewire-common/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pipewire-common" +version = "0.1.0" +edition = "2021" +authors = ["Alexis Bekhdadi "] +description = "PipeWire Common" +repository = "https://github.com/RustAudio/cpal/" +documentation = "" +license = "Apache-2.0" +keywords = ["pipewire", "common"] + +[dependencies] +pipewire = "0.8" \ No newline at end of file diff --git a/pipewire-common/src/constants.rs b/pipewire-common/src/constants.rs new file mode 100644 index 000000000..256358aaa --- /dev/null +++ b/pipewire-common/src/constants.rs @@ -0,0 +1,34 @@ +pub const PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY: &str = "PIPEWIRE_RUNTIME_DIR"; +pub const PIPEWIRE_CORE_ENVIRONMENT_KEY: &str = "PIPEWIRE_CORE"; +pub const PIPEWIRE_REMOTE_ENVIRONMENT_KEY: &str = "PIPEWIRE_REMOTE"; +pub const XDG_RUNTIME_DIR_ENVIRONMENT_KEY: &str = "XDG_RUNTIME_DIR"; +pub const PULSE_RUNTIME_PATH_ENVIRONMENT_KEY: &str = "PULSE_RUNTIME_PATH"; +pub const PIPEWIRE_REMOTE_ENVIRONMENT_DEFAULT: &str = "pipewire-0"; + +pub const PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ :u32 = 0; +pub const PIPEWIRE_CORE_SYNC_CREATE_DEVICE_SEQ :u32 = 1; + +pub const MEDIA_TYPE_PROPERTY_VALUE_AUDIO: &str = "Audio"; +pub const MEDIA_CLASS_PROPERTY_KEY: &str = "media.class"; +pub const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE: &str = "Audio/Source"; +pub const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK: &str = "Audio/Sink"; +pub const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_DUPLEX: &str = "Audio/Duplex"; +pub const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_DEVICE: &str = "Audio/Device"; +pub const MEDIA_CLASS_PROPERTY_VALUE_STREAM_OUTPUT_AUDIO: &str = "Stream/Output/Audio"; +pub const MEDIA_CLASS_PROPERTY_VALUE_STREAM_INPUT_AUDIO: &str = "Stream/Input/Audio"; +pub const METADATA_NAME_PROPERTY_KEY: &str = "metadata.name"; +pub const METADATA_NAME_PROPERTY_VALUE_SETTINGS: &str = "settings"; +pub const METADATA_NAME_PROPERTY_VALUE_DEFAULT: &str = "default"; +pub const CLOCK_RATE_PROPERTY_KEY: &str = "clock.rate"; +pub const CLOCK_QUANTUM_PROPERTY_KEY: &str = "clock.quantum"; +pub const CLOCK_QUANTUM_MIN_PROPERTY_KEY: &str = "clock.min-quantum"; +pub const CLOCK_QUANTUM_MAX_PROPERTY_KEY: &str = "clock.max-quantum"; +pub const CLOCK_ALLOWED_RATES_PROPERTY_KEY: &str = "clock.allowed-rates"; +pub const MONITOR_CHANNEL_VOLUMES_PROPERTY_KEY: &str = "monitor.channel-volumes"; +pub const MONITOR_PASSTHROUGH_PROPERTY_KEY: &str = "monitor.passthrough"; +pub const DEFAULT_AUDIO_SINK_PROPERTY_KEY: &str = "default.audio.sink"; +pub const DEFAULT_AUDIO_SOURCE_PROPERTY_KEY: &str = "default.audio.source"; +pub const AUDIO_POSITION_PROPERTY_KEY: &str = "audio.position"; +pub const APPLICATION_NAME_PROPERTY_KEY: &str = "application.name"; +pub const APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER: &str = "WirePlumber"; +pub const APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION: &str = "pipewire-media-session"; \ No newline at end of file diff --git a/pipewire-client/src/error.rs b/pipewire-common/src/error.rs similarity index 100% rename from pipewire-client/src/error.rs rename to pipewire-common/src/error.rs diff --git a/pipewire-common/src/lib.rs b/pipewire-common/src/lib.rs new file mode 100644 index 000000000..fc3f64bfa --- /dev/null +++ b/pipewire-common/src/lib.rs @@ -0,0 +1,4 @@ +pub mod constants; +pub mod error; +pub mod macros; +pub mod utils; diff --git a/pipewire-common/src/macros.rs b/pipewire-common/src/macros.rs new file mode 100644 index 000000000..131375383 --- /dev/null +++ b/pipewire-common/src/macros.rs @@ -0,0 +1,85 @@ +#[macro_export] +macro_rules! impl_callback { + ( + $t:tt => $r:ty, + $name:ident, + $( $k:ident : $v:ty ),* + ) => { + pub(super) struct $name { + callback: Arc $r + Sync + Send + 'static>>> + } + + impl From for $name + where + F: $t($($v),*) -> $r + Sync + Send + 'static + { + fn from(value: F) -> Self { + Self { callback: Arc::new(Mutex::new(Box::new(value))) } + } + } + + impl $name + { + pub fn call(&self, $($k: $v),*) -> $r + { + let callback = self.callback.lock().unwrap(); + callback($($k),*) + } + } + + impl Debug for $name { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("$name").finish() + } + } + + impl Clone for $name { + fn clone(&self) -> Self { + Self { callback: self.callback.clone() } + } + } + } +} + +#[macro_export] +macro_rules! impl_callback_generic { + ( + $t:tt => $r:ty, + $name:ident<$( $p:ident $(: $clt:lifetime)? ),*>, + $($k:ident : $v:ty),* + ) => { + pub(super) struct $name<$($p $(: $clt )? ),*> { + callback: Arc $r + Sync + Send + 'static>>> + } + + impl <$($p $(: $clt )? ),*, F> From for $name<$($p),*> + where + F: $t($($v),*) -> $r + Sync + Send + 'static + { + fn from(value: F) -> Self { + Self { callback: Arc::new(Mutex::new(Box::new(value))) } + } + } + + impl <$($p $(: $clt )? ),*> $name<$($p),*> + { + pub fn call(&self, $($k: $v),*) -> $r + { + let callback = self.callback.lock().unwrap(); + callback($($k),*) + } + } + + impl <$($p $(: $clt )? ),*> Debug for $name<$($p),*> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("$name").finish() + } + } + + impl <$($p $(: $clt )? ),*> Clone for $name<$($p),*> { + fn clone(&self) -> Self { + Self { callback: self.callback.clone() } + } + } + } +} \ No newline at end of file diff --git a/pipewire-common/src/utils.rs b/pipewire-common/src/utils.rs new file mode 100644 index 000000000..e860ffdfa --- /dev/null +++ b/pipewire-common/src/utils.rs @@ -0,0 +1,110 @@ +use crate::error::Error; +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq)] +pub enum Direction { + Input, + Output, +} + +impl From for pipewire::spa::utils::Direction { + fn from(value: Direction) -> Self { + match value { + Direction::Input => pipewire::spa::utils::Direction::Input, + Direction::Output => pipewire::spa::utils::Direction::Output, + } + } +} + +pub fn dict_ref_to_hashmap(dict: &pipewire::spa::utils::dict::DictRef) -> HashMap { + dict + .iter() + .map(move |(k, v)| { + let k = String::from(k).clone(); + let v = String::from(v).clone(); + (k, v) + }) + .collect::>() +} + +pub fn debug_dict_ref(dict: &pipewire::spa::utils::dict::DictRef) { + for (key, value) in dict.iter() { + println!("{} => {}", key ,value); + } + println!("\n"); +} + + + +pub struct Backoff { + attempts: u32, + maximum_attempts: u32, + wait_duration: std::time::Duration, + initial_wait_duration: std::time::Duration, + maximum_wait_duration: std::time::Duration, +} + +impl Default for Backoff { + fn default() -> Self { + Self::new( + 300, // 300 attempts * 100ms = 30s + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100) + ) + } +} + +impl Backoff { + pub fn constant(milliseconds: u128) -> Self { + let attempts = milliseconds / 100; + Self::new( + attempts as u32, + std::time::Duration::from_millis(100), + std::time::Duration::from_millis(100) + ) + } +} + +impl Backoff { + pub fn new( + maximum_attempts: u32, + initial_wait_duration: std::time::Duration, + maximum_wait_duration: std::time::Duration + ) -> Self { + Self { + attempts: 0, + maximum_attempts, + wait_duration: initial_wait_duration, + initial_wait_duration, + maximum_wait_duration, + } + } + + pub fn reset(&mut self) { + self.attempts = 0; + self.wait_duration = self.initial_wait_duration; + } + + pub fn retry(&mut self, mut operation: F) -> Result + where + F: FnMut() -> Result, + E: std::error::Error + { + self.reset(); + loop { + let error = match operation() { + Ok(value) => return Ok(value), + Err(value) => value + }; + std::thread::sleep(self.wait_duration); + self.wait_duration = self.maximum_wait_duration.min(self.wait_duration * 2); + self.attempts += 1; + if self.attempts < self.maximum_attempts { + continue; + } + return Err(Error { + description: format!("Backoff timeout: {}", error.to_string()), + }) + } + } +} \ No newline at end of file diff --git a/pipewire-test-utils/.containers/.digests b/pipewire-test-utils/.containers/.digests new file mode 100644 index 000000000..e13625db0 --- /dev/null +++ b/pipewire-test-utils/.containers/.digests @@ -0,0 +1,3 @@ +pipewire-default=sha256:61a3bcf12683ea189b3cfdaa9eb0439f3a24b22e13a5336d0ec26121fc2d068e +pipewire-without-node=sha256:04d6951ebb1625424b7c2f5bc012ce7d9cbb3d47c2f4a66da97f8f2f35584916 +pipewire-without-session-manager=sha256:ccaf8f57bbc06494feed36bcd1addf6af36592c0f902e4d81cfd4fc4122e2785 diff --git a/pipewire-test-utils/.containers/.tmp/entrypoint.bash b/pipewire-test-utils/.containers/.tmp/entrypoint.bash new file mode 100644 index 000000000..2fa77d53d --- /dev/null +++ b/pipewire-test-utils/.containers/.tmp/entrypoint.bash @@ -0,0 +1,6 @@ +#!/bin/bash +mkdir --parents ${PIPEWIRE_RUNTIME_DIR} +mkdir --parents /etc/pipewire/pipewire.conf.d/ +cp /root/virtual.nodes.conf /etc/pipewire/pipewire.conf.d/virtual.nodes.conf +supervisord -c /root/supervisor.conf +rm --force --recursive ${PIPEWIRE_RUNTIME_DIR} \ No newline at end of file diff --git a/pipewire-test-utils/.containers/.tmp/healthcheck.bash b/pipewire-test-utils/.containers/.tmp/healthcheck.bash new file mode 100644 index 000000000..5cbb42d2b --- /dev/null +++ b/pipewire-test-utils/.containers/.tmp/healthcheck.bash @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +(echo 'wait for pipewire') || exit 1 +(pw-cli ls 0 | grep --quiet 'id 0, type PipeWire:Interface:Core/4') || exit 1 +(echo 'wait for wireplumbler') || exit 1 +(wpctl info | grep --quiet 'WirePlumber') || exit 1 +(echo 'wait for PipeWire Pulse') || exit 1 +(pactl info | grep --quiet "$PULSE_RUNTIME_PATH/native") || exit 1 +(echo 'wait for test-sink') || exit 1 +(pactl set-default-sink 'test-sink') || exit 1 +(wpctl status | grep --quiet 'test-sink') || exit 1 +(echo 'wait for test-source') || exit 1 +(pactl set-default-source 'test-source') || exit 1 +(wpctl status | grep --quiet 'test-source') || exit 1 \ No newline at end of file diff --git a/pipewire-test-utils/.containers/.tmp/supervisor.conf b/pipewire-test-utils/.containers/.tmp/supervisor.conf new file mode 100644 index 000000000..bf4250d13 --- /dev/null +++ b/pipewire-test-utils/.containers/.tmp/supervisor.conf @@ -0,0 +1,21 @@ +[supervisord] +nodaemon=true +logfile=/var/log/supervisor/supervisord.log +[program:pipewire] +command=pipewire +stdout_logfile=/tmp/pipewire.out.log +stderr_logfile=/tmp/pipewire.err.log +autostart=true +autorestart=true +[program:wireplumber] +command=wireplumber +stdout_logfile=/tmp/wireplumber.out.log +stderr_logfile=/tmp/wireplumber.err.log +autostart=true +autorestart=true +[program:pipewire-pulse] +command=pipewire-pulse +stdout_logfile=/tmp/pipewire-pulse.out.log +stderr_logfile=/tmp/pipewire-pulse.err.log +autostart=true +autorestart=true \ No newline at end of file diff --git a/pipewire-test-utils/.containers/pipewire.test.container b/pipewire-test-utils/.containers/pipewire.test.container new file mode 100644 index 000000000..ae939246c --- /dev/null +++ b/pipewire-test-utils/.containers/pipewire.test.container @@ -0,0 +1,48 @@ +from fedora:41 as base +run dnf update --assumeyes +run dnf install --assumeyes \ + supervisor \ + socat \ + procps-ng \ + htop \ + pipewire \ + pipewire-utils \ + pipewire-alsa \ + pipewire-pulse \ + pipewire-devel \ + wireplumber \ + pulseaudio-utils + +copy virtual.nodes.conf /root/virtual.nodes.conf + +run mkdir --parents /etc/wireplumber/wireplumber.conf.d +run touch /etc/wireplumber/wireplumber.conf.d/80-disable-dbus.conf +run tee /etc/wireplumber/wireplumber.conf.d/80-disable-dbus.conf <"] +description = "PipeWire Test Utils" +repository = "https://github.com/RustAudio/cpal/" +documentation = "" +license = "Apache-2.0" +keywords = ["pipewire", "test", "utils"] + +[dependencies] +pipewire-common = { version = "0.1", path = "../pipewire-common" } +bollard = { version = "0.18", features = ["buildkit"] } +futures = "0.3" +tar = "0.4" +flate2 = "1.0" +bytes = "1.9" +sha2 = "0.10" +uuid = { version = "1.12", features = ["v4"] } +tokio = { version = "1", features = ["full"] } +libc = "0.2" +rstest = "0.24" +ctor = "0.2" +http = "1.2" +serde = "1.0" +serde_json = "1.0" +serde_urlencoded = "0.7" +url = "2.5" +hex = "0.4" +ureq = "3.0" diff --git a/pipewire-test-utils/src/containers/container.rs b/pipewire-test-utils/src/containers/container.rs new file mode 100644 index 000000000..8d13f7618 --- /dev/null +++ b/pipewire-test-utils/src/containers/container.rs @@ -0,0 +1,591 @@ +use pipewire_common::utils::Backoff; +use bollard::container::{ListContainersOptions, LogOutput, RestartContainerOptions, UploadToContainerOptions}; +use bollard::exec::{CreateExecOptions, StartExecOptions, StartExecResults}; +use bollard::image::{BuildImageOptions, BuilderVersion}; +use bollard::{Docker}; +use bytes::Bytes; +use futures::StreamExt; +use std::collections::HashMap; +use std::{fs, io}; +use std::ffi::CString; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, LazyLock}; +use bollard::errors::Error; +use bollard::models::{ContainerInspectResponse, ContainerState, ContainerSummary, Health, HealthStatusEnum, ImageInspect}; +use sha2::{Digest, Sha256}; +use tar::{Builder, Header}; +use tokio::runtime::Runtime; +use uuid::Uuid; +use crate::containers::options::{CreateContainerOptionsBuilder, StopContainerOptionsBuilder}; +use crate::environment::TEST_ENVIRONMENT; +use crate::HexSlice; + +pub(crate) static CONTAINER_PATH: LazyLock = LazyLock::new(|| { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("pipewire-test-utils") + .join(".containers") + .as_path() + .to_path_buf() +}); + +pub(crate) static CONTAINER_TMP_PATH: LazyLock = LazyLock::new(|| { + let path = CONTAINER_PATH + .join(".tmp") + .as_path() + .to_path_buf(); + fs::create_dir_all(&path).unwrap(); + path +}); + +pub(crate) static CONTAINER_DIGESTS_FILE_PATH: LazyLock = LazyLock::new(|| { + CONTAINER_PATH + .join(".digests") + .as_path() + .to_path_buf() +}); + +pub struct ImageRegistry { + images: HashMap, +} + +impl ImageRegistry { + pub fn new() -> Self { + let file = match fs::read_to_string(&*CONTAINER_DIGESTS_FILE_PATH) { + Ok(value) => value, + Err(_) => return Self { + images: HashMap::new(), + } + }; + let images = file.lines() + .into_iter() + .map(|line| { + let line_parts = line.split("=").collect::>(); + let image_name = line_parts[0]; + let container_file_digest = line_parts[1]; + (image_name.to_string(), container_file_digest.to_string().to_string()) + }) + .collect::>(); + Self { + images, + } + } + + pub fn push(&mut self, image_name: String, container_file_digest: String) { + if self.images.contains_key(&image_name) { + *self.images.get_mut(&image_name).unwrap() = container_file_digest + } + else { + self.images.insert(image_name, container_file_digest); + } + } + + pub fn is_build_needed(&self, image_name: &String, digest: &String) -> bool { + self.images.get(image_name).map_or(true, |entry| { + *entry != *digest + }) + } + + pub(crate) fn cleanup(&self) { + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&*CONTAINER_DIGESTS_FILE_PATH) + .unwrap(); + for (image_name, container_file_digest) in self.images.iter() { + unsafe { + let format = CString::new("Registering image digests to further process: %s\n").unwrap(); + libc::printf(format.as_ptr() as *const i8, CString::new(image_name.clone()).unwrap()); + } + // println!("Registering image digests to further process: {}", image_name); + writeln!( + file, + "{}={}", + image_name, + container_file_digest + ).unwrap(); + } + } +} + +pub struct ContainerRegistry { + api: ContainerApi, + containers: Vec +} + +impl ContainerRegistry { + pub fn new(api: ContainerApi) -> Self { + let registry = Self { + api: api.clone(), + containers: Vec::new(), + }; + registry.clean(); + registry + } + + pub(crate) fn clean(&self) { + let containers = self.api.get_all().unwrap(); + for container in containers { + let container_id = container.id.unwrap(); + let inspect_result = match self.api.inspect(&container_id) { + Ok(value) => value, + Err(_) => continue + }; + if let Some(state) = inspect_result.state { + self.api.clean(&container_id.to_string(), &state); + } + } + } +} + +struct ImageContext { +} + +impl ImageContext { + fn create(container_file_path: &PathBuf) -> Result<(Bytes, String), Error> { + let excluded_filename = vec![ + ".digests", + ]; + let context_path = container_file_path.parent().unwrap(); + // Hasher is used for computing all context files hashes. + // In that way we can determine later with we build the image or not. + // This is better that just computing context archive hash which include data and metadata + // that can change regarding if context files had not changed in times. + let mut hasher = Sha256::new(); + let mut archive = tar::Builder::new(Vec::new()); + Self::read_directory( + &mut archive, + &mut hasher, + context_path, + context_path, + Some(&excluded_filename) + )?; + let uncompressed = archive.into_inner()?; + let mut compressed = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + compressed.write_all(&uncompressed)?; + let compressed = compressed.finish()?; + let data = Bytes::from(compressed); + let hash_bytes = hasher.finalize().to_vec(); + let digest = HexSlice(hash_bytes.as_slice()); + let digest = format!("sha256:{}", digest); + Ok((data, digest)) + } + + fn read_directory( + archive: &mut Builder>, + hasher: &mut impl Write, + root: &Path, + directory: &Path, + excluded_filenames: Option<&Vec<&str>> + ) -> io::Result<()> { + if directory.is_dir() { + for entry in fs::read_dir(directory)? { + let entry = entry?; + let path = entry.path(); + let filename = path.file_name().unwrap().to_str().unwrap(); + if path.is_dir() { + Self::read_directory(archive, hasher, root, &path, excluded_filenames)?; + } + else if path.is_file() { + if excluded_filenames.as_ref().unwrap().contains(&filename) { + continue; + } + let mut file = File::open(&path)?; + io::copy(&mut file, hasher)?; + file.seek(SeekFrom::Start(0))?; + let mut header = Header::new_gnu(); + let metadata = file.metadata()?; + header.set_path(path.strip_prefix(root).unwrap())?; + header.set_size(metadata.len()); + header.set_mode(metadata.permissions().mode()); + header.set_mtime(metadata.modified()?.elapsed().unwrap().as_secs()); + header.set_cksum(); + archive.append(&header, &mut file)?; + } + } + } + Ok(()) + } +} + +pub struct ImageApi { + runtime: Arc, + api: Arc +} + +impl ImageApi { + pub fn new(runtime: Arc, api: Arc) -> Self { + Self { + runtime, + api, + } + } + + pub fn inspect(&self, image_name: &String) -> Result { + let result = self.api.inspect_image(image_name.as_str()); + self.runtime.block_on(result) + } + + pub fn build( + &self, + container_file_path: &PathBuf, + image_name: &String, + image_tag: &String + ) { + let tag = format!("{}:{}", image_name, image_tag); + let options = BuildImageOptions { + dockerfile: container_file_path.file_name().unwrap().to_str().unwrap(), + t: tag.as_str(), + session: Some(Uuid::new_v4().to_string()), + version: BuilderVersion::BuilderBuildKit, + ..Default::default() + }; + let (context, context_digest) = ImageContext::create(&container_file_path).unwrap(); + let mut environment = TEST_ENVIRONMENT.lock().unwrap(); + println!("Container image digest: {}", context_digest); + if environment.container_image_registry.is_build_needed(&image_name, &context_digest) == false { + println!("Skip build container image: {}", tag); + return; + } + println!("Build container image: {}", tag); + let mut stream = self.api.build_image(options, None, Some(context)); + while let Some(message) = self.runtime.block_on(stream.next()) { + match message { + Ok(message) => { + if let Some(stream) = message.stream { + if cfg!(debug_assertions) { + print!("{}", stream) + } + } + else if let Some(error) = message.error { + panic!("{}", error); + } + } + Err(value) => { + panic!("Error during image build: {:?}", value); + } + } + }; + environment.container_image_registry.push( + image_name.clone(), + context_digest.clone() + ); + } +} + +impl Clone for ImageApi { + fn clone(&self) -> Self { + Self { + runtime: self.runtime.clone(), + api: self.api.clone(), + } + } +} + +pub struct ContainerApi { + runtime: Arc, + api: Arc +} + +impl ContainerApi { + pub fn new(runtime: Arc, api: Arc) -> Self { + Self { + runtime, + api, + } + } + + pub(self) fn get_all(&self) -> Result, Error>{ + let mut filter = HashMap::new(); + filter.insert("label", vec!["test.container=true"]); + let options = ListContainersOptions { + all: true, + filters: filter, + ..Default::default() + }; + let call = self.api.list_containers(Some(options)); + self.runtime.block_on(call) + } + + pub(self) fn clean(&self, id: &String, state: &ContainerState) { + println!("Clean container with id {}", id); + if state.running.unwrap() { + let stop_options = StopContainerOptionsBuilder::default().build(); + let call = self.api.stop_container(id, Some(stop_options)); + self.runtime.block_on(call).unwrap(); + } + let call = self.api.remove_container(id, None); + self.runtime.block_on(call).unwrap(); + } + + pub fn create(&self, options: &mut CreateContainerOptionsBuilder) -> String { + let options = options + .with_label("test.container", true.to_string()) + .build(); + println!("Create container with image {}", options.image.as_ref().unwrap()); + let call = self.api.create_container::(None, options); + let result = self.runtime.block_on(call).unwrap(); + result.id + } + + pub fn start(&self, id: &String) { + println!("Start container with id {}", id); + let call = self.api.start_container::(id, None); + self.runtime.block_on(call).unwrap(); + } + + pub fn stop(&self, id: &String, options: &mut StopContainerOptionsBuilder) { + println!("Stop container with id {}", id); + let options = options.build(); + let call = self.api.stop_container(id, Some(options)); + self.runtime.block_on(call).unwrap(); + } + + pub fn restart(&self, id: &String) { + println!("Restart container with id {}", id); + let options = RestartContainerOptions { + t: 0, + }; + let call = self.api.restart_container(id, Some(options)); + self.runtime.block_on(call).unwrap(); + } + + pub fn remove(&self, id: &String) { + println!("Remove container with id {}", id); + let call = self.api.remove_container(id, None); + self.runtime.block_on(call).unwrap(); + } + + pub fn inspect(&self, id: &String) -> Result { + let call = self.api.inspect_container(id, None); + self.runtime.block_on(call).map_err(|error| { + pipewire_common::error::Error { + description: error.to_string(), + } + }) + } + + pub fn upload(&self, id: &String, path: &str, archive: Bytes) { + let options = UploadToContainerOptions { + path: path.to_string(), + no_overwrite_dir_non_dir: true.to_string(), + }; + let call = self.api.upload_to_container(id, Some(options), archive); + self.runtime.block_on(call).unwrap(); + } + + pub fn wait_healthy(&self, id: &String) { + println!("Wait container with id {} to be healthy", id); + let operation = || { + let response = self.inspect(id); + match response { + Ok(value) => { + let state = value.state.unwrap(); + let health = state.health.unwrap(); + match health { + Health { status, .. } => { + match status.unwrap() { + HealthStatusEnum::HEALTHY => Ok(()), + _ => Err(pipewire_common::error::Error { + description: "Container not yet healthy".to_string(), + }) + } + } + } + } + Err(value) => Err(pipewire_common::error::Error { + description: format!("Container {} not ready: {}", id, value), + }) + } + }; + let mut backoff = Backoff::default(); + backoff.retry(operation).unwrap() + } + + pub fn exec( + &self, + id: &String, + command: Vec<&str>, + detach: bool, + expected_exit_code: u32, + ) -> Result, pipewire_common::error::Error> { + let create_exec_options = CreateExecOptions { + attach_stdout: Some(true), + attach_stderr: Some(true), + tty: Some(true), + cmd: Some(command), + ..Default::default() + }; + let call = self.api.create_exec(id.as_str(), create_exec_options); + let create_exec_result = self.runtime.block_on(call).unwrap(); + let exec_id = create_exec_result.id; + let start_exec_options = StartExecOptions { + detach, + tty: true, + ..Default::default() + }; + let call = self.api.start_exec(exec_id.as_str(), Some(start_exec_options)); + let start_exec_result = self.runtime.block_on(call).unwrap(); + let mut output_result: Vec = Vec::new(); + if let StartExecResults::Attached { mut output, .. } = start_exec_result { + while let Some(Ok(message)) = self.runtime.block_on(output.next()) { + match message { + LogOutput::StdOut { message } => { + output_result.push( + String::from_utf8(message.to_vec()).unwrap() + ) + } + LogOutput::StdErr { message } => { + eprint!("{}", String::from_utf8(message.to_vec()).unwrap()) + } + LogOutput::Console { message } => { + output_result.push( + String::from_utf8(message.to_vec()).unwrap() + ) + } + _ => {} + } + } + let call = self.api.inspect_exec(exec_id.as_str()); + let exec_inspect_result = self.runtime.block_on(call).unwrap(); + let exit_code = exec_inspect_result.exit_code.unwrap(); + if exit_code != expected_exit_code as i64 { + return Err(pipewire_common::error::Error { + description: format!("Unexpected exit code: {exit_code}"), + }); + } + let output_result = output_result.iter() + .flat_map(move |output| { + output.split('\n') + .map(move |line| line.trim().to_string()) + .collect::>() + }) + .collect::>(); + Ok(output_result) + } else { + Ok(output_result) + } + } + + pub fn top(&self, id: &String) -> HashMap { + let call = self.api.top_processes::<&str>(id, None); + let result = self.runtime.block_on(call).unwrap(); + let titles = result.titles.unwrap(); + let pid_column_index = titles.iter().position(move |title| *title == "PID").unwrap(); + let cmd_column_index = titles.iter().position(move |title| *title == "CMD").unwrap(); + let processes = result.processes.unwrap().iter() + .map(|process| { + let pid = process.get(pid_column_index).unwrap(); + let cmd = process.get(cmd_column_index).unwrap(); + (cmd.clone(), pid.clone()) + }) + .collect::>(); + processes + } + + pub fn wait_for_pid(&self, id: &String, process_name: &str) -> u32 { + let operation = || { + let result = self.top(id); + let pid = result.iter() + .map(move |(cmd, pid)| { + let cmd = cmd.split(" ").collect::>(); + let cmd = *cmd.first().unwrap(); + (cmd, pid.clone()) + }) + .filter(move |(cmd, _)| **cmd == *process_name) + .map(|(_, pid)| pid.parse::().unwrap()) + .collect::>(); + match pid.first() { + Some(value) => Ok(value.clone()), + None => Err(pipewire_common::error::Error { + description: "Process not yet spawned".to_string(), + }) + } + }; + let mut backoff = Backoff::default(); + backoff.retry(operation).unwrap() + } + + fn wait_for_file_type(&self, id: &String, file_type: &str, path: &PathBuf) { + let file_type_argument = match file_type { + "file" => "-f", + "socket" => "-S", + _ => panic!("Cannot determine file type"), + }; + let operation = || { + self.exec( + id, + vec![ + "test", file_type_argument, path.to_str().unwrap() + ], + false, + 0 + )?; + Ok::<(), pipewire_common::error::Error>(()) + }; + let mut backoff = Backoff::default(); + backoff.retry(operation).unwrap() + } + + pub fn wait_for_file(&self, id: &String, path: &PathBuf) { + self.wait_for_file_type(id, "file", path); + } + + pub fn wait_for_socket_file(&self, id: &String, path: &PathBuf) { + self.wait_for_file_type(id, "socket", path); + } + + pub fn wait_for_socket_listening(&self, id: &String, path: &PathBuf) { + let operation = || { + self.exec( + id, + vec![ + "socat", "-u", "OPEN:/dev/null", + format!("UNIX-CONNECT:{}", path.to_str().unwrap()).as_str() + ], + false, + 0 + )?; + Ok::<(), pipewire_common::error::Error>(()) + }; + let mut backoff = Backoff::default(); + backoff.retry(operation).unwrap() + } + + pub fn wait_for_command_output(&self, id: &String, command: Vec<&str>, expected_output: &str) { + let operation = || { + let command_output = self.exec( + id, + command.clone(), + false, + 0 + )?; + return if command_output.iter().any(|output| { + let output = output.trim(); + output == expected_output + }) { + Ok(()) + } else { + Err(pipewire_common::error::Error { + description: format!("Unexpected output {}", expected_output) + }) + }; + }; + let mut backoff = Backoff::default(); + backoff.retry(operation).unwrap() + } +} + +impl Clone for ContainerApi { + fn clone(&self) -> Self { + Self { + runtime: self.runtime.clone(), + api: self.api.clone(), + } + } +} \ No newline at end of file diff --git a/pipewire-test-utils/src/containers/mod.rs b/pipewire-test-utils/src/containers/mod.rs new file mode 100644 index 000000000..40c67f24d --- /dev/null +++ b/pipewire-test-utils/src/containers/mod.rs @@ -0,0 +1,3 @@ +pub mod container; +pub mod options; +pub mod sync_api; \ No newline at end of file diff --git a/pipewire-test-utils/src/containers/options.rs b/pipewire-test-utils/src/containers/options.rs new file mode 100644 index 000000000..9211172b2 --- /dev/null +++ b/pipewire-test-utils/src/containers/options.rs @@ -0,0 +1,269 @@ +use bollard::container::{Config, StopContainerOptions}; +use bollard::models::{HealthConfig, HostConfig, Mount, MountBindOptions, MountTypeEnum, ResourcesUlimits}; +use std::collections::HashMap; +use std::time::Duration; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Unit { + Bytes, + KB, + MB, + GB, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Size { + value: f64, + unit: Unit, +} + +impl Size { + const fn new(value: f64, unit: Unit) -> Self { + Self { value, unit } + } + + pub const fn from_bytes(bytes: f64) -> Self { + Self::new(bytes, Unit::Bytes) + } + + pub const fn from_kb(kb: f64) -> Self { + Self::new(kb, Unit::KB) + } + + pub const fn from_mb(mb: f64) -> Self { + Self::new(mb, Unit::MB) + } + + pub const fn from_gb(gb: f64) -> Self { + Self::new(gb, Unit::GB) + } +} + +impl From for Size { + fn from(value: String) -> Self { + let value = value.trim(); + let unit = value.chars().last().unwrap(); + let unit = match unit { + 'b' => Unit::Bytes, + 'k' => Unit::KB, + 'm' => Unit::MB, + 'g' => Unit::GB, + _ => panic!("Invalid unit {:?}. Only b, k, m, g are supported.", unit), + }; + let value = value.chars().take(value.len() - 2).collect::(); + let value = value.parse::().unwrap(); + Self::new(value, unit) + } +} + +impl From for u64 { + fn from(value: Size) -> Self { + match value.unit { + Unit::Bytes => value.value as u64, + Unit::KB => (value.value * 1024.0) as u64, + Unit::MB => (value.value * 1024.0 * 1024.0) as u64, + Unit::GB => (value.value * 1024.0 * 1024.0 * 1024.0) as u64, + } + } +} + +impl From for i64 { + fn from(value: Size) -> Self { + let value: u64 = value.into(); + value as i64 + } +} + +pub struct CreateContainerOptionsBuilder { + image: Option, + environment: Option>, + volumes: Option>, + labels: Option>, + entrypoint: Option>, + healthcheck: Option>, + cpus: Option, + memory_swap: Option, + memory: Option +} + +impl Default for CreateContainerOptionsBuilder { + fn default() -> Self { + Self { + image: None, + environment: None, + volumes: None, + labels: None, + entrypoint: None, + healthcheck: None, + cpus: None, + memory_swap: None, + memory: None, + } + } +} + +impl CreateContainerOptionsBuilder { + pub fn with_image(&mut self, image: impl Into) -> &mut Self { + self.image = Some(image.into()); + self + } + + pub fn with_environment(&mut self, key: impl Into, value: impl Into) -> &mut Self { + if let None = self.environment { + self.environment = Some(HashMap::new()); + } + if let Some(environment) = self.environment.as_mut() { + environment.insert(key.into(), value.into()); + } + self + } + + pub fn with_volume(&mut self, name: impl Into, container_path: impl Into) -> &mut Self { + if let None = self.volumes { + self.volumes = Some(HashMap::new()); + } + if let Some(volumes) = self.volumes.as_mut() { + volumes.insert(name.into(), container_path.into()); + } + self + } + + pub fn with_label(&mut self, key: impl Into, value: impl Into) -> &mut Self { + if let None = self.labels { + self.labels = Some(HashMap::new()); + } + if let Some(labels) = self.labels.as_mut() { + labels.insert(key.into(), value.into()); + } + self + } + + pub fn with_entrypoint(&mut self, value: impl Into) -> &mut Self { + if let None = self.entrypoint { + self.entrypoint = Some(Vec::new()); + } + if let Some(entrypoint) = self.entrypoint.as_mut() { + let mut value = value.into().split(" ") + .map(|s| s.to_string()) + .collect::>(); + entrypoint.append(&mut value); + } + self + } + + pub fn with_healthcheck_command(&mut self, value: impl Into) -> &mut Self { + if let None = self.healthcheck { + self.healthcheck = Some(Vec::new()); + } + if let Some(healthcheck) = self.healthcheck.as_mut() { + healthcheck.clear(); + healthcheck.push("CMD".to_string()); + healthcheck.push(value.into()); + } + self + } + + pub fn with_healthcheck_command_shell(&mut self, value: impl Into) -> &mut Self { + if let None = self.healthcheck { + self.healthcheck = Some(Vec::new()); + } + if let Some(healthcheck) = self.healthcheck.as_mut() { + healthcheck.clear(); + healthcheck.push("CMD-SHELL".to_string()); + healthcheck.push(value.into()); + } + self + } + + pub fn with_cpus(&mut self, cpus: f64) -> &mut Self { + self.cpus = Some(cpus); + self + } + + pub fn with_memory_swap(&mut self, memory_swap: Size) -> &mut Self { + self.memory_swap = Some(memory_swap); + self + } + + pub fn with_memory(&mut self, memory: Size) -> &mut Self { + self.memory = Some(memory); + self + } + + pub fn build(&self) -> Config { + if self.image.is_none() { + panic!("Image is required"); + } + let mut builder = Config::default(); + builder.image = self.image.clone(); + builder.host_config = Some(HostConfig::default()); + if let Some(environment) = self.environment.as_ref() { + let environment = environment.iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect::>(); + builder.env = Some(environment); + } + if let Some(volumes) = self.volumes.as_ref() { + let mut volumes = volumes.iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .collect::>(); + let host_config = builder.host_config.as_mut().unwrap(); + if let None = host_config.binds { + host_config.binds = Some(Vec::new()) + } + if let Some(ref mut binds) = &mut host_config.binds { + binds.append(&mut volumes) + } + } + if let Some(labels) = self.labels.as_ref() { + builder.labels = Some(labels.clone()); + } + if let Some(entrypoint) = self.entrypoint.as_ref() { + builder.entrypoint = Some(entrypoint.clone()); + } + if let Some(healthcheck) = self.healthcheck.as_ref() { + builder.healthcheck = Some(HealthConfig { + test: Some(healthcheck.clone()), + ..HealthConfig::default() + }); + } + if let Some(cpus) = self.cpus.clone() { + let host_config = builder.host_config.as_mut().unwrap(); + host_config.nano_cpus = Some((1_000_000_000.0 * cpus) as i64); + } + if let Some(memory_swap) = self.memory_swap.clone() { + let host_config = builder.host_config.as_mut().unwrap(); + host_config.memory_swap = Some(memory_swap.into()); + } + if let Some(memory) = self.memory.clone() { + let host_config = builder.host_config.as_mut().unwrap(); + host_config.memory = Some(memory.into()); + } + builder + } +} + +pub struct StopContainerOptionsBuilder { + wait: Option, +} + +impl Default for StopContainerOptionsBuilder { + fn default() -> Self { + Self { + wait: Some(Duration::from_secs(0)), + } + } +} + +impl StopContainerOptionsBuilder { + pub fn with_wait(&mut self, time: Duration) -> &mut Self { + self.wait = Some(time); + self + } + + pub fn build(&self) -> StopContainerOptions { + let mut builder = StopContainerOptions::default(); + builder.t = self.wait.unwrap().as_secs() as i64; + builder + } +} \ No newline at end of file diff --git a/pipewire-test-utils/src/containers/sync_api.rs b/pipewire-test-utils/src/containers/sync_api.rs new file mode 100644 index 000000000..aae966767 --- /dev/null +++ b/pipewire-test-utils/src/containers/sync_api.rs @@ -0,0 +1,414 @@ +use bollard::container::{RemoveContainerOptions, StopContainerOptions}; +use bollard::errors::Error; +use bollard::errors::Error::{DockerResponseServerError, RequestTimeoutError}; +use bollard::{ClientVersion}; +use bytes::{Buf, Bytes}; +use http::header::CONTENT_TYPE; +use http::{Method, Request, Response, StatusCode, Version}; +use pipewire_common::utils::Backoff; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::ffi::OsStr; +use std::sync::atomic::AtomicUsize; +use std::sync::Arc; +use std::{fmt, io}; +use std::path::Path; +use http::request::Builder; +use ureq::{Agent, Body, RequestBuilder, SendBody}; +use ureq::middleware::MiddlewareNext; +use ureq::typestate::{WithBody, WithoutBody}; +use url::Url; + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +struct DockerServerErrorMessage { + message: String, +} + +pub trait Connector { + fn execute(&self, request: &mut Request) -> http::Result>; +} + +pub struct HttpConnector { + agent: Agent +} + +impl HttpConnector { + pub fn new() -> Self { + let config = Agent::config_builder() + .build(); + let agent = config.into(); + Self { + agent, + } + } + + fn request_builder(&self, request: &mut Request) -> ureq::RequestBuilder { + if request.method() == http::Method::GET { + self.agent.get(request.uri()) + .force_send_body() + } + else if request.method() == http::Method::POST { + self.agent.post(request.uri()) + } + else if request.method() == http::Method::PUT { + self.agent.put(request.uri()) + } + else if request.method() == http::Method::PATCH { + self.agent.patch(request.uri()) + } + else if request.method() == http::Method::DELETE { + self.agent.delete(request.uri()).force_send_body() + } + else if request.method() == http::Method::HEAD { + self.agent.head(request.uri()).force_send_body() + } + else if request.method() == http::Method::OPTIONS { + self.agent.options(request.uri()).force_send_body() + } + else { + panic!("Not supported HTTP method") + } + } +} + +impl Connector for HttpConnector +{ + fn execute(&self, mut request: &mut Request) -> http::Result> { + let mut builder = self.request_builder(request) + .version(Version::HTTP_11) + .uri(request.uri()); + for (key, value) in request.headers() { + builder = builder.header(key.as_str(), value.to_str().unwrap()); + } + let mut data = Vec::new(); + let mut bytes = request.body_mut().reader(); + io::copy(&mut bytes, &mut data).unwrap(); + let body = Body::builder() + .data(data); + let mut response = builder.send(body).unwrap(); + let mut builder = http::Response::builder() + .status(response.status()) + .version(response.version()); + for (key, value) in response.headers().iter() { + builder = builder.header(key.as_str(), value.to_str().unwrap()); + } + let mut data = Vec::new(); + let mut reader = response.body_mut().as_reader(); + io::copy(&mut reader, &mut data).unwrap(); + let bytes = Bytes::from(data); + builder.body(bytes) + } +} + +pub struct UnixConnector { + +} + +impl UnixConnector { + pub fn new() -> Self { + Self { + + } + } +} + +impl Connector for UnixConnector { + fn execute(&self, request: &mut Request) -> http::Result> { + todo!() + } +} + +pub struct Client + where C: Connector +{ + connector: C, +} + +impl Client +where + C: Connector +{ + pub fn new(connector: C) -> Self { + Self { + connector, + } + } + + pub fn execute(&self, request: &mut Request) -> http::Result> { + self.connector.execute(request) + } +} + +pub(crate) enum Transport { + Http { + client: Client, + }, + Unix { + client: Client, + }, +} + +impl fmt::Debug for Transport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Transport::Http { .. } => write!(f, "HTTP"), + Transport::Unix { .. } => write!(f, "Unix"), + } + } +} + +#[derive(Debug, Clone)] +pub(crate) enum ClientType { + Unix, + Http, +} + +#[derive(Debug)] +pub struct Uri<'a> { + encoded: Cow<'a, str>, +} + +impl<'a> Uri<'a> { + pub(crate) fn parse( + socket: &'a str, + client_type: &ClientType, + path: &'a str, + query: Option, + client_version: &ClientVersion, + ) -> Result + where + O: serde::ser::Serialize, + { + let host_str = format!( + "{}://{}/v{}.{}{}", + Uri::socket_scheme(client_type), + Uri::socket_host(socket, client_type), + client_version.major_version, + client_version.minor_version, + path + ); + let mut url = Url::parse(host_str.as_ref())?; + url = url.join(path)?; + + if let Some(pairs) = query { + let qs = serde_urlencoded::to_string(pairs)?; + url.set_query(Some(&qs)); + } + Ok(Uri { + encoded: Cow::Owned(url.as_str().to_owned()), + }) + } + + fn socket_host

(socket: P, client_type: &ClientType) -> String + where + P: AsRef, + { + match client_type { + ClientType::Http => socket.as_ref().to_string_lossy().into_owned(), + ClientType::Unix => hex::encode(socket.as_ref().to_string_lossy().as_bytes()), + } + } + + fn socket_scheme(client_type: &'a ClientType) -> &'a str { + match client_type { + ClientType::Http => "http", + ClientType::Unix => "unix", + } + } +} + +pub struct SyncContainerApi { + pub(crate) transport: Arc, + pub(crate) client_type: ClientType, + pub(crate) client_addr: String, + pub(crate) client_timeout: u64, + pub(crate) version: Arc<(AtomicUsize, AtomicUsize)>, +} + +impl SyncContainerApi { + pub fn connect_with_http( + addr: &str, + timeout: u64, + client_version: &ClientVersion, + ) -> Result { + let client_addr = addr.replacen("tcp://", "", 1).replacen("http://", "", 1); + let http_connector = HttpConnector::new(); + let client = Client::new(http_connector); + let transport = Transport::Http { client }; + let docker = Self { + transport: Arc::new(transport), + client_type: ClientType::Http, + client_addr, + client_timeout: timeout, + version: Arc::new(( + AtomicUsize::new(client_version.major_version), + AtomicUsize::new(client_version.minor_version), + )), + }; + Ok(docker) + } + + pub fn connect_with_socket( + path: &str, + timeout: u64, + client_version: &ClientVersion, + ) -> Result { + let clean_path = path.trim_start_matches("unix://"); + if !std::path::Path::new(clean_path).exists() { + return Err(Error::SocketNotFoundError(clean_path.to_string())); + } + let docker = Self::connect_with_unix(path, timeout, client_version)?; + Ok(docker) + } + + pub fn connect_with_unix( + path: &str, + timeout: u64, + client_version: &ClientVersion, + ) -> Result { + let client_addr = path.replacen("unix://", "", 1); + if !Path::new(&client_addr).exists() { + return Err(Error::SocketNotFoundError(client_addr)); + } + let unix_connector = UnixConnector::new(); + let client = Client::new(unix_connector); + let transport = Transport::Unix { client }; + let docker = Self { + transport: Arc::new(transport), + client_type: ClientType::Unix, + client_addr, + client_timeout: timeout, + version: Arc::new(( + AtomicUsize::new(client_version.major_version), + AtomicUsize::new(client_version.minor_version), + )), + }; + Ok(docker) + } + + pub fn client_version(&self) -> ClientVersion { + self.version.as_ref().into() + } + + pub fn stop( + &self, + container_name: &str, + options: Option, + ) -> Result<(), Error> { + let url = format!("/containers/{container_name}/stop"); + + let req = self.build_request( + &url, + Builder::new().method(Method::POST), + options, + Ok(Bytes::new()), + ); + self.process_request(req)?; + Ok(()) + } + + pub fn remove( + &self, + container_name: &str, + options: Option, + ) -> Result<(), Error> { + let url = format!("/containers/{container_name}"); + let req = self.build_request( + &url, + Builder::new().method(Method::DELETE), + options, + Ok(Bytes::new()), + ); + self.process_request(req)?; + Ok(()) + } + + pub(crate) fn build_request( + &self, + path: &str, + builder: Builder, + query: Option, + payload: Result, + ) -> Result, Error> + where + O: Serialize, + { + let uri = Uri::parse( + &self.client_addr, + &self.client_type, + path, + query, + &self.client_version(), + )?; + Ok(builder + .uri(uri.encoded.to_string()) + .header(CONTENT_TYPE, "application/json") + .body(payload?)?) + } + + pub(crate) fn process_request( + &self, + request: Result, Error>, + ) -> Result, Error> { + let transport = self.transport.clone(); + let timeout = self.client_timeout; + + let mut request = request?; + let response = Self::execute_request(transport, &mut request, timeout)?; + + let status = response.status(); + match status { + // Status code 200 - 299 or 304 + s if s.is_success() || s == StatusCode::NOT_MODIFIED => Ok(response), + + StatusCode::SWITCHING_PROTOCOLS => Ok(response), + + // All other status codes + _ => { + let contents = Self::decode_into_string(response)?; + + let mut message = String::new(); + if !contents.is_empty() { + message = serde_json::from_str::(&contents) + .map(|msg| msg.message) + .or_else(|e| { + if e.is_data() || e.is_syntax() { + Ok(contents) + } else { + Err(e) + } + })?; + } + Err(DockerResponseServerError { + status_code: status.as_u16(), + message, + }) + } + } + } + + fn execute_request( + transport: Arc, + request: &mut Request, + timeout: u64, + ) -> Result, Error> { + let operation = || { + let request = match *transport { + Transport::Http { ref client } => client.execute(request), + Transport::Unix { ref client } => client.execute(request), + }; + let request = request.map_err(Error::from); + match request { + Ok(value) => Ok(value), + Err(value) => Err(value), + } + }; + let mut backoff = Backoff::constant((timeout * 1000) as u128); + backoff.retry(operation).map_err(|_| RequestTimeoutError) + } + + fn decode_into_string(response: Response) -> Result { + let body = response.into_body(); + Ok(String::from_utf8_lossy(&body).to_string()) + } +} \ No newline at end of file diff --git a/pipewire-test-utils/src/environment.rs b/pipewire-test-utils/src/environment.rs new file mode 100644 index 000000000..f40fce89f --- /dev/null +++ b/pipewire-test-utils/src/environment.rs @@ -0,0 +1,231 @@ +use std::sync::{Arc, LazyLock, Mutex}; +use std::time::Duration; +use bollard::{Docker, API_DEFAULT_VERSION}; +use ctor::{ctor, dtor}; +use libc::{atexit, signal, SIGABRT, SIGINT, SIGSEGV, SIGTERM}; +use tokio::runtime::{Runtime}; +use pipewire_common::error::Error; +use url::Url; +use crate::containers::container::{ContainerApi, ContainerRegistry, ImageApi, ImageRegistry}; +use crate::containers::options::{Size}; +use crate::containers::sync_api::SyncContainerApi; +use crate::server::{server_with_default_configuration, Server}; + +pub static SHARED_SERVER: LazyLock> = LazyLock::new(move || { + let server = server_with_default_configuration(); + server +}); + +pub static TEST_ENVIRONMENT: LazyLock> = LazyLock::new(|| { + unsafe { + signal(SIGINT, cleanup_test_environment as usize); + signal(SIGTERM, cleanup_test_environment as usize); + } + unsafe { libc::printf("Initialize test environment\n\0".as_ptr() as *const i8); }; + Mutex::new(Environment::from_env()) +}); + +#[dtor] +unsafe fn cleanup_test_environment() { + libc::printf("Cleaning test environment\n\0".as_ptr() as *const i8); + let environment = TEST_ENVIRONMENT.lock().unwrap(); + environment.container_image_registry.cleanup(); + SHARED_SERVER.cleanup(); +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TestTarget { + Local, + Container, +} + +impl From for TestTarget { + fn from(value: String) -> Self { + match value.as_str() { + "local" => TestTarget::Local, + "container" => TestTarget::Container, + _ => panic!("Unknown test target {}", value), + } + } +} + +pub struct Environment { + pub runtime: Arc, + pub container_api: ContainerApi, + pub container_image_api: ImageApi, + pub container_api_timeout: Duration, + pub container_registry: ContainerRegistry, + pub container_image_registry: ImageRegistry, + pub container_cpu: f64, + pub container_memory: Size, + pub container_memory_swap: Size, + pub test_target: TestTarget, + pub client_timeout: Duration, +} + +impl Environment { + pub fn from_env() -> Self { + let default = Self::default(); + let container_api_timeout = match std::env::var("CONTAINER_API_TIMEOUT") { + Ok(value) => Self::parse_duration(value), + Err(_) => default.container_api_timeout + }; + let container_cpu = match std::env::var("CONTAINER_CPU") { + Ok(value) => value.parse::().unwrap(), + Err(_) => default.container_cpu, + }; + let container_memory = match std::env::var("CONTAINER_MEMORY") { + Ok(value) => value.into(), + Err(_) => default.container_memory + }; + let container_memory_swap = match std::env::var("CONTAINER_MEMORY_SWAP") { + Ok(value) => value.into(), + Err(_) => default.container_memory + }; + let test_target = match std::env::var("TEST_TARGET") { + Ok(value) => value.into(), + Err(_) => default.test_target.clone(), + }; + Self { + runtime: default.runtime.clone(), + container_api: default.container_api, + container_image_api: default.container_image_api, + container_api_timeout, + container_registry: default.container_registry, + container_image_registry: default.container_image_registry, + container_cpu, + container_memory, + container_memory_swap, + test_target, + client_timeout: default.client_timeout, + } + } + + fn parse_duration(value: String) -> Duration { + let value = value.trim(); + let suffix_length = value.strip_suffix("ms") + .map_or_else(|| 1, |_| 2); + let suffix_start_index = value.len() - suffix_length; + let unit = value.get(suffix_start_index..).unwrap(); + let value = value.get(..suffix_start_index) + .unwrap() + .parse::() + .unwrap(); + match unit { + "ms" => Duration::from_millis(value), + "s" => Duration::from_secs(value), + "m" => Duration::from_secs(value * 60), + _ => panic!("Invalid unit {:?}. Only ms, s, m are supported.", unit), + } + } + + fn parse_container_host( + on_http: impl FnOnce(&String) -> T, + on_socket: impl FnOnce(&String) -> T, + ) -> Result, Error> { + const DOCKER_HOST_ENVIRONMENT_KEY: &str = "DOCKER_HOST"; + const CONTAINER_HOST_ENVIRONMENT_KEY: &str = "CONTAINER_HOST"; + + let docker_host = std::env::var(DOCKER_HOST_ENVIRONMENT_KEY); + let container_host = std::env::var(CONTAINER_HOST_ENVIRONMENT_KEY); + let host = match (docker_host, container_host) { + (Ok(value), Ok(_)) => value, + (Ok(value), Err(_)) => value, + (Err(_), Ok(value)) => value, + (Err(_), Err(_)) => return Err(Error { + description: format!( + "${} or ${} should be set.", + DOCKER_HOST_ENVIRONMENT_KEY, CONTAINER_HOST_ENVIRONMENT_KEY + ) + }), + }; + let host_url = Url::parse(host.as_str()).unwrap(); + let api = match host_url.scheme() { + "http" | "tcp" => on_http(&host), + "unix" => on_socket(&host), + _ => return Err(Error { + description: format!("Unsupported uri format {}", host_url), + }), + }; + Ok(Arc::new(api)) + } + + pub fn create_runtime() -> Arc { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .max_blocking_threads(1) + .worker_threads(1) + .build() + .unwrap(); + Arc::new(runtime) + } + + pub fn create_docker_api(timeout: &Duration) -> Arc { + let on_http = |host: &String| { + Docker::connect_with_http( + host.as_str(), + timeout.as_secs(), + API_DEFAULT_VERSION + ).unwrap() + }; + let on_socket = |host: &String| { + Docker::connect_with_unix( + host.as_str(), + timeout.as_secs(), + API_DEFAULT_VERSION + ).unwrap() + }; + match Self::parse_container_host(on_http, on_socket) { + Ok(value) => value, + Err(value) => panic!("{}", value), + } + } + + pub fn create_docker_sync_api(timeout: &Duration) -> Arc { + let on_http = |host: &String| { + SyncContainerApi::connect_with_http( + host.as_str(), + timeout.as_secs(), + API_DEFAULT_VERSION + ).unwrap() + }; + let on_socket = |host: &String| { + SyncContainerApi::connect_with_unix( + host.as_str(), + timeout.as_secs(), + API_DEFAULT_VERSION + ).unwrap() + }; + match Self::parse_container_host(on_http, on_socket) { + Ok(value) => value, + Err(value) => panic!("{}", value), + } + } + + pub fn create_container_api(runtime: Arc, api: Arc) -> ContainerApi { + ContainerApi::new(runtime.clone(), api.clone()) + } +} + +impl Default for Environment { + fn default() -> Self { + let timeout = Duration::from_secs(30); + let runtime = Self::create_runtime(); + let api_runtime = Self::create_runtime(); + let docker_api = Self::create_docker_api(&timeout); + let container_api = Self::create_container_api(api_runtime.clone(), docker_api.clone()); + Self { + runtime, + container_api: container_api.clone(), + container_image_api: ImageApi::new(api_runtime.clone(), docker_api), + container_api_timeout: timeout, + container_registry: ContainerRegistry::new(container_api.clone()), + container_image_registry: ImageRegistry::new(), + container_cpu: 2.0, + container_memory: Size::from_mb(100.0), + container_memory_swap: Size::from_mb(100.0), + test_target: TestTarget::Container, + client_timeout: Duration::from_secs(3), + } + } +} \ No newline at end of file diff --git a/pipewire-test-utils/src/lib.rs b/pipewire-test-utils/src/lib.rs new file mode 100644 index 000000000..475e6b167 --- /dev/null +++ b/pipewire-test-utils/src/lib.rs @@ -0,0 +1,17 @@ +use std::fmt; +use std::fmt::Display; + +pub mod containers; +pub mod server; +pub mod environment; + +pub(crate) struct HexSlice<'a>(&'a [u8]); + +impl<'a> Display for HexSlice<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for &byte in self.0 { + write!(f, "{:0>2x}", byte)?; + } + Ok(()) + } +} \ No newline at end of file diff --git a/pipewire-test-utils/src/server.rs b/pipewire-test-utils/src/server.rs new file mode 100644 index 000000000..910b03c7a --- /dev/null +++ b/pipewire-test-utils/src/server.rs @@ -0,0 +1,730 @@ +use crate::containers::container::{ContainerApi, ImageApi, CONTAINER_PATH, CONTAINER_TMP_PATH}; +use crate::containers::options::{CreateContainerOptionsBuilder, StopContainerOptionsBuilder}; +use crate::environment::{Environment, TestTarget, TEST_ENVIRONMENT}; +use bytes::Bytes; +use pipewire_common::constants::*; +use pipewire_common::impl_callback; +use pipewire_common::error::Error; +use rstest::fixture; +use std::fmt::{Debug, Formatter}; +use std::fs; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, LazyLock, Mutex}; +use std::time::Duration; +use bollard::container::RemoveContainerOptions; +use tar::{Builder, EntryType, Header}; +use tokio::runtime::Runtime; +use uuid::Uuid; + +static CONTAINER_FILE_PATH: LazyLock = LazyLock::new(|| { + CONTAINER_PATH + .clone() + .join("pipewire.test.container") + .as_path() + .to_path_buf() +}); + +static PIPEWIRE_SERVICE: LazyLock = LazyLock::new(move || { + let service = Service::new( + "pipewire".to_string(), + |server| { + server.spawn("pipewire") + .build(); + }, + |server| { + server.wait_for_pipewire() + }, + ); + service +}); + +static WIREPLUMBER_SERVICE: LazyLock = LazyLock::new(move || { + let service = Service::new( + "wireplumber".to_string(), + |server| { + server.spawn("wireplumber") + .build(); + }, + |server| { + server.wait_for_wireplumber() + }, + ); + service +}); + +static PULSE_SERVICE: LazyLock = LazyLock::new(move || { + let service = Service::new( + "pulse".to_string(), + |server| { + server.spawn("pipewire-pulse") + .build(); + }, + |server| { + server.wait_for_pulse(); + } + ); + service +}); + +struct SpawnCommandBuilder<'a> { + configuration: &'a mut Vec>, + command: Option, + arguments: Option>, + realtime_priority: Option, + user: Option, + auto_start: Option, + auto_restart: Option, + start_retries: Option, +} + +impl <'a> SpawnCommandBuilder<'a> { + pub fn new(configuration: &'a mut Vec>) -> Self { + Self { + configuration, + command: None, + arguments: None, + user: None, + realtime_priority: None, + auto_start: None, + auto_restart: None, + start_retries: None, + } + } + + pub fn with_command(&mut self, command: &str) -> &mut Self { + self.command = Some(command.to_string()); + self + } + + pub fn with_arguments(&mut self, arguments: Vec<&str>) -> &mut Self { + self.arguments = Some(arguments.iter().map(|s| s.to_string()).collect()); + self + } + + pub fn with_realtime_priority(&mut self, priority: u32) -> &mut Self { + self.realtime_priority = Some(priority); + self + } + + pub fn with_user(&mut self, user: &str) -> &mut Self { + self.user = Some(user.to_string()); + self + } + + pub fn with_auto_start(&mut self, auto_start: bool) -> &mut Self { + self.auto_start = Some(auto_start); + self + } + + pub fn with_auto_restart(&mut self, auto_restart: bool) -> &mut Self { + self.auto_restart = Some(auto_restart); + self + } + + pub fn with_start_retries(&mut self, start_retries: u32) -> &mut Self { + self.start_retries = Some(start_retries); + self + } + + pub fn build(&mut self) { + if self.command.is_none() { + panic!("Command is required"); + } + let command = self.command.as_ref().unwrap(); + let process_name = PathBuf::from(command); + let process_name = process_name.file_name().unwrap().to_str().unwrap(); + let command = match self.arguments.as_ref() { + Some(value) => format!("{} {}", command, value.join(" ")), + None => command.to_string(), + }; + let command = match self.realtime_priority.as_ref() { + Some(value) => format!("chrt {} {}", value, command), + None => command, + }; + let mut configuration = Vec::new(); + configuration.append(&mut vec![ + format!("[program:{}]", process_name), + format!("command={}", command), + format!("stdout_logfile=/tmp/{}.out.log", process_name), + format!("stderr_logfile=/tmp/{}.err.log", process_name), + ]); + match self.user.as_ref() { + Some(value) => configuration.push(format!("user={}", value.to_string()).to_string()), + None => {} + }; + match self.auto_start { + Some(value) => configuration.push(format!("autostart={}", value.to_string()).to_string()), + None => {} + }; + match self.auto_restart { + Some(value) => configuration.push(format!("autorestart={}", value.to_string()).to_string()), + None => {} + }; + match self.start_retries { + Some(value) => configuration.push(format!("startretries={}", value.to_string()).to_string()), + None => {} + }; + self.configuration.push(configuration) + } +} + +impl_callback!( + Fn => (), + LifeCycleCallback, + server : &mut ServerApi +); + +pub struct Service { + name: String, + entrypoint: LifeCycleCallback, + healthcheck: LifeCycleCallback, +} + +impl Service { + pub fn new( + name: String, + entrypoint: impl Fn(&mut ServerApi) + Sync + Send + 'static, + healthcheck: impl Fn(&mut ServerApi) + Sync + Send + 'static, + ) -> Self { + Self { + name, + entrypoint: LifeCycleCallback::from(entrypoint), + healthcheck: LifeCycleCallback::from(healthcheck), + } + } + + pub fn entrypoint(&mut self, server: &mut ServerApi) { + self.entrypoint.call(server); + } + + pub fn healthcheck(&mut self, server: &mut ServerApi) { + self.healthcheck.call(server); + } +} + +impl Clone for Service { + fn clone(&self) -> Self { + Self { + name: self.name.clone(), + entrypoint: self.entrypoint.clone(), + healthcheck: self.healthcheck.clone(), + } + } +} + +pub struct ServerApi { + name: String, + tag: String, + socket_id: Uuid, + container_file_path: PathBuf, + image_api: ImageApi, + container_api: ContainerApi, + container: Option, + configuration: Vec>, + entrypoint: Vec>, + healthcheck: Vec>, + post_start: Vec>, +} + +impl ServerApi { + pub(self) fn new( + name: String, + container_file_path: PathBuf, + ) -> Self { + let environment = TEST_ENVIRONMENT.lock().unwrap(); + Self { + name, + tag: "latest".to_string(), + socket_id: Uuid::new_v4(), + container_file_path, + image_api: environment.container_image_api.clone(), + container_api: environment.container_api.clone(), + container: None, + configuration: Vec::new(), + entrypoint: Vec::new(), + healthcheck: Vec::new(), + post_start: Vec::new(), + } + } + + fn socket_location(&self) -> PathBuf { + Path::new("/run/pipewire-sockets").join(self.socket_id.to_string()).to_path_buf() + } + + fn socket_name(&self) -> String { + format!("{}", self.socket_id) + } + + fn build(&self) { + self.generate_configuration_file(); + self.generate_entrypoint_script(); + self.generate_healthcheck_script(); + self.image_api.build( + &self.container_file_path, + &self.name, + &self.tag, + ); + } + + fn create(&mut self) { + let environment = TEST_ENVIRONMENT.lock().unwrap(); + let socket_location = self.socket_location(); + let socket_name = self.socket_name().to_string(); + let pulse_socket_location = socket_location.join("pulse"); + let mut create_options = CreateContainerOptionsBuilder::default(); + create_options + .with_image(format!("{}:{}", self.name, self.tag)) + .with_environment(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, socket_location.to_str().unwrap()) + .with_environment(PIPEWIRE_CORE_ENVIRONMENT_KEY, socket_name.clone()) + .with_environment(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, socket_name.clone()) + .with_environment(PULSE_RUNTIME_PATH_ENVIRONMENT_KEY, pulse_socket_location.to_str().unwrap()) + .with_environment("DISABLE_RTKIT", "y") + .with_environment("DISPLAY", ":0.0") + .with_volume("pipewire-sockets", socket_location.parent().unwrap().to_str().unwrap()) + .with_cpus(environment.container_cpu) + .with_memory_swap(environment.container_memory_swap) + .with_memory(environment.container_memory_swap); + drop(environment); + self.container = Some(self.container_api.create(&mut create_options)); + } + + fn start(&self) { + self.container_api.start(self.container.as_ref().unwrap()); + self.container_api.wait_healthy(self.container.as_ref().unwrap()); + } + + fn stop(&self) { + let mut options = StopContainerOptionsBuilder::default(); + self.container_api.stop(self.container.as_ref().unwrap(), &mut options); + } + + fn restart(&self) { + self.container_api.restart(self.container.as_ref().unwrap()) + } + + fn cleanup(&self) { + let docker_api = Environment::create_docker_sync_api(&Duration::from_millis(100)); + let stop_options = StopContainerOptionsBuilder::default().build(); + docker_api.stop(&self.container.as_ref().unwrap(), Some(stop_options)).unwrap(); + let remove_options = RemoveContainerOptions { + ..Default::default() + }; + docker_api.remove(&self.container.as_ref().unwrap(), Some(remove_options)).unwrap(); + } + + fn spawn(&mut self, command: &str) -> SpawnCommandBuilder<'_> { + let mut builder = SpawnCommandBuilder::new(&mut self.configuration); + builder.with_command(command) + .with_auto_start(true) + .with_auto_restart(true); + builder + } + + fn spawn_wait_loop(&mut self) { + self.entrypoint.push(vec![ + "supervisord".to_string(), + "-c".to_string(), + "/root/supervisor.conf".to_string(), + ]); + } + + fn create_folder(&mut self, path: &PathBuf) { + self.entrypoint.push(vec![ + "mkdir".to_string(), + "--parents".to_string(), + path.to_str().unwrap().to_string(), + ]); + } + + fn create_socket_folder(&mut self) { + self.entrypoint.push(vec![ + "mkdir".to_string(), + "--parents".to_string(), + format!("${{{}}}", PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY), + ]); + } + + fn remove_socket_folder(&mut self) { + self.entrypoint.push(vec![ + "rm".to_string(), + "--force".to_string(), + "--recursive".to_string(), + format!("${{{}}}", PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY), + ]); + } + + fn set_virtual_nodes_configuration(&mut self) { + self.entrypoint.push(vec![ + "mkdir".to_string(), + "--parents".to_string(), + "/etc/pipewire/pipewire.conf.d/".to_string(), + ]); + self.entrypoint.push(vec![ + "cp".to_string(), + "/root/virtual.nodes.conf".to_string(), + "/etc/pipewire/pipewire.conf.d/virtual.nodes.conf".to_string(), + ]); + } + + fn set_default_nodes(&mut self) { + self.post_start.push(vec![ + "echo".to_string(), + "'wait for test-sink'".to_string() + ]); + self.post_start.push(vec![ + "pactl".to_string(), + "set-default-sink".to_string(), + "'test-sink'".to_string(), + ]); + self.post_start.push(vec![ + "wpctl".to_string(), + "status".to_string(), + "|".to_string(), + "grep".to_string(), + "--quiet".to_string(), + "'test-sink'".to_string() + ]); + self.post_start.push(vec![ + "echo".to_string(), + "'wait for test-source'".to_string() + ]); + self.post_start.push(vec![ + "pactl".to_string(), + "set-default-source".to_string(), + "'test-source'".to_string(), + ]); + self.post_start.push(vec![ + "wpctl".to_string(), + "status".to_string(), + "|".to_string(), + "grep".to_string(), + "--quiet".to_string(), + "'test-source'".to_string() + ]); + } + + fn wait_for_pipewire(&mut self) { + self.healthcheck.push(vec![ + "echo".to_string(), + "'wait for pipewire'".to_string() + ]); + self.healthcheck.push(vec![ + "pw-cli".to_string(), + "ls".to_string(), + "0".to_string(), + "|".to_string(), + "grep".to_string(), + "--quiet".to_string(), + "'id 0, type PipeWire:Interface:Core/4'".to_string() + ]) + } + + fn wait_for_wireplumber(&mut self) { + self.healthcheck.push(vec![ + "echo".to_string(), + "'wait for wireplumbler'".to_string() + ]); + self.healthcheck.push(vec![ + "wpctl".to_string(), + "info".to_string(), + "|".to_string(), + "grep".to_string(), + "--quiet".to_string(), + "'WirePlumber'".to_string() + ]) + } + + fn wait_for_pulse(&mut self) { + self.healthcheck.push(vec![ + "echo".to_string(), + "'wait for PipeWire Pulse'".to_string() + ]); + self.healthcheck.push(vec![ + "pactl".to_string(), + "info".to_string(), + "|".to_string(), + "grep".to_string(), + "--quiet".to_string(), + "\"$PULSE_RUNTIME_PATH/native\"".to_string() + ]) + } + + fn generate_configuration_file(&self) { + let mut configuration = self.configuration.iter() + .map(|e| e.join("\n")) + .collect::>(); + configuration.insert(0, "[supervisord]".to_string()); + configuration.insert(1, "nodaemon=true".to_string()); + configuration.insert(2, "logfile=/var/log/supervisor/supervisord.log".to_string()); + let configuration = configuration.join("\n"); + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(CONTAINER_TMP_PATH.join("supervisor.conf")) + .unwrap(); + file.write(configuration.as_bytes()).unwrap(); + file.flush().unwrap(); + } + + fn generate_entrypoint_script(&self) { + let mut script = self.entrypoint.iter() + .map(|command| command.join(" ")) + .collect::>(); + script.insert(0, "#!/bin/bash".to_string()); + let script = script.join("\n"); + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(CONTAINER_TMP_PATH.join("entrypoint.bash")) + .unwrap(); + file.write(script.as_bytes()).unwrap(); + file.flush().unwrap(); + } + + fn generate_healthcheck_script(&self) { + let mut script = self.healthcheck.iter() + .map(|command| { + format!("({}) || exit 1", command.join(" ")) + }) + .collect::>(); + script.insert(0, "#!/bin/bash".to_string()); + script.insert(1, "set -e".to_string()); + let mut post_start_script = self.post_start.iter() + .map(|command| { + format!("({}) || exit 1", command.join(" ")) + }) + .collect::>(); + script.append(&mut post_start_script); + let script = script.join("\n"); + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(CONTAINER_TMP_PATH.join("healthcheck.bash")) + .unwrap(); + file.write(script.as_bytes()).unwrap(); + file.flush().unwrap(); + } +} + +impl Drop for ServerApi { + fn drop(&mut self) { + if self.container.is_none() { + return; + } + let mut stop_options = StopContainerOptionsBuilder::default(); + stop_options.with_wait(Duration::from_millis(0)); + self.container_api.stop(self.container.as_ref().unwrap(), &mut stop_options); + self.container_api.remove(self.container.as_ref().unwrap()); + } +} + +impl Clone for ServerApi { + fn clone(&self) -> Self { + Self { + name: self.name.clone(), + tag: self.tag.clone(), + socket_id: self.socket_id.clone(), + container_file_path: self.container_file_path.clone(), + image_api: self.image_api.clone(), + container_api: self.container_api.clone(), + container: self.container.clone(), + configuration: self.configuration.clone(), + entrypoint: self.entrypoint.clone(), + healthcheck: self.healthcheck.clone(), + post_start: self.post_start.clone(), + } + } +} + +pub struct ContainerizedServer { + api: ServerApi, + services: Vec, + pre_entrypoint: Option, + post_start: Option, +} + +impl ContainerizedServer { + pub(self) fn new( + name: String, + container_file_path: PathBuf, + services: Vec, + pre_entrypoint: Option, + post_start: Option, + ) -> Self { + Self { + api: ServerApi::new(name, container_file_path), + services, + pre_entrypoint, + post_start, + } + } + + pub fn build(&mut self) { + self.api.create_socket_folder(); + match &self.pre_entrypoint { + Some(value) => value.call(&mut self.api), + None => {} + } + for service in &mut self.services { + service.entrypoint.call(&mut self.api); + service.healthcheck.call(&mut self.api); + } + match &self.post_start { + Some(value) => value.call(&mut self.api), + None => {} + } + self.api.spawn_wait_loop(); + self.api.remove_socket_folder(); + self.api.build(); + } + + pub fn create(&mut self) { + self.api.create(); + } + + pub fn start(&mut self) { + self.api.start() + } + + pub fn stop(&mut self) { + self.api.stop() + } + + pub fn restart(&mut self) { + self.api.restart(); + } + + pub fn set_socket_env_vars(&self) { + std::env::set_var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, self.api.socket_location()); + std::env::set_var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, self.api.socket_name()); + } + + pub(self) fn cleanup(&self) { + self.api.cleanup(); + } +} + +impl Clone for ContainerizedServer { + fn clone(&self) -> Self { + Self { + api: self.api.clone(), + services: self.services.clone(), + pre_entrypoint: self.pre_entrypoint.clone(), + post_start: self.post_start.clone(), + } + } +} + +impl Drop for ContainerizedServer { + fn drop(&mut self) { + self.stop(); + } +} + +pub struct LocalServer {} + +pub enum Server { + Containerized(ContainerizedServer), + Local +} + +impl Server { + pub fn start(&mut self) { + match self { + Server::Containerized(value) => { + value.start(); + } + Server::Local => {} + } + } + + pub fn clone(&self) -> Self { + match self { + Server::Containerized(value) => Server::Containerized(value.clone()), + Server::Local => Server::Local + } + } + + pub fn cleanup(&self) { + match self { + Server::Containerized(value) => value.cleanup(), + Server::Local => {} + } + } +} + +#[fixture] +pub fn server_with_default_configuration() -> Arc { + let services = vec![ + PIPEWIRE_SERVICE.clone(), + WIREPLUMBER_SERVICE.clone(), + PULSE_SERVICE.clone(), + ]; + let mut server = ContainerizedServer::new( + "pipewire-default".to_string(), + CONTAINER_FILE_PATH.clone(), + services, + Some(LifeCycleCallback::from(|server: &mut ServerApi| { + server.set_virtual_nodes_configuration(); + })), + Some(LifeCycleCallback::from(|server: &mut ServerApi| { + server.set_default_nodes(); + })), + ); + let environment = TEST_ENVIRONMENT.lock().unwrap(); + let test_target = environment.test_target.clone(); + drop(environment); + match test_target { + TestTarget::Local => Arc::new(Server::Local), + TestTarget::Container => { + server.build(); + server.create(); + server.start(); + server.set_socket_env_vars(); + Arc::new(Server::Containerized(server)) + } + } +} + +#[fixture] +pub fn server_without_session_manager() -> Arc { + let services = vec![ + PIPEWIRE_SERVICE.clone(), + ]; + let mut server = ContainerizedServer::new( + "pipewire-without-session-manager".to_string(), + CONTAINER_FILE_PATH.clone(), + services, + None, + None, + ); + server.build(); + server.create(); + server.start(); + server.set_socket_env_vars(); + Arc::new(Server::Containerized(server)) +} + +#[fixture] +pub fn server_without_node() -> Arc { + let services = vec![ + PIPEWIRE_SERVICE.clone(), + WIREPLUMBER_SERVICE.clone(), + ]; + let mut server = ContainerizedServer::new( + "pipewire-without-node".to_string(), + CONTAINER_FILE_PATH.clone(), + services, + None, + None, + ); + server.build(); + server.create(); + server.start(); + server.set_socket_env_vars(); + Arc::new(Server::Containerized(server)) +} \ No newline at end of file From 5118f4a3005ca5085140a8b72c2b04e78f0efcf5 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Mon, 3 Mar 2025 18:01:48 +0000 Subject: [PATCH 16/17] Remove pipewire sub crates and use pipewire-client external crate --- Cargo.toml | 5 +- pipewire-client/Cargo.toml | 27 - pipewire-client/src/client/api/core.rs | 90 -- pipewire-client/src/client/api/core_test.rs | 28 - pipewire-client/src/client/api/internal.rs | 42 - pipewire-client/src/client/api/mod.rs | 20 - pipewire-client/src/client/api/node.rs | 147 --- pipewire-client/src/client/api/node_test.rs | 148 --- pipewire-client/src/client/api/stream.rs | 90 -- pipewire-client/src/client/api/stream_test.rs | 229 ----- pipewire-client/src/client/channel.rs | 338 ------- pipewire-client/src/client/channel_test.rs | 88 -- .../src/client/connection_string.rs | 37 - pipewire-client/src/client/handlers/event.rs | 249 ----- pipewire-client/src/client/handlers/mod.rs | 5 - .../src/client/handlers/registry.rs | 274 ------ .../src/client/handlers/request.rs | 820 ---------------- pipewire-client/src/client/handlers/thread.rs | 152 --- pipewire-client/src/client/implementation.rs | 205 ---- .../src/client/implementation_test.rs | 80 -- pipewire-client/src/client/mod.rs | 17 - pipewire-client/src/info.rs | 53 -- pipewire-client/src/lib.rs | 22 - pipewire-client/src/listeners.rs | 128 --- pipewire-client/src/messages.rs | 138 --- pipewire-client/src/states.rs | 873 ------------------ pipewire-client/src/test_utils/api.rs | 33 - pipewire-client/src/test_utils/fixtures.rs | 398 -------- pipewire-client/src/test_utils/mod.rs | 2 - pipewire-common/Cargo.toml | 13 - pipewire-common/src/constants.rs | 34 - pipewire-common/src/error.rs | 15 - pipewire-common/src/lib.rs | 4 - pipewire-common/src/macros.rs | 85 -- pipewire-common/src/utils.rs | 110 --- pipewire-spa-utils/Cargo.toml | 31 - pipewire-spa-utils/build.rs | 18 - .../build_modules/format/mod.rs | 445 --------- pipewire-spa-utils/build_modules/mod.rs | 3 - .../syntax/generators/enumerator.rs | 145 --- .../build_modules/syntax/generators/mod.rs | 1 - .../build_modules/syntax/mod.rs | 3 - .../build_modules/syntax/parsers/mod.rs | 59 -- .../build_modules/syntax/utils.rs | 67 -- pipewire-spa-utils/build_modules/utils/mod.rs | 104 --- pipewire-spa-utils/src/audio/mod.rs | 57 -- pipewire-spa-utils/src/audio/raw.rs | 63 -- pipewire-spa-utils/src/format/mod.rs | 12 - pipewire-spa-utils/src/lib.rs | 5 - pipewire-spa-utils/src/macros/mod.rs | 115 --- pipewire-spa-utils/src/utils/mod.rs | 255 ----- pipewire-test-utils/.containers/.digests | 3 - .../.containers/.tmp/entrypoint.bash | 6 - .../.containers/.tmp/healthcheck.bash | 14 - .../.containers/.tmp/supervisor.conf | 21 - .../.containers/pipewire.test.container | 48 - .../.containers/virtual.nodes.conf | 30 - pipewire-test-utils/Cargo.toml | 31 - .../src/containers/container.rs | 591 ------------ pipewire-test-utils/src/containers/mod.rs | 3 - pipewire-test-utils/src/containers/options.rs | 269 ------ .../src/containers/sync_api.rs | 414 --------- pipewire-test-utils/src/environment.rs | 231 ----- pipewire-test-utils/src/lib.rs | 17 - pipewire-test-utils/src/server.rs | 730 --------------- src/host/pipewire/host.rs | 13 +- 66 files changed, 14 insertions(+), 8789 deletions(-) delete mode 100644 pipewire-client/Cargo.toml delete mode 100644 pipewire-client/src/client/api/core.rs delete mode 100644 pipewire-client/src/client/api/core_test.rs delete mode 100644 pipewire-client/src/client/api/internal.rs delete mode 100644 pipewire-client/src/client/api/mod.rs delete mode 100644 pipewire-client/src/client/api/node.rs delete mode 100644 pipewire-client/src/client/api/node_test.rs delete mode 100644 pipewire-client/src/client/api/stream.rs delete mode 100644 pipewire-client/src/client/api/stream_test.rs delete mode 100644 pipewire-client/src/client/channel.rs delete mode 100644 pipewire-client/src/client/channel_test.rs delete mode 100644 pipewire-client/src/client/connection_string.rs delete mode 100644 pipewire-client/src/client/handlers/event.rs delete mode 100644 pipewire-client/src/client/handlers/mod.rs delete mode 100644 pipewire-client/src/client/handlers/registry.rs delete mode 100644 pipewire-client/src/client/handlers/request.rs delete mode 100644 pipewire-client/src/client/handlers/thread.rs delete mode 100644 pipewire-client/src/client/implementation.rs delete mode 100644 pipewire-client/src/client/implementation_test.rs delete mode 100644 pipewire-client/src/client/mod.rs delete mode 100644 pipewire-client/src/info.rs delete mode 100644 pipewire-client/src/lib.rs delete mode 100644 pipewire-client/src/listeners.rs delete mode 100644 pipewire-client/src/messages.rs delete mode 100644 pipewire-client/src/states.rs delete mode 100644 pipewire-client/src/test_utils/api.rs delete mode 100644 pipewire-client/src/test_utils/fixtures.rs delete mode 100644 pipewire-client/src/test_utils/mod.rs delete mode 100644 pipewire-common/Cargo.toml delete mode 100644 pipewire-common/src/constants.rs delete mode 100644 pipewire-common/src/error.rs delete mode 100644 pipewire-common/src/lib.rs delete mode 100644 pipewire-common/src/macros.rs delete mode 100644 pipewire-common/src/utils.rs delete mode 100644 pipewire-spa-utils/Cargo.toml delete mode 100644 pipewire-spa-utils/build.rs delete mode 100644 pipewire-spa-utils/build_modules/format/mod.rs delete mode 100644 pipewire-spa-utils/build_modules/mod.rs delete mode 100644 pipewire-spa-utils/build_modules/syntax/generators/enumerator.rs delete mode 100644 pipewire-spa-utils/build_modules/syntax/generators/mod.rs delete mode 100644 pipewire-spa-utils/build_modules/syntax/mod.rs delete mode 100644 pipewire-spa-utils/build_modules/syntax/parsers/mod.rs delete mode 100644 pipewire-spa-utils/build_modules/syntax/utils.rs delete mode 100644 pipewire-spa-utils/build_modules/utils/mod.rs delete mode 100644 pipewire-spa-utils/src/audio/mod.rs delete mode 100644 pipewire-spa-utils/src/audio/raw.rs delete mode 100644 pipewire-spa-utils/src/format/mod.rs delete mode 100644 pipewire-spa-utils/src/lib.rs delete mode 100644 pipewire-spa-utils/src/macros/mod.rs delete mode 100644 pipewire-spa-utils/src/utils/mod.rs delete mode 100644 pipewire-test-utils/.containers/.digests delete mode 100644 pipewire-test-utils/.containers/.tmp/entrypoint.bash delete mode 100644 pipewire-test-utils/.containers/.tmp/healthcheck.bash delete mode 100644 pipewire-test-utils/.containers/.tmp/supervisor.conf delete mode 100644 pipewire-test-utils/.containers/pipewire.test.container delete mode 100644 pipewire-test-utils/.containers/virtual.nodes.conf delete mode 100644 pipewire-test-utils/Cargo.toml delete mode 100644 pipewire-test-utils/src/containers/container.rs delete mode 100644 pipewire-test-utils/src/containers/mod.rs delete mode 100644 pipewire-test-utils/src/containers/options.rs delete mode 100644 pipewire-test-utils/src/containers/sync_api.rs delete mode 100644 pipewire-test-utils/src/environment.rs delete mode 100644 pipewire-test-utils/src/lib.rs delete mode 100644 pipewire-test-utils/src/server.rs diff --git a/Cargo.toml b/Cargo.toml index 4609e6b2f..622027431 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ rust-version = "1.70" asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions. oboe-shared-stdcxx = ["oboe/shared-stdcxx"] # Only available on Android. See README for what it does. jack = ["dep:jack"] -pipewire = ["dep:pipewire-client"] +pipewire = ["dep:pipewire-client", "dep:tokio"] [dependencies] dasp_sample = "0.11" @@ -48,7 +48,8 @@ num-traits = { version = "0.2.6", optional = true } alsa = "0.9" libc = "0.2" jack = { version = "0.13.0", optional = true } -pipewire-client = { version = "0.1", path = "pipewire-client", optional = true } +pipewire-client = { version = "0.1", git = "https://github.com/midoriiro/pipewire-client.git", optional = true } +tokio = { version = "1.43", features = ["full"], optional = true } # For PipeWire client, will change in the future. client will provide sync and async api. [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] core-foundation-sys = "0.8.2" # For linking to CoreFoundation.framework and handling device name `CFString`s. diff --git a/pipewire-client/Cargo.toml b/pipewire-client/Cargo.toml deleted file mode 100644 index 1fe5b4535..000000000 --- a/pipewire-client/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "pipewire-client" -version = "0.1.0" -edition = "2021" -authors = ["Alexis Bekhdadi "] -description = "PipeWire Client" -repository = "https://github.com/RustAudio/cpal/" -documentation = "" -license = "Apache-2.0" -keywords = ["pipewire", "client"] - -[dependencies] -pipewire = { version = "0.8" } -pipewire-spa-utils = { version = "0.1", path = "../pipewire-spa-utils"} -pipewire-common = { version = "0.1", path = "../pipewire-common" } -serde_json = "1.0" -crossbeam-channel = "0.5" -uuid = { version = "1.12", features = ["v4"] } -tokio = { version = "1", features = ["full"] } -tokio-util = "0.7" -libc = "0.2" - -[dev-dependencies] -rstest = "0.24" -serial_test = "3.2" -ctor = "0.2" -pipewire-test-utils = { version = "0.1", path = "../pipewire-test-utils" } \ No newline at end of file diff --git a/pipewire-client/src/client/api/core.rs b/pipewire-client/src/client/api/core.rs deleted file mode 100644 index 1970c5a71..000000000 --- a/pipewire-client/src/client/api/core.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::client::api::internal::InternalApi; -use crate::error::Error; -use crate::messages::{MessageRequest, MessageResponse}; -use crate::states::{DefaultAudioNodesState, GlobalObjectState, SettingsState}; -use std::sync::Arc; - -pub struct CoreApi { - pub(crate) api: Arc, -} - -impl CoreApi { - pub(crate) fn new(api: Arc) -> Self { - CoreApi { - api, - } - } - - pub(crate) fn check_session_manager_registered(&self) -> Result<(), Error> { - let request = MessageRequest::CheckSessionManagerRegistered; - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::CheckSessionManagerRegistered{ - session_manager_registered, - error - }) => { - if session_manager_registered { - return Ok(()); - } - Err(error.unwrap()) - }, - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn quit(&self) { - let request = MessageRequest::Quit; - self.api.send_request_without_response(&request).unwrap(); - } - - pub fn get_settings(&self) -> Result { - let request = MessageRequest::Settings; - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::Settings(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub(crate) fn get_settings_state(&self) -> Result { - let request = MessageRequest::SettingsState; - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::SettingsState(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn get_default_audio_nodes(&self) -> Result { - let request = MessageRequest::DefaultAudioNodes; - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::DefaultAudioNodes(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub(crate) fn get_default_audio_nodes_state(&self) -> Result { - let request = MessageRequest::DefaultAudioNodesState; - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::DefaultAudioNodesState(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } -} \ No newline at end of file diff --git a/pipewire-client/src/client/api/core_test.rs b/pipewire-client/src/client/api/core_test.rs deleted file mode 100644 index 4a1e47ebd..000000000 --- a/pipewire-client/src/client/api/core_test.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::test_utils::fixtures::{isolated_client, shared_client, PipewireTestClient}; -use rstest::rstest; -use serial_test::serial; - -#[rstest] -#[serial] -fn quit(#[from(isolated_client)] client: PipewireTestClient) { - client.core().quit(); -} - -#[rstest] -#[serial] -pub fn settings(#[from(shared_client)] client: PipewireTestClient) { - let settings = client.core().get_settings().unwrap(); - assert_eq!(true, settings.sample_rate > u32::default()); - assert_eq!(true, settings.default_buffer_size > u32::default()); - assert_eq!(true, settings.min_buffer_size > u32::default()); - assert_eq!(true, settings.max_buffer_size > u32::default()); - assert_eq!(true, settings.allowed_sample_rates[0] > u32::default()); -} - -#[rstest] -#[serial] -pub fn default_audio_nodes(#[from(shared_client)] client: PipewireTestClient) { - let default_audio_nodes = client.core().get_default_audio_nodes().unwrap(); - assert_eq!(false, default_audio_nodes.sink.is_empty()); - assert_eq!(false, default_audio_nodes.source.is_empty()); -} \ No newline at end of file diff --git a/pipewire-client/src/client/api/internal.rs b/pipewire-client/src/client/api/internal.rs deleted file mode 100644 index 79808edab..000000000 --- a/pipewire-client/src/client/api/internal.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::client::channel::ClientChannel; -use crate::error::Error; -use crate::messages::{MessageRequest, MessageResponse}; -use std::time::Duration; - -pub(crate) struct InternalApi { - pub(crate) channel: ClientChannel, - pub(crate) timeout: Duration -} - -impl InternalApi { - pub(crate) fn new( - channel: ClientChannel, - timeout: Duration - ) -> Self { - InternalApi { - channel, - timeout, - } - } - - pub(crate) fn wait_response_with_timeout(&self, timeout: Duration) -> Result { - self.channel.receive_timeout(timeout) - } - - pub(crate) fn send_request(&self, request: &MessageRequest) -> Result { - let response = self.channel.send(request.clone()); - match response { - Ok(value) => { - match value { - MessageResponse::Error(value) => Err(value), - _ => Ok(value) - } - } - Err(value) => Err(value) - } - } - - pub(crate) fn send_request_without_response(&self, request: &MessageRequest) -> Result<(), Error> { - self.channel.fire(request.clone()).map(move |_| ()) - } -} \ No newline at end of file diff --git a/pipewire-client/src/client/api/mod.rs b/pipewire-client/src/client/api/mod.rs deleted file mode 100644 index ecc0268a5..000000000 --- a/pipewire-client/src/client/api/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod core; -pub(crate) use core::CoreApi; -#[cfg(test)] -#[path = "core_test.rs"] -mod core_test; - -mod node; -pub(crate) use node::NodeApi; -#[cfg(test)] -#[path = "node_test.rs"] -mod node_test; - -mod stream; -pub(crate) use stream::StreamApi; -#[cfg(test)] -#[path = "stream_test.rs"] -mod stream_test; - -mod internal; -pub(crate) use internal::InternalApi; diff --git a/pipewire-client/src/client/api/node.rs b/pipewire-client/src/client/api/node.rs deleted file mode 100644 index 8f3415f28..000000000 --- a/pipewire-client/src/client/api/node.rs +++ /dev/null @@ -1,147 +0,0 @@ -use crate::client::api::internal::InternalApi; -use crate::error::Error; -use crate::messages::{MessageRequest, MessageResponse}; -use crate::states::{GlobalId, GlobalObjectState}; -use crate::utils::Backoff; -use crate::{Direction, NodeInfo}; -use std::sync::Arc; - -pub struct NodeApi { - api: Arc -} - -impl NodeApi { - pub(crate) fn new(api: Arc) -> Self { - NodeApi { - api, - } - } - - pub(crate) fn state( - &self, - id: &GlobalId, - ) -> Result { - let request = MessageRequest::NodeState(id.clone()); - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::NodeState(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub(crate) fn states( - &self, - ) -> Result, Error> { - let request = MessageRequest::NodeStates; - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::NodeStates(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub(crate) fn count( - &self, - ) -> Result { - let request = MessageRequest::NodeCount; - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::NodeCount(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn get( - &self, - name: String, - direction: Direction, - ) -> Result { - let request = MessageRequest::GetNode { - name, - direction, - }; - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::GetNode(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn create( - &self, - name: String, - description: String, - nickname: String, - direction: Direction, - channels: u16, - ) -> Result<(), Error> { - let request = MessageRequest::CreateNode { - name, - description, - nickname, - direction, - channels, - }; - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::CreateNode(id)) => { - let operation = move || { - let state = self.state(&id)?; - return if state == GlobalObjectState::Initialized { - Ok(()) - } else { - Err(Error { - description: "Created node not yet initialized".to_string(), - }) - } - }; - let mut backoff = Backoff::constant(self.api.timeout.as_millis()); - backoff.retry(operation) - }, - Ok(MessageResponse::Error(value)) => Err(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn delete(&self, id: u32) -> Result<(), Error> { - let request = MessageRequest::DeleteNode(GlobalId::from(id)); - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::DeleteNode) => Ok(()), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn enumerate( - &self, - direction: Direction, - ) -> Result, Error> { - let request = MessageRequest::EnumerateNodes(direction); - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::EnumerateNodes(value)) => Ok(value), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } -} \ No newline at end of file diff --git a/pipewire-client/src/client/api/node_test.rs b/pipewire-client/src/client/api/node_test.rs deleted file mode 100644 index ac5bb9f8f..000000000 --- a/pipewire-client/src/client/api/node_test.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::states::NodeState; -use crate::test_utils::fixtures::{shared_client, PipewireTestClient}; -use crate::Direction; -use rstest::rstest; -use serial_test::serial; -use std::any::TypeId; -use uuid::Uuid; - -fn internal_enumerate(client: &PipewireTestClient, direction: Direction) -> Vec { - let nodes = client.node().enumerate(direction).unwrap(); - assert_eq!(false, nodes.is_empty()); - let default_node = nodes.iter() - .filter(|node| node.is_default) - .last(); - assert_eq!(true, default_node.is_some()); - let listeners = client.core().get_listeners().unwrap(); - let node_listeners = listeners.get(&TypeId::of::()).unwrap(); - for (_, listeners) in node_listeners { - assert_eq!(0, listeners.len()); - } - nodes.iter() - .map(move |node| node.name.clone()) - .collect() -} - -fn internal_create(client: &PipewireTestClient, direction: Direction) -> String { - let node_name = Uuid::new_v4().to_string(); - client.node() - .create( - node_name.clone(), - node_name.clone(), - node_name.clone(), - direction, - 2 - ).unwrap(); - let listeners = client.core().get_listeners().unwrap(); - let node_listeners = listeners.get(&TypeId::of::()).unwrap(); - for (_, listeners) in node_listeners { - assert_eq!(0, listeners.len()); - } - node_name -} - -#[rstest] -#[serial] -fn enumerate_input( - #[from(shared_client)] client: PipewireTestClient, -) { - internal_enumerate(&client, Direction::Input); -} - -#[rstest] -#[serial] -fn enumerate_output( - #[from(shared_client)] client: PipewireTestClient, -) { - internal_enumerate(&client, Direction::Output); -} - -#[rstest] -#[serial] -fn create_input( - #[from(shared_client)] client: PipewireTestClient, -) { - internal_create(&client, Direction::Input); -} - -#[rstest] -#[serial] -fn create_output( - #[from(shared_client)] client: PipewireTestClient, -) { - internal_create(&client, Direction::Output); -} - -#[rstest] -#[serial] -fn create_twice_same_direction( - #[from(shared_client)] client: PipewireTestClient, -) { - let node_name = Uuid::new_v4().to_string(); - client.node() - .create( - node_name.clone(), - node_name.clone(), - node_name.clone(), - Direction::Output, - 2 - ).unwrap(); - let error = client.node() - .create( - node_name.clone(), - node_name.clone(), - node_name.clone(), - Direction::Output, - 2 - ).unwrap_err(); - assert_eq!( - format!("Node with name({}) already exists", node_name), - error.description - ) -} - -#[rstest] -#[serial] -fn create_twice_different_direction( - #[from(shared_client)] client: PipewireTestClient, -) { - let node_name = Uuid::new_v4().to_string(); - client.node() - .create( - node_name.clone(), - node_name.clone(), - node_name.clone(), - Direction::Input, - 2 - ).unwrap(); - client.node() - .create( - node_name.clone(), - node_name.clone(), - node_name.clone(), - Direction::Output, - 2 - ).unwrap(); -} - -#[rstest] -#[serial] -fn create_then_enumerate_input( - #[from(shared_client)] client: PipewireTestClient, -) { - let direction = Direction::Input; - let node = internal_create(&client, direction.clone()); - let nodes = internal_enumerate(&client, direction.clone()); - assert_eq!(true, nodes.contains(&node)) -} - -#[rstest] -#[serial] -fn create_then_enumerate_output( - #[from(shared_client)] client: PipewireTestClient, -) { - let direction = Direction::Output; - let node = internal_create(&client, direction.clone()); - let nodes = internal_enumerate(&client, direction.clone()); - assert_eq!(true, nodes.contains(&node)) -} \ No newline at end of file diff --git a/pipewire-client/src/client/api/stream.rs b/pipewire-client/src/client/api/stream.rs deleted file mode 100644 index ae6ad9cbd..000000000 --- a/pipewire-client/src/client/api/stream.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::client::api::internal::InternalApi; -use crate::error::Error; -use crate::listeners::ListenerControlFlow; -use crate::messages::{MessageRequest, MessageResponse, StreamCallback}; -use crate::states::GlobalId; -use crate::{AudioStreamInfo, Direction}; -use std::sync::Arc; - -pub struct StreamApi { - api: Arc, -} - -impl StreamApi { - pub(crate) fn new(api: Arc) -> Self { - StreamApi { - api, - } - } - - pub fn create( - &self, - node_id: u32, - direction: Direction, - format: AudioStreamInfo, - callback: F, - ) -> Result - where - F: FnMut(&mut ListenerControlFlow, pipewire::buffer::Buffer) + Send + 'static - { - let request = MessageRequest::CreateStream { - node_id: GlobalId::from(node_id), - direction, - format, - callback: StreamCallback::from(callback), - }; - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::CreateStream(name)) => Ok(name), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn delete( - &self, - name: String - ) -> Result<(), Error> { - let request = MessageRequest::DeleteStream(name); - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::DeleteStream) => Ok(()), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value) - }), - } - } - - pub fn connect( - &self, - name: String - ) -> Result<(), Error> { - let request = MessageRequest::ConnectStream(name); - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::ConnectStream) => Ok(()), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } - - pub fn disconnect( - &self, - name: String - ) -> Result<(), Error> { - let request = MessageRequest::DisconnectStream(name); - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::DisconnectStream) => Ok(()), - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } -} \ No newline at end of file diff --git a/pipewire-client/src/client/api/stream_test.rs b/pipewire-client/src/client/api/stream_test.rs deleted file mode 100644 index 79c2a3dc1..000000000 --- a/pipewire-client/src/client/api/stream_test.rs +++ /dev/null @@ -1,229 +0,0 @@ -use crate::listeners::ListenerControlFlow; -use crate::states::StreamState; -use crate::test_utils::fixtures::{input_connected_stream, input_node, input_stream, output_connected_stream, output_node, output_stream, shared_client, ConnectedStreamFixture, NodeInfoFixture, PipewireTestClient, StreamFixture}; -use crate::{Direction, PipewireClient}; -use rstest::rstest; -use serial_test::serial; -use std::any::TypeId; -use std::fmt::{Display, Formatter}; -use std::ops::Deref; -use crate::client::api::StreamApi; -use crate::client::CoreApi; - -fn assert_listeners(client: &CoreApi, stream_name: &String, expected_listener: u32) { - let listeners = client.get_listeners().unwrap(); - let stream_listeners = listeners.get(&TypeId::of::()).unwrap().iter() - .find_map(move |(key, listeners)| { - if key == stream_name { - Some(listeners) - } - else { - None - } - }) - .unwrap(); - assert_eq!(expected_listener as usize, stream_listeners.len()); -} - -fn internal_create( - client: &StreamApi, - node: &NodeInfoFixture, - direction: Direction, - callback: F, -) -> String -where - F: FnMut(&mut ListenerControlFlow, pipewire::buffer::Buffer) + Send + 'static -{ - let stream_name = client - .create( - node.id, - direction, - node.format.clone().into(), - callback - ) - .unwrap(); - stream_name -} - -fn abstract_create( - client: &PipewireClient, - node: &NodeInfoFixture, - direction: Direction -) { - let stream = internal_create( - &client.stream(), - node, - direction.clone(), - move |control_flow, _| { - assert!(true); - control_flow.release(); - } - ); - match direction { - Direction::Input => assert_eq!(true, stream.ends_with(".stream_input")), - Direction::Output => assert_eq!(true, stream.ends_with(".stream_output")) - }; - assert_listeners(client.core(), &stream, 1); -} - -#[rstest] -#[serial] -fn create_input( - #[from(input_node)] node: NodeInfoFixture -) { - let direction = Direction::Input; - abstract_create(&node.client(), &node, direction); -} - -#[rstest] -#[serial] -fn create_output( - #[from(output_node)] node: NodeInfoFixture -) { - let direction = Direction::Output; - abstract_create(&node.client(), &node, direction); -} - -#[rstest] -#[serial] -fn create_twice( - #[from(output_node)] node: NodeInfoFixture -) { - let direction = Direction::Output; - let stream = node.client().stream() - .create( - node.id, - direction.clone(), - node.format.clone().into(), - move |_, _| {} - ) - .unwrap(); - let error = node.client().stream() - .create( - node.id, - direction.clone(), - node.format.clone().into(), - move |_, _| {} - ) - .unwrap_err(); - assert_eq!( - format!("Stream with name({}) already exists", stream), - error.description - ); - assert_listeners(node.client().core(), &stream, 1); -} - -#[rstest] -#[serial] -fn delete_input( - #[from(input_stream)] stream: StreamFixture -) { - stream.delete().unwrap(); -} - -#[rstest] -#[serial] -fn delete_output( - #[from(output_stream)] stream: StreamFixture -) { - stream.delete().unwrap(); -} - -#[rstest] -#[serial] -fn delete_when_not_exists( - #[from(shared_client)] client: PipewireTestClient, -) { - let stream = "not_existing_stream".to_string(); - let error = client.stream().delete(stream.clone()).unwrap_err(); - assert_eq!( - format!("Stream with name({}) not found", stream), - error.description - ) -} - -#[rstest] -#[serial] -fn delete_twice( - #[from(output_stream)] stream: StreamFixture -) { - stream.delete().unwrap(); - let error = stream.delete().unwrap_err(); - assert_eq!( - format!("Stream with name({}) not found", stream), - error.description - ) -} - -#[rstest] -#[serial] -fn connect_input( - #[from(input_stream)] stream: StreamFixture -) { - stream.connect().unwrap(); - assert_listeners(stream.client().core(), &stream, 1); -} - -#[rstest] -#[serial] -fn connect_output( - #[from(output_stream)] stream: StreamFixture -) { - stream.connect().unwrap(); - assert_listeners(stream.client().core(), &stream, 1); -} - -#[rstest] -#[serial] -fn connect_twice( - #[from(output_connected_stream)] stream: ConnectedStreamFixture -) { - let error = stream.connect().unwrap_err(); - assert_eq!( - format!("Stream {} is already connected", stream), - error.description - ) -} - -#[rstest] -#[serial] -fn disconnect_input( - #[from(input_connected_stream)] stream: ConnectedStreamFixture -) { - stream.disconnect().unwrap(); - assert_listeners(stream.client().core(), &stream, 1); -} - -#[rstest] -#[serial] -fn disconnect_output( - #[from(output_connected_stream)] stream: ConnectedStreamFixture -) { - stream.disconnect().unwrap(); - assert_listeners(stream.client().core(), &stream, 1); -} - -#[rstest] -#[serial] -fn disconnect_when_not_connected( - #[from(output_stream)] stream: StreamFixture -) { - let error = stream.disconnect().unwrap_err(); - assert_eq!( - format!("Stream {} is not connected", stream), - error.description - ) -} - -#[rstest] -#[serial] -fn disconnect_twice( - #[from(output_connected_stream)] stream: ConnectedStreamFixture -) { - stream.disconnect().unwrap(); - let error = stream.disconnect().unwrap_err(); - assert_eq!( - format!("Stream {} is not connected", stream), - error.description - ) -} \ No newline at end of file diff --git a/pipewire-client/src/client/channel.rs b/pipewire-client/src/client/channel.rs deleted file mode 100644 index 6bbcfbd9c..000000000 --- a/pipewire-client/src/client/channel.rs +++ /dev/null @@ -1,338 +0,0 @@ -use ControlFlow::Break; -use crate::error::Error; -use crossbeam_channel::{unbounded, SendError, TryRecvError}; -use std::collections::HashMap; -use std::fmt::Debug; -use std::ops::ControlFlow; -use std::sync::{Arc, Mutex}; -use std::time::Duration; -use tokio::runtime::Runtime; -use tokio::select; -use tokio::time::Instant; -use tokio_util::sync::CancellationToken; -use uuid::Uuid; - -pub(crate) struct Request { - pub id: Uuid, - pub message: T, -} - -impl Request { - pub(super) fn new(message: T) -> Self { - Self { - id: Uuid::new_v4(), - message, - } - } -} - -impl From for Request { - fn from(value: T) -> Self { - Self::new(value) - } -} - -pub(crate) struct Response { - pub id: Uuid, - pub message: T, -} - -impl Response { - pub fn from(request: &Request, message: T) -> Self { - Self { - id: request.id.clone(), - message, - } - } -} - -type PendingMessages = Arc>>>; -type GlobalMessages = Arc>>>; - -const GLOBAL_MESSAGE_ID: Uuid = Uuid::nil(); - -pub(crate) struct ClientChannel { - sender: pipewire::channel::Sender>, - receiver: crossbeam_channel::Receiver>, - pub(super) global_messages: GlobalMessages, - pub(super) pending_messages: PendingMessages, - runtime: Arc -} - -impl ClientChannel { - pub(self) fn new( - sender: pipewire::channel::Sender>, - receiver: crossbeam_channel::Receiver>, - runtime: Arc - ) -> Self { - Self { - sender, - receiver, - global_messages: Arc::new(Mutex::new(Vec::new())), - pending_messages: Arc::new(Mutex::new(HashMap::new())), - runtime, - } - } - - pub fn fire(&self, request: Q) -> Result { - let id = Uuid::new_v4(); - let request = Request { - id: id.clone(), - message: request, - }; - let response = self.sender.send(request); - match response { - Ok(_) => Ok(id), - Err(value) => Err(Error { - description: format!("Failed to send request: {:?}", value.message), - }), - } - } - - pub fn send(&self, request: Q) -> Result { - let id = Uuid::new_v4(); - let request = Request { - id: id.clone(), - message: request, - }; - let response = self.sender.send(request); - let response = match response { - Ok(_) => self.receive( - id, - self.global_messages.clone(), - self.pending_messages.clone(), - self.receiver.clone(), - CancellationToken::default() - ), - Err(value) => return Err(Error { - description: format!("Failed to send request: {:?}", value.message), - }), - }; - match response { - Ok(value) => Ok(value.message), - Err(value) => Err(Error { - description: format!( - "Failed to execute request ({:?})", value - ), - }), - } - } - - pub fn send_timeout(&self, request: Q, timeout: Duration) -> Result { - let request_id = match self.fire(request) { - Ok(value) => value, - Err(value) => return Err(value) - }; - self.internal_receive_timeout(request_id, timeout) - } - - pub fn receive_timeout(&self, timeout: Duration) -> Result { - self.internal_receive_timeout(GLOBAL_MESSAGE_ID, timeout) - } - - fn internal_receive_timeout(&self, id: Uuid, timeout: Duration) -> Result { - let global_messages = self.global_messages.clone(); - let pending_messages = self.pending_messages.clone(); - let receiver = self.receiver.clone(); - let handle = self.runtime.spawn(async move { - let start_time = Instant::now(); - loop { - let control_flow = Self::internal_receive( - id, - global_messages.clone(), - pending_messages.clone(), - receiver.clone() - ); - match control_flow.await { - Break(value) => { - return match value { - Ok(value ) => Ok(value.message), - Err(value) => Err(value) - }; - }, - _ => { - let now_time = Instant::now(); - let delta_time = now_time - start_time; - if delta_time >= timeout { - return Err(Error { - description: "Timeout".to_string(), - }); - } - continue - }, - } - } - }); - self.runtime.block_on(handle).unwrap() - } - - fn receive( - &self, - id: Uuid, - global_messages: GlobalMessages, - pending_messages: PendingMessages, - receiver: crossbeam_channel::Receiver>, - cancellation_token: CancellationToken - ) -> Result, Error> { - let handle = self.runtime.spawn(async move { - loop { - select! { - _ = cancellation_token.cancelled() => (), - control_flow = Self::internal_receive( - id, - global_messages.clone(), - pending_messages.clone(), - receiver.clone() - ) => { - match control_flow { - Break(value) => return value, - _ => continue, - } - } - } - } - }); - self.runtime.block_on(handle).unwrap() - } - - async fn internal_receive( - id: Uuid, - global_messages: GlobalMessages, - pending_messages: PendingMessages, - receiver: crossbeam_channel::Receiver>, - ) -> ControlFlow, Error>, ()> - { - let response = receiver.try_recv(); - match response { - Ok(value) => { - // When request id is equal Uuid::nil, - // message is sent when unrecoverable error occurred outside of request context. - // But it's not necessary an error message, initialized message is global too. - // - // Those errors are sent in event handler, registry - // and during server thread init phase. - // Might a good idea to find a better solution, because any request could fail - // but not because request is malformed or request result cannot be computed, but - // because somewhere else something bad happen. - // - // Maybe putting error messages into a vec which will be regularly watched by an async - // periodic task ? But that involve to create a thread/task in tokio and that task will - // live during client lifetime (maybe for a long time). - // - // Maybe add a separate channel but same issue occur here. An async periodic task will - // watch if any error message spawned - // - // For now, solution is simple: - // 1.1: Requested id is equal to response id - // in that case we break the loop because that's the requested id - // 1.2: Requested id is not equal to response id but to global id - // we store that response to further process - if value.id == id { - Break(Ok(value)) - } - else if value.id == GLOBAL_MESSAGE_ID { - global_messages.lock().unwrap().push(value); - ControlFlow::Continue(()) - } - else { - pending_messages.lock().unwrap().insert(value.id.clone(), value); - ControlFlow::Continue(()) - } - } - Err(value) => { - match value { - TryRecvError::Empty => { - match pending_messages.lock().unwrap().remove(&id) { - Some(value) => Break(Ok(value)), - None => ControlFlow::Continue(()), - } - } - TryRecvError::Disconnected => { - Break(Err(Error { - description: "Channel disconnected".to_string(), - })) - } - } - } - } - } -} - -impl Clone for ClientChannel { - fn clone(&self) -> Self { - Self { - sender: self.sender.clone(), - receiver: self.receiver.clone(), - global_messages: self.global_messages.clone(), - pending_messages: self.pending_messages.clone(), - runtime: self.runtime.clone(), - } - } -} - -pub(crate) struct ServerChannel { - sender: crossbeam_channel::Sender>, - receiver: Option>> -} - -impl ServerChannel { - pub(self) fn new( - sender: crossbeam_channel::Sender>, - receiver: pipewire::channel::Receiver>, - ) -> Self { - Self { - sender, - receiver: Some(receiver), - } - } - - pub fn attach<'a, F>(&mut self, loop_: &'a pipewire::loop_::LoopRef, callback: F) -> pipewire::channel::AttachedReceiver<'a, Request> - where - F: Fn(Request) + 'static, - { - let receiver = self.receiver.take().unwrap(); - let attached_receiver = receiver.attach(loop_, callback); - attached_receiver - } - - pub fn fire(&self, response: R) -> Result<(), SendError>> { - let response = Response { - id: GLOBAL_MESSAGE_ID, - message: response, - }; - self.sender.send(response) - } - - pub fn send(&self, request: &Request, response: R) -> Result<(), SendError>> { - let response = Response::from(request, response); - self.sender.send(response) - } -} - -impl Clone for ServerChannel { - fn clone(&self) -> Self { - Self { - sender: self.sender.clone(), - receiver: None // pipewire receiver cannot be cloned - } - } -} - -pub(crate) fn channels(runtime: Arc) -> (ClientChannel, ServerChannel) -where - Q: Debug + Send, - R: Send + 'static -{ - let (pw_sender, pw_receiver) = pipewire::channel::channel(); - let (main_sender, main_receiver) = unbounded(); - let client_channel = ClientChannel::::new( - pw_sender, - main_receiver, - runtime - ); - let server_channel = ServerChannel::::new( - main_sender, - pw_receiver - ); - (client_channel, server_channel) -} \ No newline at end of file diff --git a/pipewire-client/src/client/channel_test.rs b/pipewire-client/src/client/channel_test.rs deleted file mode 100644 index 634686897..000000000 --- a/pipewire-client/src/client/channel_test.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::client::channel::{channels, Request}; -use rstest::rstest; -use std::thread; -use std::time::Duration; -use pipewire_test_utils::environment::TEST_ENVIRONMENT; - -#[derive(Debug, Clone, Copy)] -enum MessageRequest { - Quit, - Test1, - Test2 -} - -#[derive(Debug, PartialEq)] -enum MessageResponse { - Test1, - Test2, - GlobalMessage -} - -#[rstest] -fn request_context() { - let (client_channel, mut server_channel) = channels(TEST_ENVIRONMENT.lock().unwrap().runtime.clone()); - let handle_main = thread::spawn(move || { - let sender = server_channel.clone(); - let main_loop = pipewire::main_loop::MainLoop::new(None).unwrap(); - let attached_main_loop = main_loop.clone(); - let _attached_channel = server_channel.attach( - main_loop.loop_(), - move |message| { - let request = message.message; - match request { - MessageRequest::Test1 => { - sender - .send(&message, MessageResponse::Test1) - .unwrap() - } - MessageRequest::Test2 => { - sender - .send(&message, MessageResponse::Test2) - .unwrap() - } - _ => attached_main_loop.quit() - } - } - ); - main_loop.run(); - }); - let request_1 = MessageRequest::Test1; - let request_2 = MessageRequest::Test2; - let response = client_channel.send(request_1).unwrap(); - assert_eq!(MessageResponse::Test1, response); - let response = client_channel.send(request_2).unwrap(); - assert_eq!(MessageResponse::Test2, response); - client_channel.fire(MessageRequest::Quit).unwrap(); - assert_eq!(0, client_channel.global_messages.lock().unwrap().len()); - assert_eq!(0, client_channel.pending_messages.lock().unwrap().len()); - handle_main.join().unwrap(); -} - -#[rstest] -fn global_message() { - let (client_channel, server_channel) = channels::(TEST_ENVIRONMENT.lock().unwrap().runtime.clone()); - server_channel.fire(MessageResponse::GlobalMessage).unwrap(); - client_channel - .send_timeout( - MessageRequest::Test2, - Duration::from_millis(200), - ) - .unwrap_err(); - assert_eq!(1, client_channel.global_messages.lock().unwrap().len()); - assert_eq!(0, client_channel.pending_messages.lock().unwrap().len()); -} - -#[rstest] -fn pending_message() { - let (client_channel, server_channel) = channels::(TEST_ENVIRONMENT.lock().unwrap().runtime.clone()); - let request = Request::new(MessageRequest::Test1); - server_channel.send(&request ,MessageResponse::Test1).unwrap(); - client_channel - .send_timeout( - MessageRequest::Test2, - Duration::from_millis(200), - ) - .unwrap_err(); - assert_eq!(0, client_channel.global_messages.lock().unwrap().len()); - assert_eq!(1, client_channel.pending_messages.lock().unwrap().len()); -} diff --git a/pipewire-client/src/client/connection_string.rs b/pipewire-client/src/client/connection_string.rs deleted file mode 100644 index 5862498f6..000000000 --- a/pipewire-client/src/client/connection_string.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::constants::*; -use std::path::PathBuf; - -pub(super) struct PipewireClientSocketPath; - -impl PipewireClientSocketPath { - pub(super) fn from_env() -> PathBuf { - let pipewire_runtime_dir = std::env::var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY); - let xdg_runtime_dir = std::env::var(XDG_RUNTIME_DIR_ENVIRONMENT_KEY); - - let socket_directory = match (xdg_runtime_dir, pipewire_runtime_dir) { - (Ok(value), Ok(_)) => value, - (Ok(value), Err(_)) => value, - (Err(_), Ok(value)) => value, - (Err(_), Err(_)) => panic!( - "${} or ${} should be set. See https://docs.pipewire.org/page_man_pipewire_1.html", - PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, XDG_RUNTIME_DIR_ENVIRONMENT_KEY - ), - }; - - let pipewire_remote = match std::env::var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY) { - Ok(value) => value, - Err(_) => panic!( - "${PIPEWIRE_REMOTE_ENVIRONMENT_KEY} should be set. See https://docs.pipewire.org/page_man_pipewire_1.html", - ) - }; - - let socket_path = PathBuf::from(socket_directory).join(pipewire_remote); - socket_path - } -} - -pub(super) struct PipewireClientInfo { - pub name: String, - pub socket_location: String, - pub socket_name: String, -} \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/event.rs b/pipewire-client/src/client/handlers/event.rs deleted file mode 100644 index 8501f3caa..000000000 --- a/pipewire-client/src/client/handlers/event.rs +++ /dev/null @@ -1,249 +0,0 @@ -use crate::constants::{METADATA_NAME_PROPERTY_VALUE_DEFAULT, METADATA_NAME_PROPERTY_VALUE_SETTINGS}; -use crate::error::Error; -use crate::messages::{EventMessage, MessageRequest, MessageResponse}; -use crate::states::{DefaultAudioNodesState, GlobalId, GlobalState, SettingsState}; -use pipewire_spa_utils::audio::raw::AudioInfoRaw; -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; -use std::sync::{Arc, Mutex}; -use crate::client::channel::ServerChannel; - -pub(super) fn event_handler( - state: Arc>, - server_channel: ServerChannel, - event_sender: pipewire::channel::Sender, -) -> impl Fn(EventMessage) + 'static -{ - move |event_message: EventMessage| match event_message { - EventMessage::SetMetadataListeners { id } => handle_set_metadata_listeners( - id, - state.clone(), - server_channel.clone(), - ), - EventMessage::RemoveNode { id } => handle_remove_node( - id, - state.clone(), - server_channel.clone() - ), - EventMessage::SetNodePropertiesListener { id } => handle_set_node_properties_listener( - id, - state.clone(), - server_channel.clone(), - event_sender.clone() - ), - EventMessage::SetNodeFormatListener { id } => handle_set_node_format_listener( - id, - state.clone(), - server_channel.clone(), - event_sender.clone() - ), - EventMessage::SetNodeProperties { - id, - properties - } => handle_set_node_properties( - id, - properties, - state.clone(), - server_channel.clone() - ), - EventMessage::SetNodeFormat { id, format } => handle_set_node_format( - id, - format, - state.clone(), - server_channel.clone() - ), - } -} - -fn handle_set_metadata_listeners( - id: GlobalId, - state: Arc>, - server_channel: ServerChannel, -) -{ - let listener_state = state.clone(); - let mut state = state.lock().unwrap(); - let metadata = match state.get_metadata_mut(&id) { - Ok(value) => value, - Err(value) => { - server_channel - .fire(MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let server_channel = server_channel.clone(); - match metadata.name.as_str() { - METADATA_NAME_PROPERTY_VALUE_SETTINGS => { - metadata.add_property_listener( - SettingsState::listener(listener_state) - ) - }, - METADATA_NAME_PROPERTY_VALUE_DEFAULT => { - metadata.add_property_listener( - DefaultAudioNodesState::listener(listener_state) - ) - }, - _ => { - server_channel - .fire(MessageResponse::Error(Error { - description: format!("Unexpected metadata with name: {}", metadata.name) - })) - .unwrap(); - } - }; -} -fn handle_remove_node( - id: GlobalId, - state: Arc>, - server_channel: ServerChannel, -) -{ - let mut state = state.lock().unwrap(); - let _ = match state.get_node(&id) { - Ok(_) => {}, - Err(value) => { - server_channel - .fire(MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - state.remove(&id); -} -fn handle_set_node_properties_listener( - id: GlobalId, - state: Arc>, - server_channel: ServerChannel, - event_sender: pipewire::channel::Sender, -) -{ - let mut state = state.lock().unwrap(); - let node = match state.get_node_mut(&id) { - Ok(value) => value, - Err(value) => { - server_channel - .fire(MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let event_sender = event_sender.clone(); - node.add_properties_listener( - move |control_flow, properties| { - // "object.register" property when set to "false", indicate we should not - // register this object - // Some bluez nodes don't have sample rate information in their - // EnumFormat object. We delete those nodes since parsing node audio format - // imply to retrieve: - // - Media type - // - Media subtype - // - Sample format - // - Sample rate - // - Channels - // - Channels position - // Lets see in the future if node with no "object.register: false" property - // and with incorrect EnumFormat object occur. - if properties.get("object.register").is_some_and(move |value| value == "false") { - event_sender - .send(EventMessage::RemoveNode { - id: id.clone(), - }) - .unwrap(); - } - else { - event_sender - .send(EventMessage::SetNodeProperties { - id: id.clone(), - properties, - }) - .unwrap(); - event_sender - .send(EventMessage::SetNodeFormatListener { - id: id.clone(), - }) - .unwrap(); - } - control_flow.release(); - } - ); -} -fn handle_set_node_format_listener( - id: GlobalId, - state: Arc>, - server_channel: ServerChannel, - event_sender: pipewire::channel::Sender, -) -{ - let mut state = state.lock().unwrap(); - let node = match state.get_node_mut(&id) { - Ok(value) => value, - Err(value) => { - server_channel - .fire(MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let server_channel = server_channel.clone(); - let event_sender = event_sender.clone(); - node.add_format_listener( - move |control_flow, format| { - match format { - Ok(value) => { - event_sender - .send(EventMessage::SetNodeFormat { - id, - format: value, - }) - .unwrap(); - } - Err(value) => { - server_channel - .fire(MessageResponse::Error(value)) - .unwrap(); - } - }; - control_flow.release(); - } - ) -} -fn handle_set_node_properties( - id: GlobalId, - properties: HashMap, - state: Arc>, - server_channel: ServerChannel, -) -{ - let mut state = state.lock().unwrap(); - let node = match state.get_node_mut(&id) { - Ok(value) => value, - Err(value) => { - server_channel - .fire(MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - node.set_properties(properties); -} -fn handle_set_node_format( - id: GlobalId, - format: AudioInfoRaw, - state: Arc>, - server_channel: ServerChannel, -) -{ - let mut state = state.lock().unwrap(); - let node = match state.get_node_mut(&id) { - Ok(value) => value, - Err(value) => { - server_channel - .fire(MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - node.set_format(format); -} \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/mod.rs b/pipewire-client/src/client/handlers/mod.rs deleted file mode 100644 index caeefb05e..000000000 --- a/pipewire-client/src/client/handlers/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod event; -mod registry; -mod request; -mod thread; -pub use thread::pw_thread as thread; diff --git a/pipewire-client/src/client/handlers/registry.rs b/pipewire-client/src/client/handlers/registry.rs deleted file mode 100644 index 454db8b91..000000000 --- a/pipewire-client/src/client/handlers/registry.rs +++ /dev/null @@ -1,274 +0,0 @@ -use crate::client::channel::ServerChannel; -use crate::constants::{APPLICATION_NAME_PROPERTY_KEY, APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION, APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER, MEDIA_CLASS_PROPERTY_KEY, MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK, MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE, METADATA_NAME_PROPERTY_KEY, METADATA_NAME_PROPERTY_VALUE_DEFAULT, METADATA_NAME_PROPERTY_VALUE_SETTINGS}; -use crate::messages::{EventMessage, MessageRequest, MessageResponse}; -use crate::states::{ClientState, GlobalId, GlobalObjectState, GlobalState, MetadataState, NodeState}; -use pipewire::registry::GlobalObject; -use pipewire::spa; -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::{Arc, Mutex}; -use pipewire_common::utils::dict_ref_to_hashmap; - -pub(super) fn registry_global_handler( - state: Arc>, - registry: Rc, - server_channel: ServerChannel, - event_sender: pipewire::channel::Sender, -) -> impl Fn(&GlobalObject<&spa::utils::dict::DictRef>) + 'static -{ - move |global: &GlobalObject<&spa::utils::dict::DictRef>| match global.type_ { - pipewire::types::ObjectType::Client => handle_client( - global, - state.clone(), - server_channel.clone() - ), - pipewire::types::ObjectType::Metadata => handle_metadata( - global, - state.clone(), - registry.clone(), - server_channel.clone(), - event_sender.clone() - ), - pipewire::types::ObjectType::Node => handle_node( - global, - state.clone(), - registry.clone(), - server_channel.clone(), - event_sender.clone() - ), - pipewire::types::ObjectType::Port => handle_port( - global, - state.clone(), - registry.clone(), - server_channel.clone(), - event_sender.clone() - ), - pipewire::types::ObjectType::Link => handle_link( - global, - state.clone(), - registry.clone(), - server_channel.clone(), - event_sender.clone() - ), - _ => {} - } -} - -fn handle_client( - global: &GlobalObject<&spa::utils::dict::DictRef>, - state: Arc>, - server_channel: ServerChannel, -) -{ - if global.props.is_none() { - return; - } - let properties = global.props.unwrap(); - let client = - match properties.get(APPLICATION_NAME_PROPERTY_KEY) { - Some(APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER) => { - ClientState::new( - APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER.to_string() - ) - } - Some(APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION) => { - ClientState::new( - APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION.to_string() - ) - } - _ => return, - }; - let mut state = state.lock().unwrap(); - if let Err(value) = state.insert_client(global.id.into(), client) { - server_channel - .fire(MessageResponse::Error(value)) - .unwrap(); - return; - }; -} - -fn handle_metadata( - global: &GlobalObject<&spa::utils::dict::DictRef>, - state: Arc>, - registry: Rc, - server_channel: ServerChannel, - event_sender: pipewire::channel::Sender, -) -{ - if global.props.is_none() { - return; - } - let properties = global.props.unwrap(); - let metadata = - match properties.get(METADATA_NAME_PROPERTY_KEY) { - Some(METADATA_NAME_PROPERTY_VALUE_SETTINGS) - | Some(METADATA_NAME_PROPERTY_VALUE_DEFAULT) => { - let metadata = registry.bind(global).unwrap(); - MetadataState::new( - metadata, - properties.get(METADATA_NAME_PROPERTY_KEY).unwrap().to_string(), - ) - } - _ => return, - }; - let mut state = state.lock().unwrap(); - if let Err(value) = state.insert_metadata(global.id.into(), metadata) { - server_channel - .fire(MessageResponse::Error(value)) - .unwrap(); - return; - }; - let metadata = state.get_metadata(&global.id.into()).unwrap(); - add_metadata_listeners( - global.id.into(), - &metadata, - &event_sender - ); -} - -fn handle_node( - global: &GlobalObject<&spa::utils::dict::DictRef>, - state: Arc>, - registry: Rc, - server_channel: ServerChannel, - event_sender: pipewire::channel::Sender, -) -{ - if global.props.is_none() { - return; - } - let properties = global.props.unwrap(); - let mut node = match properties.get(MEDIA_CLASS_PROPERTY_KEY) { - Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE) - | Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK) => { - let node: pipewire::node::Node = registry.bind(global).unwrap(); - NodeState::new(node) - } - _ => return, - }; - node.set_properties(dict_ref_to_hashmap(properties)); - let mut state = state.lock().unwrap(); - if let Err(value) = state.insert_node(global.id.into(), node) { - server_channel - .fire(MessageResponse::Error(value)) - .unwrap(); - return; - }; - let node = state.get_node(&global.id.into()).unwrap(); - add_node_listeners( - global.id.into(), - &node, - &event_sender - ); -} - -fn handle_port( - global: &GlobalObject<&spa::utils::dict::DictRef>, - state: Arc>, - registry: Rc, - server_channel: ServerChannel, - event_sender: pipewire::channel::Sender, -) -{ - if global.props.is_none() { - - } - let properties = global.props.unwrap(); - // debug_dict_ref(properties); - - let port: pipewire::port::Port = registry.bind(global).unwrap(); - - // let node = match properties.get(MEDIA_CLASS_PROPERTY_KEY) { - // Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE) - // | Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK) => { - // let node: pipewire::node::Node = registry.bind(global).unwrap(); - // NodeState::new(node) - // } - // _ => return, - // }; - // let mut state = state.borrow_mut(); - // if let Err(value) = state.insert_node(global.id.into(), node) { - // main_sender - // .send(MessageResponse::Error(value)) - // .unwrap(); - // return; - // }; - // let node = state.get_node(&global.id.into()).unwrap(); - // add_node_listeners( - // global.id.into(), - // &node, - // &event_sender - // ); -} - -fn handle_link( - global: &GlobalObject<&spa::utils::dict::DictRef>, - state: Arc>, - registry: Rc, - server_channel: ServerChannel, - event_sender: pipewire::channel::Sender, -) -{ - if global.props.is_none() { - - } - let properties = global.props.unwrap(); - // debug_dict_ref(properties); - - let link: pipewire::link::Link = registry.bind(global).unwrap(); - // link. - - // let node = match properties.get(MEDIA_CLASS_PROPERTY_KEY) { - // Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE) - // | Some(MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK) => { - // let node: pipewire::node::Node = registry.bind(global).unwrap(); - // NodeState::new(node) - // } - // _ => return, - // }; - // let mut state = state.borrow_mut(); - // if let Err(value) = state.insert_node(global.id.into(), node) { - // main_sender - // .send(MessageResponse::Error(value)) - // .unwrap(); - // return; - // }; - // let node = state.get_node(&global.id.into()).unwrap(); - // add_node_listeners( - // global.id.into(), - // &node, - // &event_sender - // ); -} - -fn add_metadata_listeners( - id: GlobalId, - metadata: &MetadataState, - event_sender: &pipewire::channel::Sender -) { - if *metadata.state.borrow() != GlobalObjectState::Pending { - return; - } - let id = id.clone(); - event_sender - .send(EventMessage::SetMetadataListeners { - id, - }) - .unwrap() -} - -fn add_node_listeners( - id: GlobalId, - node: &NodeState, - event_sender: &pipewire::channel::Sender -) { - if node.state() != GlobalObjectState::Pending { - return; - } - let id = id.clone(); - event_sender - .send(EventMessage::SetNodePropertiesListener { - id, - }) - .unwrap() -} \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/request.rs b/pipewire-client/src/client/handlers/request.rs deleted file mode 100644 index 109946fbd..000000000 --- a/pipewire-client/src/client/handlers/request.rs +++ /dev/null @@ -1,820 +0,0 @@ -use crate::client::channel::{Request, ServerChannel}; -use crate::constants::*; -use crate::error::Error; -use crate::listeners::PipewireCoreSync; -use crate::messages::{MessageRequest, MessageResponse, StreamCallback}; -use crate::states::{GlobalId, GlobalObjectState, GlobalState, NodeState, OrphanState, StreamState}; -use crate::{AudioStreamInfo, Direction, NodeInfo}; -use pipewire::proxy::ProxyT; -use std::cell::RefCell; -use std::rc::Rc; - -#[cfg(test)] -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use pipewire_common::utils::dict_ref_to_hashmap; - -struct Context { - request: Request, - core: Rc, - core_sync: Rc, - main_loop: pipewire::main_loop::MainLoop, - state: Arc>, - server_channel: ServerChannel, -} - -pub(super) fn request_handler( - core: Rc, - core_sync: Rc, - main_loop: pipewire::main_loop::MainLoop, - state: Arc>, - server_channel: ServerChannel, -) -> impl Fn(Request) + 'static -{ - move |request| { - let message_request = request.message.clone(); - let context = Context { - request, - core: core.clone(), - core_sync: core_sync.clone(), - main_loop: main_loop.clone(), - state: state.clone(), - server_channel: server_channel.clone(), - }; - match message_request { - MessageRequest::Quit => main_loop.quit(), - MessageRequest::Settings => handle_settings( - context, - ), - MessageRequest::DefaultAudioNodes => handle_default_audio_nodes( - context, - ), - MessageRequest::GetNode { - name, - direction - } => handle_get_node(context, name, direction), - MessageRequest::CreateNode { - name, - description, - nickname, - direction, - channels, - } => handle_create_node( - context, - name, - description, - nickname, - direction, - channels, - ), - MessageRequest::DeleteNode(id) => handle_delete_node(context, id), - MessageRequest::EnumerateNodes(direction) => handle_enumerate_node( - context, - direction, - ), - MessageRequest::CreateStream { - node_id, - direction, - format, - callback - } => handle_create_stream( - context, - node_id, - direction, - format, - callback, - ), - MessageRequest::DeleteStream(name) => handle_delete_stream( - context, - name, - ), - MessageRequest::ConnectStream(name) => handle_connect_stream( - context, - name, - ), - MessageRequest::DisconnectStream(name) => handle_disconnect_stream( - context, - name, - ), - // Internal requests - MessageRequest::CheckSessionManagerRegistered => handle_check_session_manager_registered( - context, - ), - MessageRequest::SettingsState => handle_settings_state( - context, - ), - MessageRequest::DefaultAudioNodesState => handle_default_audio_nodes_state( - context, - ), - MessageRequest::NodeState(id) => handle_node_state( - context, - id, - ), - MessageRequest::NodeStates => handle_node_states( - context, - ), - MessageRequest::NodeCount => handle_node_count( - context, - ), - #[cfg(test)] - MessageRequest::Listeners => handle_listeners( - context, - ), - } - } -} - -fn handle_settings( - context: Context, -) -{ - let state = context.state.lock().unwrap(); - let settings = state.get_settings(); - context.server_channel - .send(&context.request, MessageResponse::Settings(settings)) - .unwrap(); -} -fn handle_default_audio_nodes( - context: Context, -) -{ - let state = context.state.lock().unwrap(); - let default_audio_devices = state.get_default_audio_nodes(); - context.server_channel - .send(&context.request, MessageResponse::DefaultAudioNodes(default_audio_devices)) - .unwrap(); -} -fn handle_get_node( - context: Context, - name: String, - direction: Direction, -) -{ - let control_flow = RefCell::new(false); - let state = context.state.lock().unwrap(); - let default_audio_nodes = state.get_default_audio_nodes(); - let default_audio_node = match direction { - Direction::Input => default_audio_nodes.source.clone(), - Direction::Output => default_audio_nodes.sink.clone() - }; - let nodes = match state.get_nodes() { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let node = nodes.iter() - .find_map(|(id, node)| { - let properties = node.properties().unwrap(); - let format = node.format().unwrap(); - let name_to_compare = match node.name() { - Ok(value) => value, - Err(value) => { - control_flow.replace(true); - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return None; - } - }; - let direction_to_compare = match node.direction() { - Ok(value) => value, - Err(value) => { - control_flow.replace(true); - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return None; - } - }; - if name_to_compare == name && direction_to_compare == direction { - Some((id, properties, format)) - } else { - None - } - }) - .iter() - .find_map(|(id, properties, format)| { - if *control_flow.borrow() == true { - return None; - } - let name = properties.get(*pipewire::keys::NODE_NAME).unwrap().clone(); - let description = properties - .get(*pipewire::keys::NODE_DESCRIPTION) - .unwrap() - .clone(); - let nickname = match properties.contains_key(*pipewire::keys::NODE_NICK) { - true => properties.get(*pipewire::keys::NODE_NICK).unwrap().clone(), - false => name.clone(), - }; - let is_default = name == default_audio_node; - Some(NodeInfo { - id: (**id).clone().into(), - name, - description, - nickname, - direction: direction.clone(), - is_default, - format: format.clone() - }) - }); - match node { - Some(value) => context.server_channel - .send(&context.request, MessageResponse::GetNode(value)) - .unwrap(), - None => context.server_channel - .send(&context.request, MessageResponse::Error(Error { - description: format!("Node with name({}) not found", name), - })) - .unwrap() - } - -} -fn handle_create_node( - context: Context, - name: String, - description: String, - nickname: String, - direction: Direction, - channels: u16, -) -{ - { - let control_flow = RefCell::new(false); - let state = context.state.lock().unwrap(); - let nodes = match state.get_nodes() { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let is_exists = nodes.iter().any(|(_, node)| { - if *control_flow.borrow() == true { - return false; - } - let name_to_compare = match node.name() { - Ok(value) => value, - Err(value) => { - control_flow.replace(true); - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return false; - } - }; - let direction_to_compare = match node.direction() { - Ok(value) => value, - Err(value) => { - control_flow.replace(true); - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return false; - } - }; - name_to_compare == name && direction_to_compare == direction - }); - if is_exists { - context.server_channel - .send( - &context.request, - MessageResponse::Error(Error { - description: format!("Node with name({}) already exists", name).to_string(), - } - )) - .unwrap(); - } - } - let default_audio_position = format!( - "[ {} ]", - (1..=channels + 1) - .map(|n| n.to_string()) - .collect::>() - .join(" ") - ); - let properties = &pipewire::properties::properties! { - *pipewire::keys::FACTORY_NAME => "support.null-audio-sink", - *pipewire::keys::NODE_NAME => name.clone(), - *pipewire::keys::NODE_DESCRIPTION => description.clone(), - *pipewire::keys::NODE_NICK => nickname.clone(), - *pipewire::keys::MEDIA_CLASS => match direction { - Direction::Input => MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE, - Direction::Output => MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK, - }, - *pipewire::keys::OBJECT_LINGER => "false", - *pipewire::keys::AUDIO_CHANNELS => channels.to_string(), - MONITOR_CHANNEL_VOLUMES_PROPERTY_KEY => "true", - MONITOR_PASSTHROUGH_PROPERTY_KEY => "true", - AUDIO_POSITION_PROPERTY_KEY => match channels { - 1 => "[ MONO ]", - 2 => "[ FL FR ]", // 2.0 - 3 => "[ FL FR LFE ]", // 2.1 - 4 => "[ FL FR RL RR ]", // 4.0 - 5 => "[ FL FR FC RL RR ]", // 5.0 - 6 => "[ FL FR FC RL RR LFE ]", // 5.1 - 7 => "[ FL FR FC RL RR SL SR ]", // 7.0 - 8 => "[ FL FR FC RL RR SL SR LFE ]", // 7.1 - _ => default_audio_position.as_str(), - } - }; - let node: pipewire::node::Node = match context.core - .create_object("adapter", properties) - .map_err(move |error| { - Error { - description: error.to_string(), - } - }) { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let core_sync = context.core_sync.clone(); - let listener_server_channel = context.server_channel.clone(); - let listener_state = context.state.clone(); - let listener_properties = properties.clone(); - core_sync.register( - PIPEWIRE_CORE_SYNC_CREATE_DEVICE_SEQ, - move |control_flow| { - let mut state = listener_state.lock().unwrap(); - let mut nodes = match state.get_nodes_mut() { - Ok(value) => value, - Err(value) => { - listener_server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - control_flow.release(); - return; - } - }; - let node = nodes.iter_mut() - .find(move |(_, node)| { - node.state() == GlobalObjectState::Pending - }); - match node { - Some((id, node)) => { - let properties = dict_ref_to_hashmap(listener_properties.dict()); - node.set_properties(properties); - listener_server_channel - .send( - &context.request, - MessageResponse::CreateNode((*id).clone()) - ) - .unwrap(); - } - None => { - listener_server_channel - .send( - &context.request, - MessageResponse::Error(Error { - description: "Created node not found".to_string(), - }) - ) - .unwrap(); - } - } - control_flow.release(); - } - ); - let mut state = context.state.lock().unwrap(); - // We need to store created node object as orphan since it had not been - // registered by server at this point (does not have an id yet). - // - // When a proxy object is dropped its send a server request to remove it on server - // side, then the server ask clients to remove proxy object on their side. - // - // The server will send a global object (through registry global object event - // listener) later, represented by a new proxy object instance that we can store - // as a NodeState. - // OrphanState object define "removed" listener from Proxy to ensure our orphan - // proxy object is removed when proper NodeState object is retrieved from server - let orphan = OrphanState::new(node.upcast()); - state.insert_orphan(orphan); -} -fn handle_delete_node( - context: Context, - id: GlobalId, -) -{ - match context.state.lock().unwrap().delete_node(&id) { - Ok(_) => { - context.server_channel - .send(&context.request, MessageResponse::DeleteNode) - .unwrap() - } - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap() - } - }; -} - -fn handle_enumerate_node( - context: Context, - direction: Direction, -) -{ - let state = context.state.lock().unwrap(); - let default_audio_nodes = state.get_default_audio_nodes(); - let default_audio_node = match direction { - Direction::Input => default_audio_nodes.source.clone(), - Direction::Output => default_audio_nodes.sink.clone() - }; - let filter_value = match direction { - Direction::Input => MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE, - Direction::Output => MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK, - }; - let nodes = match state.get_nodes() { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let nodes: Vec = nodes - .iter() - .filter_map(|(id, node)| { - let properties = node.properties().unwrap(); - let format = node.format().unwrap(); - if properties.iter().any(|(_, v)| v == filter_value) { - Some((id, properties, format)) - } else { - None - } - }) - .map(|(id, properties, format)| { - let name = properties.get(*pipewire::keys::NODE_NAME).unwrap().clone(); - let description = properties - .get(*pipewire::keys::NODE_DESCRIPTION) - .unwrap() - .clone(); - let nickname = match properties.contains_key(*pipewire::keys::NODE_NICK) { - true => properties.get(*pipewire::keys::NODE_NICK).unwrap().clone(), - false => name.clone(), - }; - let is_default = name == default_audio_node; - NodeInfo { - id: (*id).clone().into(), - name, - description, - nickname, - direction: direction.clone(), - is_default, - format: format.clone() - } - }) - .collect(); - context.server_channel.send(&context.request, MessageResponse::EnumerateNodes(nodes)).unwrap(); -} -fn handle_create_stream( - context: Context, - node_id: GlobalId, - direction: Direction, - format: AudioStreamInfo, - callback: StreamCallback, -) -{ - let mut state = context.state.lock().unwrap(); - let node_name = match state.get_node(&node_id) { - Ok(value) => { - match value.name() { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - } - }, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let stream_name = match direction { - Direction::Input => { - format!("{}.stream_input", node_name) - } - Direction::Output => { - format!("{}.stream_output", node_name) - } - }; - let properties = pipewire::properties::properties! { - *pipewire::keys::MEDIA_TYPE => MEDIA_TYPE_PROPERTY_VALUE_AUDIO, - *pipewire::keys::MEDIA_CLASS => match direction { - Direction::Input => MEDIA_CLASS_PROPERTY_VALUE_STREAM_INPUT_AUDIO, - Direction::Output => MEDIA_CLASS_PROPERTY_VALUE_STREAM_OUTPUT_AUDIO, - }, - }; - let stream = match pipewire::stream::Stream::new( - &context.core, - stream_name.clone().as_str(), - properties, - ) - .map_err(move |error| { - Error { - description: error.to_string(), - } - }) { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let mut stream = StreamState::new( - stream_name.clone(), - format.into(), - direction.into(), - stream - ); - stream.add_process_listener(callback); - if let Err(value) = state.insert_stream(stream_name.clone(), stream) { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - }; - context.server_channel - .send( - &context.request, - MessageResponse::CreateStream(stream_name.clone()) - ) - .unwrap(); -} -fn handle_delete_stream( - context: Context, - name: String, -) -{ - let mut state = context.state.lock().unwrap(); - let stream = match state.get_stream_mut(&name) { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - if stream.is_connected() { - if let Err(value) = stream.disconnect() { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - }; - } - if let Err(value) = state.delete_stream(&name) { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - }; - context.server_channel.send(&context.request, MessageResponse::DeleteStream).unwrap(); -} -fn handle_connect_stream( - context: Context, - name: String, -) -{ - let mut state = context.state.lock().unwrap(); - let stream = match state.get_stream_mut(&name) { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - if let Err(value) = stream.connect() { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - }; - context.server_channel.send(&context.request, MessageResponse::ConnectStream).unwrap(); -} -fn handle_disconnect_stream( - context: Context, - name: String, -) -{ - let mut state = context.state.lock().unwrap(); - let stream = match state.get_stream_mut(&name) { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - if let Err(value) = stream.disconnect() { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - }; - context.server_channel.send(&context.request, MessageResponse::DisconnectStream).unwrap(); -} -fn handle_check_session_manager_registered( - context: Context, -) -{ - pub(crate) fn generate_error_message(session_managers: &Vec<&str>) -> String { - let session_managers = session_managers.iter() - .map(move |session_manager| { - let session_manager = match *session_manager { - APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER => "WirePlumber", - APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION => "PipeWire Media Session", - _ => panic!("Cannot determine session manager name") - }; - format!(" - {}", session_manager) - }) - .collect::>() - .join("\n"); - let message = format!( - "No session manager registered. Install and run one of the following:\n{}", - session_managers - ); - message - } - // Checking if session manager is registered because we need "default" metadata - // object to determine default audio nodes (sink and source). - let session_managers = vec![ - APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER, - APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION - ]; - let error_description = generate_error_message(&session_managers); - let state = context.state.lock().unwrap(); - let clients = state.get_clients().map_err(|_| { - Error { - description: error_description.clone(), - } - }); - let clients = match clients { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let session_manager_registered = clients.iter() - .any(|(_, client)| { - session_managers.contains(&client.name.as_str()) - }); - context.server_channel - .send( - &context.request, - MessageResponse::CheckSessionManagerRegistered { - session_manager_registered, - error: match session_manager_registered { - true => Some(Error { - description: error_description.clone() - }), - false => None - }, - } - ) - .unwrap(); -} -fn handle_settings_state( - context: Context, -) -{ - let state = context.state.lock().unwrap(); - context.server_channel - .send(&context.request, MessageResponse::SettingsState(state.get_settings().state)) - .unwrap(); -} -fn handle_default_audio_nodes_state( - context: Context, -) -{ - let state = context.state.lock().unwrap(); - context.server_channel - .send(&context.request, MessageResponse::DefaultAudioNodesState(state.get_default_audio_nodes().state)) - .unwrap(); -} -fn handle_node_state( - context: Context, - id: GlobalId, -) { - let state = context.state.lock().unwrap(); - let node = match state.get_node(&id) { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let state = node.state(); - context.server_channel - .send(&context.request, MessageResponse::NodeState(state)) - .unwrap(); -} -fn handle_node_states( - context: Context, -) -{ - let state = context.state.lock().unwrap(); - let nodes = match state.get_nodes() { - Ok(value) => value, - Err(value) => { - context.server_channel - .send(&context.request, MessageResponse::Error(value)) - .unwrap(); - return; - } - }; - let states = nodes.iter() - .map(move |(_, node)| { - node.state() - }) - .collect::>(); - context.server_channel - .send(&context.request, MessageResponse::NodeStates(states)) - .unwrap(); -} -fn handle_node_count( - context: Context, -) -{ - let state = context.state.lock().unwrap(); - match state.get_nodes() { - Ok(value) => { - context.server_channel - .send(&context.request, MessageResponse::NodeCount(value.len() as u32)) - .unwrap(); - }, - Err(_) => { - context.server_channel - .send(&context.request, MessageResponse::NodeCount(0)) - .unwrap(); - } - }; -} -#[cfg(test)] -fn handle_listeners( - context: Context, -) -{ - let state = context.state.lock().unwrap(); - let mut core = HashMap::new(); - core.insert("0".to_string(), context.core_sync.get_listener_names()); - let metadata = state.get_metadatas() - .unwrap_or_default() - .iter() - .map(move |(id, metadata)| { - (id.to_string(), metadata.get_listener_names()) - }) - .collect::>(); - let nodes = state.get_nodes() - .unwrap_or_default() - .iter() - .map(move |(id, node)| { - (id.to_string(), node.get_listener_names()) - }) - .collect::>(); - let streams = state.get_streams() - .unwrap_or_default() - .iter() - .map(move |(name, stream)| { - ((*name).clone(), stream.get_listener_names()) - }) - .collect::>(); - context.server_channel - .send( - &context.request, - MessageResponse::Listeners { - core, - metadata, - nodes, - streams, - } - ) - .unwrap(); -} \ No newline at end of file diff --git a/pipewire-client/src/client/handlers/thread.rs b/pipewire-client/src/client/handlers/thread.rs deleted file mode 100644 index 00b806c2c..000000000 --- a/pipewire-client/src/client/handlers/thread.rs +++ /dev/null @@ -1,152 +0,0 @@ -use crate::client::connection_string::PipewireClientInfo; -use crate::client::handlers::event::event_handler; -use crate::client::handlers::registry::registry_global_handler; -use crate::client::handlers::request::request_handler; -use crate::constants::{PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ, PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY}; -use crate::error::Error; -use crate::messages::{EventMessage, MessageRequest, MessageResponse}; -use crate::states::GlobalState; -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::{Arc, Mutex, Once}; -use libc::atexit; -use crate::client::channel::ServerChannel; -use crate::listeners::PipewireCoreSync; - -static AT_EXIT: Once = Once::new(); - -extern "C" fn at_exit_callback() { - unsafe { pipewire::deinit(); } -} - -pub fn pw_thread( - client_info: PipewireClientInfo, - mut server_channel: ServerChannel, - event_sender: pipewire::channel::Sender, - event_receiver: pipewire::channel::Receiver, -) { - pipewire::init(); - - AT_EXIT.call_once(|| { - unsafe { - atexit(at_exit_callback); - } - }); - - let connection_properties = Some(pipewire::properties::properties! { - PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY => client_info.socket_location, - *pipewire::keys::REMOTE_NAME => client_info.socket_name, - *pipewire::keys::APP_NAME => client_info.name, - }); - - let main_loop = match pipewire::main_loop::MainLoop::new(None) { - Ok(value) => value, - Err(value) => { - server_channel - .fire(MessageResponse::Error(Error { - description: format!("Failed to create PipeWire main loop: {}", value), - })) - .unwrap(); - return; - } - }; - - let context = match pipewire::context::Context::new(&main_loop) { - Ok(value) => Rc::new(value), - Err(value) => { - server_channel - .fire(MessageResponse::Error(Error { - description: format!("Failed to create PipeWire context: {}", value), - })) - .unwrap(); - return; - } - }; - - let core = match context.connect(connection_properties.clone()) { - Ok(value) => value, - Err(value) => { - server_channel - .fire(MessageResponse::Error(Error { - description: format!("Failed to connect PipeWire server: {}", value), - })) - .unwrap(); - return; - } - }; - - let listener_main_sender = server_channel.clone(); - let _core_listener = core - .add_listener_local() - .error(move |_, _, _, message| { - listener_main_sender - .fire(MessageResponse::Error(Error { - description: format!("Server error: {}", message), - })) - .unwrap(); - }) - .register(); - - let registry = match core.get_registry() { - Ok(value) => Rc::new(value), - Err(value) => { - server_channel - .fire(MessageResponse::Error(Error { - description: format!("Failed to get Pipewire registry: {}", value), - })) - .unwrap(); - return; - } - }; - - let core_sync = Rc::new(PipewireCoreSync::new(Rc::new(RefCell::new(core.clone())))); - let core = Rc::new(core); - let state = Arc::new(Mutex::new(GlobalState::default())); - - let listener_main_sender = server_channel.clone(); - core_sync.register( - PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ, - move |control_flow| { - listener_main_sender - .fire(MessageResponse::Initialized) - .unwrap(); - control_flow.release(); - } - ); - - let _attached_event_receiver = event_receiver.attach( - main_loop.loop_(), - event_handler( - state.clone(), - server_channel.clone(), - event_sender.clone() - ) - ); - - let _attached_pw_receiver = server_channel.attach( - main_loop.loop_(), - request_handler( - core.clone(), - core_sync.clone(), - main_loop.clone(), - state.clone(), - server_channel.clone() - ) - ); - - let _registry_listener = registry - .add_listener_local() - .global(registry_global_handler( - state.clone(), - registry.clone(), - server_channel.clone(), - event_sender.clone(), - )) - .global_remove(move |global_id| { - let mut state = state.lock().unwrap(); - state.remove(&global_id.into()) - }) - .register(); - - main_loop.run(); -} \ No newline at end of file diff --git a/pipewire-client/src/client/implementation.rs b/pipewire-client/src/client/implementation.rs deleted file mode 100644 index 7acb23f42..000000000 --- a/pipewire-client/src/client/implementation.rs +++ /dev/null @@ -1,205 +0,0 @@ -extern crate pipewire; - -use std::thread; -use crate::client::api::{CoreApi, InternalApi, NodeApi, StreamApi}; -use crate::client::channel::channels; -use crate::client::connection_string::{PipewireClientInfo, PipewireClientSocketPath}; -use crate::client::handlers::thread; -use crate::error::Error; -use crate::messages::{EventMessage, MessageRequest, MessageResponse}; -use crate::states::GlobalObjectState; -use crate::utils::Backoff; -use std::fmt::{Debug, Formatter}; -use std::path::PathBuf; -use std::string::ToString; -use std::sync::atomic::{AtomicU32, Ordering}; -use std::sync::Arc; -use std::thread::JoinHandle; -use std::time::Duration; -use tokio::runtime::Runtime; - -pub(super) static CLIENT_NAME_PREFIX: &str = "pipewire-client"; -pub(super) static CLIENT_INDEX: AtomicU32 = AtomicU32::new(0); - -pub struct PipewireClient { - pub(crate) name: String, - socket_path: PathBuf, - thread_handle: Option>, - timeout: Duration, - internal_api: Arc, - core_api: CoreApi, - node_api: NodeApi, - stream_api: StreamApi, -} - -impl PipewireClient { - pub fn new( - runtime: Arc, - timeout: Duration, - ) -> Result { - let name = format!("{}-{}", CLIENT_NAME_PREFIX, CLIENT_INDEX.load(Ordering::SeqCst)); - CLIENT_INDEX.fetch_add(1, Ordering::SeqCst); - - let socket_path = PipewireClientSocketPath::from_env(); - - let client_info = PipewireClientInfo { - name: name.clone(), - socket_location: socket_path.parent().unwrap().to_str().unwrap().to_string(), - socket_name: socket_path.file_name().unwrap().to_str().unwrap().to_string(), - }; - - let (client_channel, server_channel) = channels(runtime.clone()); - let (event_sender, event_receiver) = pipewire::channel::channel::(); - - let pw_thread = thread::spawn(move || thread( - client_info, - server_channel, - event_sender, - event_receiver - )); - - let internal_api = Arc::new(InternalApi::new(client_channel, timeout.clone())); - let core_api = CoreApi::new(internal_api.clone()); - let node_api = NodeApi::new(internal_api.clone()); - let stream_api = StreamApi::new(internal_api.clone()); - - let client = Self { - name, - socket_path, - thread_handle: Some(pw_thread), - timeout, - internal_api, - core_api, - node_api, - stream_api, - }; - - match client.wait_initialization() { - Ok(_) => {} - Err(value) => return Err(Error { - description: format!("Initialization error: {}", value), - }) - }; - match client.wait_post_initialization() { - Ok(_) => {} - Err(value) => { - let global_messages = &client.internal_api.channel.global_messages; - return Err(Error { - description: format!("Post initialization error: {}", value), - }) - }, - }; - Ok(client) - } - - fn wait_initialization(&self) -> Result<(), Error> { - let response = self.internal_api.wait_response_with_timeout(self.timeout); - let response = match response { - Ok(value) => value, - Err(value) => { - // Timeout is certainly due to missing session manager - // We need to check if that's the case. If session manager is running then we return - // timeout error. - return match self.core_api.check_session_manager_registered() { - Ok(_) => Err(value), - Err(value) => Err(value) - }; - } - }; - match response { - MessageResponse::Initialized => Ok(()), - _ => Err(Error { - description: format!("Received unexpected response: {:?}", response), - }), - } - } - - fn wait_post_initialization(&self) -> Result<(), Error> { - let mut settings_initialized = false; - let mut default_audio_nodes_initialized = false; - let mut nodes_initialized = false; - self.core_api.check_session_manager_registered()?; - match self.node_api.count() { - Ok(value) => { - if value == 0 { - return Err(Error { - description: "Zero node registered".to_string(), - }) - } - } - Err(value) => return Err(value), - } - let operation = move || { - if settings_initialized == false { - let settings_state = self.core_api.get_settings_state()?; - if settings_state == GlobalObjectState::Initialized { - settings_initialized = true; - } - } - if default_audio_nodes_initialized == false { - let default_audio_nodes_state = self.core_api.get_default_audio_nodes_state()?; - if default_audio_nodes_state == GlobalObjectState::Initialized { - default_audio_nodes_initialized = true; - } - } - if nodes_initialized == false { - let node_states = self.node_api.states()?; - let condition = node_states.iter() - .all(|state| *state == GlobalObjectState::Initialized); - if condition { - nodes_initialized = true; - } - } - if settings_initialized == false || default_audio_nodes_initialized == false || nodes_initialized == false { - return Err(Error { - description: format!( - r"Conditions not yet initialized: - - settings: {} - - default audio nodes: {} - - nodes: {}", - settings_initialized, - default_audio_nodes_initialized, - nodes_initialized - ), - }) - } - return Ok(()); - }; - let mut backoff = Backoff::constant(self.timeout.as_millis()); - backoff.retry(operation) - } - - pub(crate) fn internal(&self) -> Arc { - self.internal_api.clone() - } - - pub fn core(&self) -> &CoreApi { - &self.core_api - } - - pub fn node(&self) -> &NodeApi { - &self.node_api - } - - pub fn stream(&self) -> &StreamApi { - &self.stream_api - } -} - -impl Debug for PipewireClient { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "PipewireClient: {}", self.socket_path.to_str().unwrap()) - } -} - -impl Drop for PipewireClient { - fn drop(&mut self) { - if self.internal_api.send_request_without_response(&MessageRequest::Quit).is_ok() { - if let Some(thread_handle) = self.thread_handle.take() { - thread_handle.join().unwrap(); - } - } else { - panic!("Failed to send Quit message to PipeWire thread."); - } - } -} \ No newline at end of file diff --git a/pipewire-client/src/client/implementation_test.rs b/pipewire-client/src/client/implementation_test.rs deleted file mode 100644 index 95e712a39..000000000 --- a/pipewire-client/src/client/implementation_test.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::client::implementation::{CLIENT_INDEX, CLIENT_NAME_PREFIX}; -use crate::states::{MetadataState, NodeState}; -use crate::test_utils::fixtures::{client2, shared_client, PipewireTestClient}; -use crate::PipewireClient; -use rstest::rstest; -use serial_test::serial; -use std::any::TypeId; -use std::sync::Arc; -use std::thread; -use std::time::Duration; -use tokio::runtime::Runtime; -use pipewire_test_utils::environment::TEST_ENVIRONMENT; -use pipewire_test_utils::server::{server_with_default_configuration, server_without_node, server_without_session_manager, Server}; -use crate::listeners::PipewireCoreSync; - -#[rstest] -#[serial] -pub fn names( - #[from(client2)] (client_1, client_2): (PipewireTestClient, PipewireTestClient) -) { - let client_1_index = client_1.name.replace(format!("{}-", CLIENT_NAME_PREFIX).as_str(), "") - .parse::() - .unwrap(); - assert_eq!(format!("{}-{}", CLIENT_NAME_PREFIX, client_1_index), client_1.name); - assert_eq!(format!("{}-{}", CLIENT_NAME_PREFIX, client_1_index + 1), client_2.name); -} - -#[rstest] -#[serial] -#[ignore] -fn init100(#[from(server_with_default_configuration)] _server: Arc) { - for index in 0..100 { - thread::sleep(Duration::from_millis(10)); - println!("Init client: {}", index); - let _ = PipewireClient::new( - Arc::new(Runtime::new().unwrap()), - TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), - ).unwrap(); - assert_eq!(index + 1, CLIENT_INDEX.load(std::sync::atomic::Ordering::SeqCst)); - } -} - -#[rstest] -#[serial] -pub fn with_default_configuration(#[from(shared_client)] client: PipewireTestClient) { - let listeners = client.core().get_listeners().unwrap(); - let core_listeners = listeners.get(&TypeId::of::()).unwrap(); - let metadata_listeners = listeners.get(&TypeId::of::()).unwrap(); - let nodes_listeners = listeners.get(&TypeId::of::()).unwrap(); - // No need to check stream listeners since we had to create them in first place (i.e. after client init phases). - for (_, listeners) in core_listeners { - assert_eq!(0, listeners.len()); - } - for (_, listeners) in metadata_listeners { - assert_eq!(0, listeners.len()); - } - for (_, listeners) in nodes_listeners { - assert_eq!(0, listeners.len()); - } -} - -#[rstest] -#[serial] -pub fn without_session_manager(#[from(server_without_session_manager)] _server: Arc) { - let error = PipewireClient::new( - Arc::new(Runtime::new().unwrap()), - TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), - ).unwrap_err(); - assert_eq!(true, error.description.contains("No session manager registered")) -} - -#[rstest] -#[serial] -pub fn without_node(#[from(server_without_node)] _server: Arc) { - let error = PipewireClient::new( - Arc::new(Runtime::new().unwrap()), - TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), - ).unwrap_err(); - assert_eq!("Post initialization error: Zero node registered", error.description) -} \ No newline at end of file diff --git a/pipewire-client/src/client/mod.rs b/pipewire-client/src/client/mod.rs deleted file mode 100644 index 3b8a656c9..000000000 --- a/pipewire-client/src/client/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod implementation; -pub use implementation::PipewireClient; -mod connection_string; -mod handlers; -mod api; -mod channel; - -#[cfg(test)] -pub(super) use api::CoreApi; - -#[cfg(test)] -#[path = "./implementation_test.rs"] -mod implementation_test; - -#[cfg(test)] -#[path = "./channel_test.rs"] -mod channel_test; \ No newline at end of file diff --git a/pipewire-client/src/info.rs b/pipewire-client/src/info.rs deleted file mode 100644 index 9551f7475..000000000 --- a/pipewire-client/src/info.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::utils::Direction; -use pipewire_spa_utils::audio::raw::AudioInfoRaw; -use pipewire_spa_utils::audio::AudioChannelPosition; -use pipewire_spa_utils::audio::AudioSampleFormat; -use pipewire_spa_utils::format::{MediaSubtype, MediaType}; - -#[derive(Debug, Clone)] -pub struct NodeInfo { - pub id: u32, - pub name: String, - pub description: String, - pub nickname: String, - pub direction: Direction, - pub is_default: bool, - pub format: AudioInfoRaw -} - -#[derive(Debug, Clone)] -pub struct AudioStreamInfo { - pub media_type: MediaType, - pub media_subtype: MediaSubtype, - pub sample_format: AudioSampleFormat, - pub sample_rate: u32, - pub channels: u32, - pub position: AudioChannelPosition -} - -impl From for AudioStreamInfo { - fn from(value: AudioInfoRaw) -> Self { - Self { - media_type: MediaType::Audio, - media_subtype: MediaSubtype::Raw, - sample_format: value.sample_format.default, - sample_rate: value.sample_rate.value, - channels: *value.channels, - position: AudioChannelPosition::default(), - } - } -} - -impl From for pipewire::spa::param::audio::AudioInfoRaw { - fn from(value: AudioStreamInfo) -> Self { - let format: pipewire::spa::sys::spa_audio_format = value.sample_format as u32; - let format = pipewire::spa::param::audio::AudioFormat::from_raw(format); - let position: [u32; 64] = value.position.to_array(); - let mut info = pipewire::spa::param::audio::AudioInfoRaw::default(); - info.set_format(format); - info.set_rate(value.sample_rate); - info.set_channels(value.channels); - info.set_position(position); - info - } -} \ No newline at end of file diff --git a/pipewire-client/src/lib.rs b/pipewire-client/src/lib.rs deleted file mode 100644 index f2b752d54..000000000 --- a/pipewire-client/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -use pipewire_common::error as error; -use pipewire_common::utils as utils; -pub use pipewire_common::utils::Direction; -pub use pipewire_common::constants as constants; - -mod client; -pub use client::PipewireClient; - -mod listeners; -mod messages; -mod states; - -mod info; - -#[cfg(test)] -pub mod test_utils; - -pub use info::AudioStreamInfo; -pub use info::NodeInfo; - -pub use pipewire as pipewire; -pub use pipewire_spa_utils as spa_utils; diff --git a/pipewire-client/src/listeners.rs b/pipewire-client/src/listeners.rs deleted file mode 100644 index 273550aa7..000000000 --- a/pipewire-client/src/listeners.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; - -pub struct ListenerControlFlow { - is_released: bool -} - -impl ListenerControlFlow { - pub fn new() -> Self { - Self { - is_released: false, - } - } - - pub fn is_released(&self) -> bool { - self.is_released - } - - pub fn release(&mut self) { - if self.is_released { - return; - } - self.is_released = true; - } -} - -pub(super) struct Listener { - inner: T, - control_flow: Rc>, -} - -impl Listener { - pub fn new(inner: T, control_flow: Rc>) -> Self - { - Self { - inner, - control_flow, - } - } -} - -pub(super) struct Listeners { - listeners: Rc>>>, -} - -impl Listeners { - pub fn new() -> Self { - Self { - listeners: Rc::new(RefCell::new(HashMap::new())), - } - } - - pub fn get_names(&self) -> Vec { - self.listeners.borrow().keys().cloned().collect() - } - - pub fn add(&mut self, name: String, listener: Listener) { - let mut listeners = self.listeners.borrow_mut(); - listeners.insert(name, listener); - } - - pub fn triggered(&mut self, name: &String) { - let mut listeners = self.listeners.borrow_mut(); - let listener = listeners.get_mut(name).unwrap(); - if listener.control_flow.borrow().is_released == false { - return; - } - listeners.remove(name); - } -} - -pub(super) struct PipewireCoreSync { - core: Rc>, - listeners: Rc>>, -} - -impl PipewireCoreSync { - pub fn new(core: Rc>) -> Self { - Self { - core, - listeners: Rc::new(RefCell::new(Listeners::new())), - } - } - - pub(super) fn get_listener_names(&self) -> Vec { - self.listeners.borrow().get_names() - } - - pub fn register(&self, seq: u32, callback: F) - where - F: Fn(&mut ListenerControlFlow) + 'static, - { - let sync_id = self.core.borrow_mut().sync(seq as i32).unwrap(); - let name = format!("sync-{}", sync_id.raw()); - let listeners = self.listeners.clone(); - let listener_name = name.clone(); - let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); - let listener_control_flow = control_flow.clone(); - let listener = self - .core - .borrow_mut() - .add_listener_local() - .done(move |_, seq| { - if seq != sync_id { - return; - } - if listener_control_flow.borrow().is_released() { - return; - } - callback(&mut listener_control_flow.borrow_mut()); - listeners.borrow_mut().triggered(&listener_name); - }) - .register(); - self.listeners - .borrow_mut() - .add(name, Listener::new(listener, control_flow)); - } -} - -impl Clone for PipewireCoreSync { - fn clone(&self) -> Self { - Self { - core: self.core.clone(), - listeners: self.listeners.clone(), - } - } -} \ No newline at end of file diff --git a/pipewire-client/src/messages.rs b/pipewire-client/src/messages.rs deleted file mode 100644 index f541d2697..000000000 --- a/pipewire-client/src/messages.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::error::Error; -use crate::info::{AudioStreamInfo, NodeInfo}; -use crate::listeners::ListenerControlFlow; -use crate::states::{DefaultAudioNodesState, GlobalId, GlobalObjectState, SettingsState}; -use crate::utils::Direction; -use pipewire_spa_utils::audio::raw::AudioInfoRaw; -use std::collections::HashMap; -use std::fmt::{Debug, Formatter}; -use std::sync::{Arc, Mutex}; - -pub(super) struct StreamCallback { - callback: Arc>> -} - -impl From for StreamCallback { - fn from(value: F) -> Self { - Self { callback: Arc::new(Mutex::new(Box::new(value))) } - } -} - -impl StreamCallback { - pub fn call(&mut self, control_flow: &mut ListenerControlFlow, buffer: pipewire::buffer::Buffer) { - let mut callback = self.callback.lock().unwrap(); - callback(control_flow, buffer); - } -} - -impl Debug for StreamCallback { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("StreamCallback").finish() - } -} - -impl Clone for StreamCallback { - fn clone(&self) -> Self { - Self { callback: self.callback.clone() } - } -} - -#[derive(Debug, Clone)] -pub(super) enum MessageRequest { - Quit, - Settings, - DefaultAudioNodes, - // Node - GetNode { - name: String, - direction: Direction, - }, - CreateNode { - name: String, - description: String, - nickname: String, - direction: Direction, - channels: u16, - }, - DeleteNode(GlobalId), - EnumerateNodes(Direction), - // Stream - CreateStream { - node_id: GlobalId, - direction: Direction, - format: AudioStreamInfo, - callback: StreamCallback, - }, - DeleteStream(String), - ConnectStream(String), - DisconnectStream(String), - // Internal requests - CheckSessionManagerRegistered, - SettingsState, - DefaultAudioNodesState, - NodeState(GlobalId), - NodeStates, - NodeCount, - #[cfg(test)] - Listeners -} - -#[derive(Debug, Clone)] -pub(super) enum MessageResponse { - Error(Error), - Initialized, - Settings(SettingsState), - DefaultAudioNodes(DefaultAudioNodesState), - // Nodes - GetNode(NodeInfo), - CreateNode(GlobalId), - DeleteNode, - EnumerateNodes(Vec), - // Streams - CreateStream(String), - DeleteStream, - ConnectStream, - DisconnectStream, - // Internals responses - CheckSessionManagerRegistered { - session_manager_registered: bool, - error: Option, - }, - SettingsState(GlobalObjectState), - DefaultAudioNodesState(GlobalObjectState), - NodeState(GlobalObjectState), - NodeStates(Vec), - NodeCount(u32), - // For testing purpose only - #[cfg(test)] - Listeners { - core: HashMap>, - metadata: HashMap>, - nodes: HashMap>, - streams: HashMap>, - } -} - -#[derive(Debug, Clone)] -pub(super) enum EventMessage { - SetMetadataListeners { - id: GlobalId - }, - RemoveNode { - id: GlobalId - }, - SetNodePropertiesListener { - id: GlobalId - }, - SetNodeFormatListener{ - id: GlobalId - }, - SetNodeProperties { - id: GlobalId, - properties: HashMap, - }, - SetNodeFormat { - id: GlobalId, - format: AudioInfoRaw, - }, -} \ No newline at end of file diff --git a/pipewire-client/src/states.rs b/pipewire-client/src/states.rs deleted file mode 100644 index 308384805..000000000 --- a/pipewire-client/src/states.rs +++ /dev/null @@ -1,873 +0,0 @@ -use super::constants::*; -use crate::error::Error; -use crate::listeners::{Listener, ListenerControlFlow, Listeners}; -use crate::messages::StreamCallback; -use crate::utils::dict_ref_to_hashmap; -use crate::Direction; -use pipewire::spa::utils::dict::ParsableValue; -use pipewire_spa_utils::audio::raw::AudioInfoRaw; -use pipewire_spa_utils::audio::AudioChannel; -use pipewire_spa_utils::format::{MediaSubtype, MediaType}; -use std::cell::{Cell, RefCell}; -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use std::io::Cursor; -use std::rc::Rc; -use std::str::FromStr; -use std::sync::{Arc, Mutex}; -use pipewire::proxy::ProxyT; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub(super) struct GlobalId(u32); - -impl From for GlobalId { - fn from(value: String) -> Self { - u32::parse_value(value.as_str()).unwrap().into() - } -} - -impl From for GlobalId { - fn from(value: u32) -> Self { - GlobalId(value) - } -} - -impl From for GlobalId { - fn from(value: i32) -> Self { - GlobalId(value as u32) - } -} - -impl Into for GlobalId { - fn into(self) -> i32 { - self.0 as i32 - } -} - -impl From for u32 { - fn from(value: GlobalId) -> Self { - value.0 - } -} - -impl Display for GlobalId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub(super) enum GlobalObjectState { - Pending, - Initialized -} - -pub(super) struct GlobalState { - orphans: Rc>>, - clients: HashMap, - metadata: HashMap, - nodes: HashMap, - streams: HashMap, - settings: SettingsState, - default_audio_nodes: DefaultAudioNodesState, -} - -impl GlobalState { - pub fn insert_orphan(&mut self, mut state: OrphanState) { - let index = std::ptr::addr_of!(state) as usize; - let listener_orphans = self.orphans.clone(); - state.add_removed_listener( - move |control_flow| { - listener_orphans.borrow_mut().remove(&index); - control_flow.release() - } - ); - self.orphans.borrow_mut().insert(index, state); - } - - pub fn insert_client(&mut self, id: GlobalId, state: ClientState) -> Result<(), Error> { - if self.clients.contains_key(&id) { - return Err(Error { - description: format!("Client with id({}) already exists", id), - }); - } - self.clients.insert(id, state); - Ok(()) - } - - pub fn get_clients(&self) -> Result, Error> { - let clients = self.clients.iter() - .map(|(id, state)| (id, state)) - .collect::>(); - if clients.is_empty() { - return Err(Error { - description: "Zero client registered".to_string(), - }) - } - Ok(clients) - } - - pub fn insert_metadata(&mut self, id: GlobalId, state: MetadataState) -> Result<(), Error> { - if self.metadata.contains_key(&id) { - return Err(Error { - description: format!("Metadata with id({}) already exists", id), - }); - } - self.metadata.insert(id, state); - Ok(()) - } - - pub fn get_metadata(&self, id: &GlobalId) -> Result<&MetadataState, Error> { - self.metadata.get(id).ok_or(Error { - description: format!("Metadata with id({}) not found", id), - }) - } - - pub fn get_metadata_mut(&mut self, id: &GlobalId) -> Result<&mut MetadataState, Error> { - self.metadata.get_mut(id).ok_or(Error { - description: format!("Metadata with id({}) not found", id), - }) - } - - pub fn get_metadatas(&self) -> Result, Error> { - let metadatas = self.metadata.iter() - .map(|(id, state)| (id, state)) - .collect::>(); - if metadatas.is_empty() { - return Err(Error { - description: "Zero metadata registered".to_string(), - }) - } - Ok(metadatas) - } - - pub fn insert_node(&mut self, id: GlobalId, state: NodeState) -> Result<(), Error> { - if self.nodes.contains_key(&id) { - return Err(Error { - description: format!("Node with id({}) already exists", id), - }); - } - self.nodes.insert(id, state); - Ok(()) - } - - pub fn delete_node(&mut self, id: &GlobalId) -> Result<(), Error> { - if self.nodes.contains_key(id) == false { - return Err(Error { - description: format!("Node with id({}) not found", id), - }); - } - self.nodes.remove(id); - Ok(()) - } - - pub fn get_node(&self, id: &GlobalId) -> Result<&NodeState, Error> { - self.nodes.get(id).ok_or(Error { - description: format!("Node with id({}) not found", id), - }) - } - - pub fn get_node_mut(&mut self, id: &GlobalId) -> Result<&mut NodeState, Error> { - self.nodes.get_mut(id).ok_or(Error { - description: format!("Node with id({}) not found", id), - }) - } - - pub fn get_nodes(&self) -> Result, Error> { - let nodes = self.nodes.iter() - .map(|(id, state)| (id, state)) - .collect::>(); - if nodes.is_empty() { - return Err(Error { - description: "Zero node registered".to_string(), - }) - } - Ok(nodes) - } - - pub fn get_nodes_mut(&mut self) -> Result, Error> { - let nodes = self.nodes.iter_mut() - .map(|(id, state)| (id, state)) - .collect::>(); - if nodes.is_empty() { - return Err(Error { - description: "Zero node registered".to_string(), - }) - } - Ok(nodes) - } - - pub fn insert_stream(&mut self, name: String, state: StreamState) -> Result<(), Error> { - if self.streams.contains_key(&name) { - return Err(Error { - description: format!("Stream with name({}) already exists", name), - }); - } - self.streams.insert(name, state); - Ok(()) - } - - pub fn delete_stream(&mut self, name: &String) -> Result<(), Error> { - if self.streams.contains_key(name) == false { - return Err(Error { - description: format!("Stream with name({}) not found", name), - }); - } - self.streams.remove(name); - Ok(()) - } - - pub fn get_stream(&self, name: &String) -> Result<&StreamState, Error> { - self.streams.get(name).ok_or(Error { - description: format!("Stream with name({}) not found", name), - }) - } - - pub fn get_stream_mut(&mut self, name: &String) -> Result<&mut StreamState, Error> { - self.streams.get_mut(name).ok_or(Error { - description: format!("Stream with name({}) not found", name), - }) - } - - pub fn get_streams(&self) -> Result, Error> { - let streams = self.streams.iter() - .map(|(id, state)| (id, state)) - .collect::>(); - if streams.is_empty() { - return Err(Error { - description: "Zero stream registered".to_string(), - }) - } - Ok(streams) - } - - pub fn get_settings(&self) -> SettingsState { - self.settings.clone() - } - - pub fn get_default_audio_nodes(&self) -> DefaultAudioNodesState { - self.default_audio_nodes.clone() - } - - pub fn remove(&mut self, id: &GlobalId) { - self.metadata.remove(id); - self.nodes.remove(id); - } -} - -impl Default for GlobalState { - fn default() -> Self { - GlobalState { - orphans: Rc::new(RefCell::new(HashMap::new())), - clients: HashMap::new(), - metadata: HashMap::new(), - nodes: HashMap::new(), - streams: HashMap::new(), - settings: SettingsState::default(), - default_audio_nodes: DefaultAudioNodesState::default(), - } - } -} - -pub(super) struct OrphanState { - proxy: pipewire::proxy::Proxy, - listeners: Rc>> -} - -impl OrphanState { - pub fn new(proxy: pipewire::proxy::Proxy) -> Self { - Self { - proxy, - listeners: Rc::new(RefCell::new(Listeners::new())), - } - } - - pub fn add_removed_listener(&mut self, callback: F) - where - F: Fn(&mut ListenerControlFlow) + 'static - { - const LISTENER_NAME: &str = "removed"; - let listeners = self.listeners.clone(); - let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); - let listener_control_flow = control_flow.clone(); - let listener = self.proxy.add_listener_local() - .removed(move || { - if listener_control_flow.borrow().is_released() { - return; - } - callback(&mut listener_control_flow.borrow_mut()); - listeners.borrow_mut().triggered(&LISTENER_NAME.to_string()); - }) - .register(); - self.listeners.borrow_mut().add( - LISTENER_NAME.to_string(), - Listener::new(listener, control_flow) - ); - } -} - -pub(super) struct NodeState { - proxy: pipewire::node::Node, - state: GlobalObjectState, - properties: Option>, - format: Option, - listeners: Rc>> -} - -impl NodeState { - pub fn new(proxy: pipewire::node::Node) -> Self { - Self { - proxy, - state: GlobalObjectState::Pending, - properties: None, - format: None, - listeners: Rc::new(RefCell::new(Listeners::new())), - } - } - - pub(super) fn get_listener_names(&self) -> Vec { - self.listeners.borrow().get_names() - } - - pub fn state(&self) -> GlobalObjectState { - self.state.clone() - } - - fn set_state(&mut self) { - if self.properties.is_some() && self.format.is_some() { - self.state = GlobalObjectState::Initialized - } else { - self.state = GlobalObjectState::Pending - }; - } - - pub fn properties(&self) -> Option> { - self.properties.clone() - } - - pub fn set_properties(&mut self, properties: HashMap) { - if self.properties.is_none() { - self.properties = Some(HashMap::new()); - } - self.properties.as_mut().unwrap().extend(properties); - self.set_state(); - } - - pub fn format(&self) -> Option { - self.format.clone() - } - - pub fn set_format(&mut self, format: AudioInfoRaw) { - self.format = Some(format); - self.set_state(); - } - - pub fn name(&self) -> Result { - match self.properties.as_ref().unwrap().get(*pipewire::keys::NODE_NAME) { - Some(value) => Ok(value.clone()), - None => Err(Error { - description: "Node name not found in properties".to_string(), - }) - } - } - - pub fn direction(&self) -> Result { - let media_class = self.properties.as_ref().unwrap().get(*pipewire::keys::MEDIA_CLASS).unwrap().clone(); - match media_class.as_str() { - MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE => Ok(Direction::Input), - MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK => Ok(Direction::Output), - _ => Err(Error { - description: "Media class not an audio sink/source".to_string(), - }) - } - } - - fn add_info_listener(&mut self, name: String, listener: F) - where - F: Fn(&mut ListenerControlFlow, &pipewire::node::NodeInfoRef) + 'static - { - let listeners = self.listeners.clone(); - let listener_name = name.clone(); - let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); - let listener_control_flow = control_flow.clone(); - let listener = self.proxy.add_listener_local() - .info(move |info| { - if listener_control_flow.borrow().is_released() { - return; - } - listener(&mut listener_control_flow.borrow_mut(), info); - listeners.borrow_mut().triggered(&listener_name); - }) - .register(); - self.listeners.borrow_mut().add(name, Listener::new(listener, control_flow)); - } - - pub fn add_properties_listener(&mut self, callback: F) - where - F: Fn(&mut ListenerControlFlow, HashMap) + 'static, - { - self.add_info_listener( - "properties".to_string(), - move |control_flow, info| { - if info.props().is_none() { - return; - } - let properties = info.props().unwrap(); - let properties = dict_ref_to_hashmap(properties); - callback(control_flow, properties); - } - ); - } - - fn add_parameter_listener( - &mut self, - name: String, - expected_kind: pipewire::spa::param::ParamType, - listener: F - ) - where - F: Fn(&mut ListenerControlFlow, &pipewire::spa::pod::Pod) + 'static, - { - let listeners = self.listeners.clone(); - let listener_name = name.clone(); - let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); - let listener_control_flow = control_flow.clone(); - self.proxy.subscribe_params(&[expected_kind]); - let listener = self.proxy.add_listener_local() - // parameters: seq, kind, id, next_id, parameter - .param(move |_, kind, _, _, parameter| { - if listener_control_flow.borrow().is_released() { - return; - } - if kind != expected_kind { - return; - } - let Some(parameter) = parameter else { - return; - }; - listener(&mut listener_control_flow.borrow_mut(), parameter); - listeners.borrow_mut().triggered(&listener_name); - }) - .register(); - self.listeners.borrow_mut().add(name, Listener::new(listener, control_flow)); - } - - pub fn add_format_listener(&mut self, callback: F) - where - F: Fn(&mut ListenerControlFlow, Result) + 'static, - { - self.add_parameter_listener( - "format".to_string(), - pipewire::spa::param::ParamType::EnumFormat, - move |control_flow, parameter| { - let (media_type, media_subtype): (MediaType, MediaSubtype) = - match pipewire::spa::param::format_utils::parse_format(parameter) { - Ok((media_type, media_subtype)) => (media_type.0.into(), media_subtype.0.into()), - Err(_) => return, - }; - let pod = parameter; - let data = pod.as_bytes(); - let parameter = match media_type { - MediaType::Audio => match media_subtype { - MediaSubtype::Raw => { - let result = pipewire::spa::pod::deserialize::PodDeserializer::deserialize_from(data); - let result = result - .map(move |(_, parameter)| { - parameter - }) - .map_err(move |error| { - let description = match error { - pipewire::spa::pod::deserialize::DeserializeError::Nom(_) => "Parsing error", - pipewire::spa::pod::deserialize::DeserializeError::UnsupportedType => "Unsupported type", - pipewire::spa::pod::deserialize::DeserializeError::InvalidType => "Invalid type", - pipewire::spa::pod::deserialize::DeserializeError::PropertyMissing => "Property missing", - pipewire::spa::pod::deserialize::DeserializeError::PropertyWrongKey(value) => &*format!( - "Wrong property key({})", - value - ), - pipewire::spa::pod::deserialize::DeserializeError::InvalidChoiceType => "Invalide choice type", - pipewire::spa::pod::deserialize::DeserializeError::MissingChoiceValues => "Missing choice values", - }; - Error { - description: format!( - "Failed POD deserialization for type(AudioInfoRaw): {}", - description - ), - } - }); - result - } - _ => return - }, - _ => return - }; - callback(control_flow, parameter); - } - ); - } -} - -pub(super) struct ClientState { - pub(super) name: String -} - -impl ClientState { - pub fn new(name: String) -> Self { - Self { - name, - } - } -} - -pub(super) struct MetadataState { - proxy: pipewire::metadata::Metadata, - pub(super) state: Rc>, - pub(super) name: String, - listeners: Rc>>, -} - -impl MetadataState { - pub fn new(proxy: pipewire::metadata::Metadata, name: String) -> Self { - Self { - proxy, - name, - state: Rc::new(RefCell::new(GlobalObjectState::Pending)), - listeners: Rc::new(RefCell::new(Listeners::new())), - } - } - - pub(super) fn get_listener_names(&self) -> Vec { - self.listeners.borrow().get_names() - } - - pub fn add_property_listener(&mut self, listener: F) - where - F: Fn(&mut ListenerControlFlow, u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + Sized + 'static - { - const LISTENER_NAME: &str = "property"; - let listeners = self.listeners.clone(); - let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); - let listener_control_flow = control_flow.clone(); - let listener = self.proxy.add_listener_local() - .property(move |subject , key, kind, value| { - if listener_control_flow.borrow().is_released() { - return 0; - } - let result = listener( - &mut listener_control_flow.borrow_mut(), - subject, - key, - kind, - value - ); - listeners.borrow_mut().triggered(&LISTENER_NAME.to_string()); - result - }) - .register(); - self.listeners.borrow_mut().add( - LISTENER_NAME.to_string(), - Listener::new(listener, control_flow) - ); - } -} - -#[derive(Debug, Default)] -pub(super) struct StreamUserData {} - -pub(super) struct StreamState { - proxy: pipewire::stream::Stream, - pub(super) name: String, - is_connected: bool, - format: pipewire::spa::param::audio::AudioInfoRaw, - direction: pipewire::spa::utils::Direction, - listeners: Rc>>>, -} - -impl StreamState { - pub fn new( - name: String, - format: pipewire::spa::param::audio::AudioInfoRaw, - direction: pipewire::spa::utils::Direction, - proxy: pipewire::stream::Stream - ) -> Self { - Self { - name, - proxy, - is_connected: false, - format, - direction, - listeners: Rc::new(RefCell::new(Listeners::new())), - } - } - - pub(super) fn get_listener_names(&self) -> Vec { - self.listeners.borrow().get_names() - } - - pub fn is_connected(&self) -> bool { - self.is_connected - } - - pub fn connect(&mut self) -> Result<(), Error> { - if self.is_connected { - return Err(Error { - description: format!("Stream {} is already connected", self.name) - }); - } - let object = pipewire::spa::pod::Value::Object(pipewire::spa::pod::Object { - type_: pipewire::spa::sys::SPA_TYPE_OBJECT_Format, - id: pipewire::spa::sys::SPA_PARAM_EnumFormat, - properties: self.format.into(), - }); - let values: Vec = pipewire::spa::pod::serialize::PodSerializer::serialize( - Cursor::new(Vec::new()), - &object, - ) - .unwrap() - .0 - .into_inner(); - let mut params = [pipewire::spa::pod::Pod::from_bytes(&values).unwrap()]; - let flags = pipewire::stream::StreamFlags::AUTOCONNECT | pipewire::stream::StreamFlags::MAP_BUFFERS; - self.proxy - .connect( - self.direction, - None, - flags, - &mut params, - ) - .map_err(move |error| Error { description: error.to_string() })?; - self.is_connected = true; - Ok(()) - } - - pub fn disconnect(&mut self) -> Result<(), Error> { - if self.is_connected == false { - return Err(Error { - description: format!("Stream {} is not connected", self.name) - }); - } - self.proxy - .disconnect() - .map_err(move |error| Error { description: error.to_string() })?; - self.is_connected = false; - Ok(()) - } - - pub fn add_process_listener( - &mut self, - mut callback: StreamCallback - ) - { - const LISTENER_NAME: &str = "process"; - let listeners = self.listeners.clone(); - let control_flow = Rc::new(RefCell::new(ListenerControlFlow::new())); - let listener_control_flow = control_flow.clone(); - let listener = self.proxy.add_local_listener() - .process(move |stream, _| { - if listener_control_flow.borrow().is_released() { - return; - } - let buffer = stream.dequeue_buffer().unwrap(); - callback.call(&mut listener_control_flow.borrow_mut(), buffer); - listeners.borrow_mut().triggered(&LISTENER_NAME.to_string()); - }) - .register() - .unwrap(); - self.listeners.borrow_mut().add(LISTENER_NAME.to_string(), Listener::new(listener, control_flow)); - } -} - -pub(super) struct PortStateProperties { - path: String, - channel: AudioChannel, - id: GlobalId, - name: String, - direction: Direction, - alias: String, - group: String, -} - -impl From<&pipewire::spa::utils::dict::DictRef> for PortStateProperties { - fn from(value: &pipewire::spa::utils::dict::DictRef) -> Self { - let properties = dict_ref_to_hashmap(value); - let path = properties.get("object.path").unwrap().to_string(); - let channel = properties.get("audio.channel").unwrap().to_string(); - let id = properties.get("port.id").unwrap().to_string(); - let name = properties.get("port.name").unwrap().to_string(); - let direction = properties.get("port.direction").unwrap().to_string(); - let alias = properties.get("port.alias").unwrap().to_string(); - let group = properties.get("port.group").unwrap().to_string(); - Self { - path, - channel: AudioChannel::UNKNOWN, - id: id.into(), - name, - direction: match direction.as_str() { - "in" => Direction::Input, - "out" => Direction::Output, - &_ => panic!("Cannot determine direction: {}", direction.as_str()), - }, - alias, - group, - } - } -} - -pub(super) struct PortState { - proxy: pipewire::link::Link, - properties: Rc>>, - pub(super) state: Rc>, - listeners: Rc>>>, -} - -// impl PortState { -// pub fn new(proxy: pipewire::port::Port) { -// proxy.add_listener_local().info(move |x| { -// x. -// }) -// .param(move |subject , key, kind, value| { -// -// }) -// } -// } - -pub(super) struct LinkState { - proxy: pipewire::link::Link, - input_node_id: GlobalId, - input_port_id: GlobalId, - output_node_id: GlobalId, - output_port_id: GlobalId, - pub(super) state: Rc>, - listeners: Rc>>>, -} - -#[derive(Debug, Clone)] -pub struct SettingsState { - pub(super) state: GlobalObjectState, - pub allowed_sample_rates: Vec, - pub sample_rate: u32, - pub min_buffer_size: u32, - pub max_buffer_size: u32, - pub default_buffer_size: u32, -} - -impl Default for SettingsState { - fn default() -> Self { - Self { - state: GlobalObjectState::Pending, - allowed_sample_rates: vec![], - sample_rate: 0, - min_buffer_size: 0, - max_buffer_size: 0, - default_buffer_size: 0, - } - } -} - -impl SettingsState { - pub(super) fn listener(state: Arc>) -> impl Fn(&mut ListenerControlFlow, u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static - { - const EXPECTED_PROPERTY: u32 = 5; - let property_count: Rc> = Rc::new(Cell::new(0)); - move |control_flow, _, key, _, value| { - let settings = &mut state.lock().unwrap().settings; - let key = key.unwrap(); - let value = value.unwrap(); - match key { - CLOCK_RATE_PROPERTY_KEY => { - settings.sample_rate = u32::from_str(value).unwrap(); - property_count.set(property_count.get() + 1); - }, - CLOCK_QUANTUM_PROPERTY_KEY => { - settings.default_buffer_size = u32::from_str(value).unwrap(); - property_count.set(property_count.get() + 1); - } - CLOCK_QUANTUM_MIN_PROPERTY_KEY => { - settings.min_buffer_size = u32::from_str(value).unwrap(); - property_count.set(property_count.get() + 1); - } - CLOCK_QUANTUM_MAX_PROPERTY_KEY => { - settings.max_buffer_size = u32::from_str(value).unwrap(); - property_count.set(property_count.get() + 1); - } - CLOCK_ALLOWED_RATES_PROPERTY_KEY => { - let rates: Result, _> = value[2..value.len() - 2] - .split_whitespace() - .map(|x| x.parse::()) - .collect(); - settings.allowed_sample_rates = rates.unwrap(); - property_count.set(property_count.get() + 1); - } - &_ => {} - }; - if let (GlobalObjectState::Pending, EXPECTED_PROPERTY) = (settings.state.clone(), property_count.get()) { - settings.state = GlobalObjectState::Initialized; - control_flow.release(); - } - 0 - } - } -} - -#[derive(Debug, Clone)] -pub struct DefaultAudioNodesState { - pub(super) state: GlobalObjectState, - pub source: String, - pub sink: String, -} - -impl Default for DefaultAudioNodesState { - fn default() -> Self { - Self { - state: GlobalObjectState::Pending, - source: "".to_string(), - sink: "".to_string(), - } - } -} - -impl DefaultAudioNodesState { - pub(super) fn listener(state: Arc>) -> impl Fn(&mut ListenerControlFlow, u32, Option<&str>, Option<&str>, Option<&str>) -> i32 + 'static - { - const EXPECTED_PROPERTY: u32 = 2; - let property_count: Rc> = Rc::new(Cell::new(0)); - move |control_flow, _, key, _, value| { - let default_audio_devices = &mut state.lock().unwrap().default_audio_nodes; - let key = key.unwrap(); - if value.is_none() { - return 0; - } - let value = value.unwrap(); - match key { - DEFAULT_AUDIO_SINK_PROPERTY_KEY => { - let value: serde_json::Value = serde_json::from_str(value).unwrap(); - default_audio_devices.sink = value.as_object() - .unwrap() - .get("name") - .unwrap() - .as_str() - .unwrap() - .to_string(); - property_count.set(property_count.get() + 1); - }, - DEFAULT_AUDIO_SOURCE_PROPERTY_KEY => { - let value: serde_json::Value = serde_json::from_str(value).unwrap(); - default_audio_devices.source = value.as_object() - .unwrap() - .get("name") - .unwrap() - .as_str() - .unwrap() - .to_string(); - property_count.set(property_count.get() + 1); - }, - &_ => {} - }; - if let (GlobalObjectState::Pending, EXPECTED_PROPERTY) = (default_audio_devices.state.clone(), property_count.get()) { - default_audio_devices.state = GlobalObjectState::Initialized; - control_flow.release(); - } - 0 - } - } -} \ No newline at end of file diff --git a/pipewire-client/src/test_utils/api.rs b/pipewire-client/src/test_utils/api.rs deleted file mode 100644 index 440d81310..000000000 --- a/pipewire-client/src/test_utils/api.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::client::CoreApi; -use crate::error::Error; -use crate::messages::{MessageRequest, MessageResponse}; -use crate::states::{MetadataState, NodeState, StreamState}; -use std::any::TypeId; -use std::collections::HashMap; -use crate::listeners::PipewireCoreSync; - -impl CoreApi { - pub(crate) fn get_listeners(&self) -> Result>>, Error> { - let request = MessageRequest::Listeners; - let response = self.api.send_request(&request); - match response { - Ok(MessageResponse::Listeners { - core, - metadata, - nodes, - streams - }) => { - let mut map = HashMap::new(); - map.insert(TypeId::of::(), core); - map.insert(TypeId::of::(), metadata); - map.insert(TypeId::of::(), nodes); - map.insert(TypeId::of::(), streams); - Ok(map) - }, - Err(value) => Err(value), - Ok(value) => Err(Error { - description: format!("Received unexpected response: {:?}", value), - }), - } - } -} \ No newline at end of file diff --git a/pipewire-client/src/test_utils/fixtures.rs b/pipewire-client/src/test_utils/fixtures.rs deleted file mode 100644 index 53ad16d59..000000000 --- a/pipewire-client/src/test_utils/fixtures.rs +++ /dev/null @@ -1,398 +0,0 @@ -use std::any::TypeId; -use std::collections::hash_map::Iter; -use std::collections::HashMap; -use crate::{NodeInfo, PipewireClient}; -use pipewire_common::utils::Direction; -use pipewire_test_utils::server::{server_with_default_configuration, server_without_node, server_without_session_manager, Server}; -use rstest::{fixture, Context}; -use std::fmt::{Display, Formatter}; -use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, LazyLock, Mutex, OnceLock}; -use std::{mem, thread}; -use std::ptr::drop_in_place; -use std::time::Duration; -use ctor::{ctor, dtor}; -use libc::{atexit, signal, SIGINT, SIGSEGV, SIGTERM}; -use tokio::runtime::Runtime; -use uuid::Uuid; -use pipewire_common::error::Error; -use pipewire_test_utils::environment::{SHARED_SERVER, TEST_ENVIRONMENT}; -use crate::states::StreamState; - -pub struct NodeInfoFixture { - client: Arc, - node: OnceLock, - direction: Direction -} - -impl NodeInfoFixture { - pub(self) fn new(client: Arc, direction: Direction) -> Self { - Self { - client, - node: OnceLock::new(), - direction, - } - } - - pub fn client(&self) -> Arc { - self.client.clone() - } -} - -impl Deref for NodeInfoFixture { - type Target = NodeInfo; - - fn deref(&self) -> &Self::Target { - let node = self.node.get_or_init(|| { - let node_name = Uuid::new_v4().to_string(); - self.client.node() - .create( - node_name.clone(), - node_name.clone(), - node_name.clone(), - self.direction.clone(), - 2 - ).unwrap(); - let node = self.client.node().get(node_name, self.direction.clone()).unwrap(); - node - }); - node - } -} - -impl Drop for NodeInfoFixture { - fn drop(&mut self) { - self.client.node() - .delete(self.node.get().unwrap().id) - .unwrap() - } -} - -pub struct StreamFixture { - client: Arc, - node: NodeInfoFixture, - stream: OnceLock, - direction: Direction -} - -impl StreamFixture { - pub(self) fn new(client: Arc, node: NodeInfoFixture) -> Self { - let direction = node.direction.clone(); - Self { - client: client.clone(), - node, - stream: OnceLock::new(), - direction, - } - } - - pub fn client(&self) -> Arc { - self.client.clone() - } - - pub fn name(&self) -> String { - self.deref().clone() - } - - pub fn listeners(&self) -> HashMap> { - let listeners = self.client.core().get_listeners().unwrap(); - let stream_listeners = listeners.get(&TypeId::of::()).unwrap(); - stream_listeners.clone() - } - - pub fn connect(&self) -> Result<(), Error> { - let stream = self.deref().clone(); - self.client.stream().connect(stream) - } - - pub fn disconnect(&self) -> Result<(), Error> { - let stream = self.deref().clone(); - self.client.stream().disconnect(stream) - } - - pub fn delete(&self) -> Result<(), Error> { - let stream = self.deref().clone(); - self.client.stream().delete(stream) - } -} - -impl Display for StreamFixture { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.deref().clone()) - } -} - -impl Deref for StreamFixture { - type Target = String; - - fn deref(&self) -> &Self::Target { - let stream = self.stream.get_or_init(|| { - self.client.stream() - .create( - self.node.id, - self.direction.clone(), - self.node.format.clone().into(), - move |control_flow, _| { - assert!(true); - control_flow.release(); - } - ).unwrap() - }); - stream - } -} - -impl Drop for StreamFixture { - fn drop(&mut self) { - let stream = self.stream.get().unwrap().clone(); - let result = self.client.stream().delete(stream.clone()); - match result { - Ok(_) => {} - Err(value) => { - let error_message = format!( - "Stream with name({}) not found", - self.stream.get().unwrap().clone() - ); - if error_message != value.description { - panic!("{}", error_message); - } - // If error is raised, we can assume this stream had been deleted. - // Certainly due to delete tests, we cannot be sure at this point but let just - // show a warning for now. - eprintln!( - "Failed to delete stream: {}. Stream delete occurred during test method ?", - self.stream.get().unwrap() - ); - } - } - } -} - -pub struct ConnectedStreamFixture { - client: Arc, - stream: StreamFixture, -} - -impl ConnectedStreamFixture { - pub(self) fn new(client: Arc, stream: StreamFixture) -> Self { - stream.connect().unwrap(); - Self { - client, - stream, - } - } - - pub fn disconnect(&self) -> Result<(), Error> { - let stream = self.stream.deref().clone(); - self.client.stream().disconnect(stream) - } -} - -impl Display for ConnectedStreamFixture { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.stream.fmt(f) - } -} - -impl Deref for ConnectedStreamFixture { - type Target = StreamFixture; - - fn deref(&self) -> &Self::Target { - &self.stream - } -} - -impl Drop for ConnectedStreamFixture { - fn drop(&mut self) { - let stream = self.stream.deref().clone(); - let result = self.client.stream().disconnect(stream.clone()); - match result { - Ok(_) => {} - Err(value) => { - let error_message = format!( - "Stream {} is not connected", - stream.clone() - ); - if error_message != value.description { - panic!("{}", error_message); - } - // If error is raised, we can assume this stream had been disconnected. - // Certainly due to disconnect tests, we cannot be sure at this point but let just - // show a warning for now. - eprintln!( - "Failed to disconnect stream: {}. Stream disconnect occurred during test method ?", - stream.clone() - ); - } - } - } -} - -pub struct PipewireTestClient { - name: String, - server: Arc, - client: Arc, -} - -impl PipewireTestClient { - pub(self) fn new( - name: String, - server: Arc, - client: PipewireClient - ) -> Self { - let client = Arc::new(client); - println!("Create {} client: {}", name.clone(), Arc::strong_count(&client)); - Self { - name, - server, - client: client.clone(), - } - } - - pub(self) fn reference_count(&self) -> usize { - Arc::strong_count(&self.client) - } - - pub(self) fn create_input_node(&self) -> NodeInfoFixture { - NodeInfoFixture::new(self.client.clone(), Direction::Input) - } - - pub(self) fn create_output_node(&self) -> NodeInfoFixture { - NodeInfoFixture::new(self.client.clone(), Direction::Output) - } - - pub(self) unsafe fn cleanup(&mut self) { - let pointer = std::ptr::addr_of_mut!(self.client); - let reference_count = Arc::strong_count(&self.client); - for _ in 0..reference_count { - drop_in_place(pointer); - } - } -} - -impl Clone for PipewireTestClient { - fn clone(&self) -> Self { - let client = Self { - name: self.name.clone(), - server: self.server.clone(), - client: self.client.clone(), - }; - println!("Clone {} client: {}", self.name.clone(), self.reference_count()); - client - } -} - -impl Deref for PipewireTestClient { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.client - } -} - -impl Drop for PipewireTestClient { - fn drop(&mut self) { - println!("Drop {} client: {}", self.name.clone(), self.reference_count() - 1); - } -} - -#[ctor] -static SHARED_CLIENT: Arc> = { - unsafe { libc::printf("Initialize shared client\n\0".as_ptr() as *const i8); }; - let server = SHARED_SERVER.clone(); - let client = PipewireTestClient::new( - "shared".to_string(), - server, - PipewireClient::new( - Arc::new(Runtime::new().unwrap()), - TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), - ).unwrap(), - ); - Arc::new(Mutex::new(client)) -}; - -#[dtor] -unsafe fn cleanup_shared_client() { - libc::printf("Cleaning shared client\n\0".as_ptr() as *const i8); - SHARED_CLIENT.lock().unwrap().cleanup(); -} - -#[fixture] -pub fn isolated_client() -> PipewireTestClient { - let server = SHARED_SERVER.clone(); - let client = PipewireTestClient::new( - "isolated".to_string(), - server, - PipewireClient::new( - Arc::new(Runtime::new().unwrap()), - TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), - ).unwrap(), - ); - client -} - -#[fixture] -pub fn shared_client() -> PipewireTestClient { - // Its seems that shared client, for some reason, raise - // timeout error during init phase and create node object phase. - // Give a bit of space between tests seem to mitigate that issue. - thread::sleep(Duration::from_millis(10)); - let client = SHARED_CLIENT.lock().unwrap().clone(); - client -} - -#[fixture] -pub fn client2(server_with_default_configuration: Arc) -> (PipewireTestClient, PipewireTestClient) { - let server = server_with_default_configuration.clone(); - let runtime = Arc::new(Runtime::new().unwrap()); - let client_1 = PipewireClient::new( - runtime.clone(), - TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), - ).unwrap(); - let client_2 = PipewireClient::new( - runtime.clone(), - TEST_ENVIRONMENT.lock().unwrap().client_timeout.clone(), - ).unwrap(); - ( - PipewireTestClient::new( - "isolated_client_1".to_string(), - server.clone(), - client_1, - ), - PipewireTestClient::new( - "isolated_client_2".to_string(), - server.clone(), - client_2, - ) - ) -} - -#[fixture] -pub fn input_node(shared_client: PipewireTestClient) -> NodeInfoFixture { - shared_client.create_input_node() -} - -#[fixture] -pub fn output_node(shared_client: PipewireTestClient) -> NodeInfoFixture { - shared_client.create_output_node() -} - -#[fixture] -pub fn input_stream(input_node: NodeInfoFixture) -> StreamFixture { - StreamFixture::new(input_node.client.clone(), input_node) -} - -#[fixture] -pub fn output_stream(output_node: NodeInfoFixture) -> StreamFixture { - StreamFixture::new(output_node.client.clone(), output_node) -} - -#[fixture] -pub fn input_connected_stream(input_stream: StreamFixture) -> ConnectedStreamFixture { - ConnectedStreamFixture::new(input_stream.client.clone(), input_stream) -} - -#[fixture] -pub fn output_connected_stream(output_stream: StreamFixture) -> ConnectedStreamFixture { - ConnectedStreamFixture::new(output_stream.client.clone(), output_stream) -} \ No newline at end of file diff --git a/pipewire-client/src/test_utils/mod.rs b/pipewire-client/src/test_utils/mod.rs deleted file mode 100644 index 147b5c7da..000000000 --- a/pipewire-client/src/test_utils/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod fixtures; -mod api; \ No newline at end of file diff --git a/pipewire-common/Cargo.toml b/pipewire-common/Cargo.toml deleted file mode 100644 index 3a020f601..000000000 --- a/pipewire-common/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "pipewire-common" -version = "0.1.0" -edition = "2021" -authors = ["Alexis Bekhdadi "] -description = "PipeWire Common" -repository = "https://github.com/RustAudio/cpal/" -documentation = "" -license = "Apache-2.0" -keywords = ["pipewire", "common"] - -[dependencies] -pipewire = "0.8" \ No newline at end of file diff --git a/pipewire-common/src/constants.rs b/pipewire-common/src/constants.rs deleted file mode 100644 index 256358aaa..000000000 --- a/pipewire-common/src/constants.rs +++ /dev/null @@ -1,34 +0,0 @@ -pub const PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY: &str = "PIPEWIRE_RUNTIME_DIR"; -pub const PIPEWIRE_CORE_ENVIRONMENT_KEY: &str = "PIPEWIRE_CORE"; -pub const PIPEWIRE_REMOTE_ENVIRONMENT_KEY: &str = "PIPEWIRE_REMOTE"; -pub const XDG_RUNTIME_DIR_ENVIRONMENT_KEY: &str = "XDG_RUNTIME_DIR"; -pub const PULSE_RUNTIME_PATH_ENVIRONMENT_KEY: &str = "PULSE_RUNTIME_PATH"; -pub const PIPEWIRE_REMOTE_ENVIRONMENT_DEFAULT: &str = "pipewire-0"; - -pub const PIPEWIRE_CORE_SYNC_INITIALIZATION_SEQ :u32 = 0; -pub const PIPEWIRE_CORE_SYNC_CREATE_DEVICE_SEQ :u32 = 1; - -pub const MEDIA_TYPE_PROPERTY_VALUE_AUDIO: &str = "Audio"; -pub const MEDIA_CLASS_PROPERTY_KEY: &str = "media.class"; -pub const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SOURCE: &str = "Audio/Source"; -pub const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_SINK: &str = "Audio/Sink"; -pub const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_DUPLEX: &str = "Audio/Duplex"; -pub const MEDIA_CLASS_PROPERTY_VALUE_AUDIO_DEVICE: &str = "Audio/Device"; -pub const MEDIA_CLASS_PROPERTY_VALUE_STREAM_OUTPUT_AUDIO: &str = "Stream/Output/Audio"; -pub const MEDIA_CLASS_PROPERTY_VALUE_STREAM_INPUT_AUDIO: &str = "Stream/Input/Audio"; -pub const METADATA_NAME_PROPERTY_KEY: &str = "metadata.name"; -pub const METADATA_NAME_PROPERTY_VALUE_SETTINGS: &str = "settings"; -pub const METADATA_NAME_PROPERTY_VALUE_DEFAULT: &str = "default"; -pub const CLOCK_RATE_PROPERTY_KEY: &str = "clock.rate"; -pub const CLOCK_QUANTUM_PROPERTY_KEY: &str = "clock.quantum"; -pub const CLOCK_QUANTUM_MIN_PROPERTY_KEY: &str = "clock.min-quantum"; -pub const CLOCK_QUANTUM_MAX_PROPERTY_KEY: &str = "clock.max-quantum"; -pub const CLOCK_ALLOWED_RATES_PROPERTY_KEY: &str = "clock.allowed-rates"; -pub const MONITOR_CHANNEL_VOLUMES_PROPERTY_KEY: &str = "monitor.channel-volumes"; -pub const MONITOR_PASSTHROUGH_PROPERTY_KEY: &str = "monitor.passthrough"; -pub const DEFAULT_AUDIO_SINK_PROPERTY_KEY: &str = "default.audio.sink"; -pub const DEFAULT_AUDIO_SOURCE_PROPERTY_KEY: &str = "default.audio.source"; -pub const AUDIO_POSITION_PROPERTY_KEY: &str = "audio.position"; -pub const APPLICATION_NAME_PROPERTY_KEY: &str = "application.name"; -pub const APPLICATION_NAME_PROPERTY_VALUE_WIRE_PLUMBER: &str = "WirePlumber"; -pub const APPLICATION_NAME_PROPERTY_VALUE_PIPEWIRE_MEDIA_SESSION: &str = "pipewire-media-session"; \ No newline at end of file diff --git a/pipewire-common/src/error.rs b/pipewire-common/src/error.rs deleted file mode 100644 index 05e44d5d5..000000000 --- a/pipewire-common/src/error.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::error::Error as StdError; -use std::fmt::{Display, Formatter}; - -#[derive(Debug, Clone)] -pub struct Error { - pub description: String, -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.description) - } -} - -impl StdError for Error {} \ No newline at end of file diff --git a/pipewire-common/src/lib.rs b/pipewire-common/src/lib.rs deleted file mode 100644 index fc3f64bfa..000000000 --- a/pipewire-common/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod constants; -pub mod error; -pub mod macros; -pub mod utils; diff --git a/pipewire-common/src/macros.rs b/pipewire-common/src/macros.rs deleted file mode 100644 index 131375383..000000000 --- a/pipewire-common/src/macros.rs +++ /dev/null @@ -1,85 +0,0 @@ -#[macro_export] -macro_rules! impl_callback { - ( - $t:tt => $r:ty, - $name:ident, - $( $k:ident : $v:ty ),* - ) => { - pub(super) struct $name { - callback: Arc $r + Sync + Send + 'static>>> - } - - impl From for $name - where - F: $t($($v),*) -> $r + Sync + Send + 'static - { - fn from(value: F) -> Self { - Self { callback: Arc::new(Mutex::new(Box::new(value))) } - } - } - - impl $name - { - pub fn call(&self, $($k: $v),*) -> $r - { - let callback = self.callback.lock().unwrap(); - callback($($k),*) - } - } - - impl Debug for $name { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("$name").finish() - } - } - - impl Clone for $name { - fn clone(&self) -> Self { - Self { callback: self.callback.clone() } - } - } - } -} - -#[macro_export] -macro_rules! impl_callback_generic { - ( - $t:tt => $r:ty, - $name:ident<$( $p:ident $(: $clt:lifetime)? ),*>, - $($k:ident : $v:ty),* - ) => { - pub(super) struct $name<$($p $(: $clt )? ),*> { - callback: Arc $r + Sync + Send + 'static>>> - } - - impl <$($p $(: $clt )? ),*, F> From for $name<$($p),*> - where - F: $t($($v),*) -> $r + Sync + Send + 'static - { - fn from(value: F) -> Self { - Self { callback: Arc::new(Mutex::new(Box::new(value))) } - } - } - - impl <$($p $(: $clt )? ),*> $name<$($p),*> - { - pub fn call(&self, $($k: $v),*) -> $r - { - let callback = self.callback.lock().unwrap(); - callback($($k),*) - } - } - - impl <$($p $(: $clt )? ),*> Debug for $name<$($p),*> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("$name").finish() - } - } - - impl <$($p $(: $clt )? ),*> Clone for $name<$($p),*> { - fn clone(&self) -> Self { - Self { callback: self.callback.clone() } - } - } - } -} \ No newline at end of file diff --git a/pipewire-common/src/utils.rs b/pipewire-common/src/utils.rs deleted file mode 100644 index e860ffdfa..000000000 --- a/pipewire-common/src/utils.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::error::Error; -use std::collections::HashMap; - -#[derive(Debug, Clone, PartialEq)] -pub enum Direction { - Input, - Output, -} - -impl From for pipewire::spa::utils::Direction { - fn from(value: Direction) -> Self { - match value { - Direction::Input => pipewire::spa::utils::Direction::Input, - Direction::Output => pipewire::spa::utils::Direction::Output, - } - } -} - -pub fn dict_ref_to_hashmap(dict: &pipewire::spa::utils::dict::DictRef) -> HashMap { - dict - .iter() - .map(move |(k, v)| { - let k = String::from(k).clone(); - let v = String::from(v).clone(); - (k, v) - }) - .collect::>() -} - -pub fn debug_dict_ref(dict: &pipewire::spa::utils::dict::DictRef) { - for (key, value) in dict.iter() { - println!("{} => {}", key ,value); - } - println!("\n"); -} - - - -pub struct Backoff { - attempts: u32, - maximum_attempts: u32, - wait_duration: std::time::Duration, - initial_wait_duration: std::time::Duration, - maximum_wait_duration: std::time::Duration, -} - -impl Default for Backoff { - fn default() -> Self { - Self::new( - 300, // 300 attempts * 100ms = 30s - std::time::Duration::from_millis(100), - std::time::Duration::from_millis(100) - ) - } -} - -impl Backoff { - pub fn constant(milliseconds: u128) -> Self { - let attempts = milliseconds / 100; - Self::new( - attempts as u32, - std::time::Duration::from_millis(100), - std::time::Duration::from_millis(100) - ) - } -} - -impl Backoff { - pub fn new( - maximum_attempts: u32, - initial_wait_duration: std::time::Duration, - maximum_wait_duration: std::time::Duration - ) -> Self { - Self { - attempts: 0, - maximum_attempts, - wait_duration: initial_wait_duration, - initial_wait_duration, - maximum_wait_duration, - } - } - - pub fn reset(&mut self) { - self.attempts = 0; - self.wait_duration = self.initial_wait_duration; - } - - pub fn retry(&mut self, mut operation: F) -> Result - where - F: FnMut() -> Result, - E: std::error::Error - { - self.reset(); - loop { - let error = match operation() { - Ok(value) => return Ok(value), - Err(value) => value - }; - std::thread::sleep(self.wait_duration); - self.wait_duration = self.maximum_wait_duration.min(self.wait_duration * 2); - self.attempts += 1; - if self.attempts < self.maximum_attempts { - continue; - } - return Err(Error { - description: format!("Backoff timeout: {}", error.to_string()), - }) - } - } -} \ No newline at end of file diff --git a/pipewire-spa-utils/Cargo.toml b/pipewire-spa-utils/Cargo.toml deleted file mode 100644 index c358d667b..000000000 --- a/pipewire-spa-utils/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "pipewire-spa-utils" -version = "0.1.0" -authors = ["Alexis Bekhdadi "] -description = "PipeWire SPA Utils" -repository = "https://github.com/RustAudio/cpal/" -documentation = "" -license = "Apache-2.0" -keywords = ["pipewire", "spa", "utils"] -build = "build.rs" - -[build-dependencies] -cargo = "0.84" -cargo_metadata = "0.19" -libspa = "0.8" -syn = "2.0" -quote = "1.0" -prettyplease = "0.2" -itertools = "0.14" -indexmap = "2.7" - -[dependencies] -libspa = { version = "0.8" } - -[features] -v0_3_33 = [] -v0_3_40 = ["v0_3_33", "libspa/v0_3_33"] -v0_3_65 = ["v0_3_40", "libspa/v0_3_65"] -v0_3_75 = ["v0_3_65", "libspa/v0_3_75"] - - diff --git a/pipewire-spa-utils/build.rs b/pipewire-spa-utils/build.rs deleted file mode 100644 index 7c385f63d..000000000 --- a/pipewire-spa-utils/build.rs +++ /dev/null @@ -1,18 +0,0 @@ -extern crate cargo; -extern crate cargo_metadata; -extern crate indexmap; -extern crate itertools; -extern crate quote; -extern crate syn; - -mod build_modules; - -use build_modules::format; -use build_modules::utils::map_package_info; - - -fn main() { - let package = map_package_info(); - format::generate_enums(&package.src_path, &package.build_path, &package.features); -} - diff --git a/pipewire-spa-utils/build_modules/format/mod.rs b/pipewire-spa-utils/build_modules/format/mod.rs deleted file mode 100644 index 99688cad5..000000000 --- a/pipewire-spa-utils/build_modules/format/mod.rs +++ /dev/null @@ -1,445 +0,0 @@ -use build_modules::syntax::generators::enumerator::{EnumInfo, EnumVariantInfo}; -use build_modules::syntax::parsers::{StructImplVisitor, StructVisitor}; -use build_modules::syntax::utils::AttributeExt; -use build_modules::utils::read_source_file; -use indexmap::IndexMap; -use itertools::Itertools; -use std::cmp::Ordering; -use std::path::PathBuf; -use syn::__private::quote::__private::ext::RepToTokensExt; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::token::PathSep; -use syn::{Attribute, Expr, Fields, Ident, ImplItemConst, Item, ItemConst, PathSegment, Type}; -use debug; - -#[derive(Debug, Clone)] -struct StructInfo { - ident: Ident, - unnamed_field_ident: Ident, -} - -#[derive(Debug, Clone)] -struct StructImplInfo { - attributes: Vec, - constants: Vec -} - -pub fn generate_enums(src_path: &PathBuf, build_path: &PathBuf, features: &Vec) { - let file_path = PathBuf::from(&"param/format.rs"); - let src = read_source_file(&src_path, &file_path); - - let media_type_enum_info = map_media_type_enum_info(&src.items); - let media_subtype_enum_info = map_media_subtype_enum_info( - &src.items, - move |constant | { - if features.is_empty() { - constant.attrs.contains(&"feature".to_string()) == false - } - else { - features.iter().any(|feature| { - constant.attrs.contains(feature) - }) == false - } - } - ); - - let enum_infos = vec![ - media_type_enum_info, - media_subtype_enum_info - ]; - - generate_enums_code(enum_infos, "format.rs"); - - let file_path = PathBuf::from(&"bindings.rs"); - let src = read_source_file(&build_path, &file_path); - - let audio_sample_format_enum_info = map_audio_sample_format_enum_info(&src.items); - let audio_channel_enum_info = map_audio_channel_enum_info(&src.items); - - let enum_infos = vec![ - audio_sample_format_enum_info, - audio_channel_enum_info - ]; - - generate_enums_code(enum_infos, "audio.rs"); -} - -fn map_media_type_enum_info(items: &Vec) -> EnumInfo { - const IDENT: &str = "MediaType"; - - let filter = move |ident: String| ident == IDENT; - let struct_info = map_struct_info(&items, filter); - let struct_impl_info = map_struct_impl_info(&items, filter); - - EnumInfo { - ident: struct_info.ident.clone(), - attributes: struct_impl_info.attributes, - spa_type: struct_info.unnamed_field_ident.clone(), - representation_type: "u32".to_string(), - variants: struct_impl_info.constants.iter() - .map(move |constant| { - let index = constant.ident.to_string(); - let variant = EnumVariantInfo { - attributes: constant.attrs.clone(), - fields: Fields::Unit, - ident: constant.ident.clone(), - discriminant: match constant.expr.clone() { - Expr::Call(value) => { - let mut arg = value.args[0].clone(); - match arg { - Expr::Path(ref mut value) => { - let mut segments = Punctuated::::new(); - for index in 1..value.path.segments.len() { - segments.push(value.path.segments[index].clone()); - } - value.path.segments = segments; - }, - _ => panic!("Expected a path expression"), - }; - arg - }, - _ => panic!("Expected a call expression"), - }, - }; - (index, variant) - }) - .collect::>(), - } -} - -fn map_media_subtype_enum_info(items: &Vec, filter: F) -> EnumInfo -where - F: FnMut(&&ImplItemConst) -> bool -{ - const IDENT: &str = "MediaSubtype"; - - let ident_filter = move |ident: String| ident == IDENT; - let struct_info = map_struct_info(&items, ident_filter); - let mut struct_impl_info = map_struct_impl_info(&items, ident_filter); - - struct_impl_info.attributes.push_one("allow", "unexpected_cfgs"); // TODO remove this when V0_3_68 will be added to libspa manifest - struct_impl_info.attributes.push_one("allow", "unused_doc_comments"); - - EnumInfo { - ident: struct_info.ident.clone(), - attributes: struct_impl_info.attributes, - spa_type: struct_info.unnamed_field_ident.clone(), - representation_type: "u32".to_string(), - variants: struct_impl_info.constants.iter() - .filter(filter) - .filter(move |constant| { - match &constant.expr { - Expr::Call(_) => true, - _ => false, - } - }) - .map(move |constant| { - let index = constant.ident.to_string(); - let variant = EnumVariantInfo { - attributes: constant.attrs.clone(), - fields: Fields::Unit, - ident: constant.ident.clone(), - discriminant: match constant.expr.clone() { - Expr::Call(value) => { - let mut arg = value.args[0].clone(); - match arg { - Expr::Path(ref mut value) => { - let mut segments = Punctuated::::new(); - for index in 1..value.path.segments.len() { - segments.push(value.path.segments[index].clone()); - } - value.path.segments = segments; - }, - _ => panic!("Expected a path expression"), - }; - arg - }, - _ => panic!("Expected a call expression: {:?}", constant.expr), - }, - }; - (index, variant) - }) - .collect::>(), - } -} - -fn spa_audio_format_idents() -> Vec { - let audio_formats: Vec = vec![ - "S8".to_string(), - "U8".to_string(), - "S16".to_string(), - "U16".to_string(), - "S24".to_string(), - "U24".to_string(), - "S24_32".to_string(), - "U24_32".to_string(), - "S32".to_string(), - "U32".to_string(), - "F32".to_string(), - "F64".to_string(), - ]; - - let ends = vec![ - "LE".to_string(), - "BE".to_string(), - "P".to_string(), - ]; - - audio_formats.iter() - .flat_map(move |format| { - ends.iter() - .map(move |end| { - if format.contains("8") && end != "P" { - format!("SPA_AUDIO_FORMAT_{}", format) - } - else if format.contains("8") && end == "P" { - format!("SPA_AUDIO_FORMAT_{}{}", format, end) - } - else if format.contains("8") == false && end == "P" { - format!("SPA_AUDIO_FORMAT_{}{}", format, end) - } - else { - format!("SPA_AUDIO_FORMAT_{}_{}", format, end) - } - }) - .collect::>() - }) - .collect() -} - -fn map_audio_sample_format_enum_info(items: &Vec) -> EnumInfo { - let spa_audio_format_idents = spa_audio_format_idents(); - let constants = map_constant_info( - &items, - move |constant| spa_audio_format_idents.contains(constant), - move |a, b| { - a.cmp(&b) - } - ); - - let ident = "AudioSampleFormat"; - let spa_type = "spa_audio_format"; - - let mut attributes: Vec = vec![]; - attributes.push_one("allow", "non_camel_case_types"); - - EnumInfo { - ident: Ident::new(ident, ident.span()), - attributes, - spa_type: Ident::new(spa_type, spa_type.span()), - representation_type: "u32".to_string(), - variants: constants.iter() - .map(move |constant| { - let index = constant.ident.to_string(); - let ident = constant.ident.to_string().replace("SPA_AUDIO_FORMAT_", ""); - let ident = Ident::new(&ident, ident.span()); - let discriminant = *constant.expr.clone(); - let variant = EnumVariantInfo { - attributes: constant.attrs.clone(), - fields: Fields::Unit, - ident, - discriminant, - }; - (index, variant) - }) - .collect::>(), - } -} - -fn map_audio_channel_enum_info(items: &Vec) -> EnumInfo { - let constants = map_constant_info( - &items, - move |constant| { - if constant.starts_with("SPA_AUDIO_CHANNEL") == false { - return false; - } - - let constant = constant.replace("SPA_AUDIO_CHANNEL_", ""); - - if constant.starts_with("START") || constant.starts_with("LAST") || constant.starts_with("AUX") { - return false; - } - - return true; - }, - move |a, b| { - a.cmp(&b) - } - ); - - let ident = "AudioChannel"; - let spa_type = "spa_audio_channel"; - - let mut attributes: Vec = vec![]; - attributes.push_one("allow", "unused_doc_comments"); - - EnumInfo { - ident: Ident::new(ident, ident.span()), - attributes, - spa_type: Ident::new(spa_type, spa_type.span()), - representation_type: "u32".to_string(), - variants: constants.iter() - .map(move |constant| { - let index = constant.ident.to_string(); - let ident = constant.ident.to_string().replace("SPA_AUDIO_CHANNEL_", ""); - let ident = Ident::new(&ident, ident.span()); - let discriminant = *constant.expr.clone(); - let variant = EnumVariantInfo { - attributes: constant.attrs.clone(), - fields: Fields::Unit, - ident, - discriminant, - }; - (index, variant) - }) - .collect::>(), - } -} - -fn map_constant_info(items: &Vec, filter: F, sorter: S) -> Vec<&ItemConst> -where - F: Fn(&String) -> bool, - S: Fn(&String, &String) -> Ordering -{ - items.iter() - .filter_map(move |item| { - match item { - Item::Const(value) => { - Some(value) - } - &_ => None - } - }) - .filter(move |constant| filter(&constant.ident.to_string())) - .sorted_by(move |a, b| sorter(&a.ident.to_string(), &b.ident.to_string())) - .collect::>() -} - -fn map_struct_info(items: &Vec, filter: F) -> StructInfo -where - F: Fn(String) -> bool -{ - items.iter() - .filter_map(move |item| { - let item = item.next().unwrap(); - let item = item.next().unwrap(); - match item { - Item::Struct(value) => { - let ident = value.ident.clone(); - if filter(ident.to_string()) == false { - return None; - } - let visitor = StructVisitor::new(value); - Some((visitor, ident)) - } - &_ => None - } - }) - .filter_map(move |(visitor, ident)| { - let fields = visitor.fields(); - if fields.is_empty() == false { - Some(StructInfo { - ident, - unnamed_field_ident: { - let field = fields - .iter() - .map(|field| field.clone()) - .collect::>() - .first() - .cloned() - .unwrap(); - let ident = match field.ty { - Type::Path(value) => { - value.path.segments.iter() - .map(|segment| segment.ident.to_string()) - .join("::") - } - _ => panic!("Unsupported type: {:?}", field.ty), - }; - let ident = ident - .replace("spa_sys::", ""); - Ident::new(&ident, ident.span()) - }, - }) - } - else { - None - } - }) - .collect::>() - .first() - .cloned() - .unwrap() -} - -fn map_struct_impl_info(items: &Vec, filter: F) -> StructImplInfo -where - F: Fn(String) -> bool -{ - items.iter() - .filter_map(move |item| { - let item = item.next().unwrap(); - let item = item.next().unwrap(); - match item { - Item::Impl(value) => { - let visitor = StructImplVisitor::new(value); - let self_ident = visitor.self_type() - .segments - .iter() - .map(|segment| segment.ident.to_string()) - .collect::>() - .join("::"); - if filter(self_ident) == false { - return None; - } - let attributes = visitor.attributes(); - Some((visitor, attributes)) - } - &_ => None - } - }) - .filter_map(move |(visitor, attributes)| { - if attributes.is_empty() { - return None; - } - let constants = visitor.constants() - .iter() - .filter_map(move |constant| { - match constant.ty { - Type::Path(_) => { - Some(constant.clone()) - } - _ => None - } - }) - .collect::>(); - if constants.is_empty() == false { - Some(StructImplInfo { - attributes, - constants: constants.clone(), - }) - } - else { - None - } - }) - .collect::>() - .first() - .cloned() - .unwrap() -} - -fn generate_enums_code(enums: Vec, filename: &str) { - let code = enums.iter() - .map(move |enum_info| enum_info.generate()) - .collect::>() - .join("\n"); - - let out_dir = std::env::var("OUT_DIR") - .expect("OUT_DIR not set"); - - let path = std::path::Path::new(&out_dir).join(filename); - std::fs::write(path, code) - .expect("Unable to write generated file"); -} \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/mod.rs b/pipewire-spa-utils/build_modules/mod.rs deleted file mode 100644 index 7f94a002b..000000000 --- a/pipewire-spa-utils/build_modules/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod syntax; -pub mod format; -pub mod utils; \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/syntax/generators/enumerator.rs b/pipewire-spa-utils/build_modules/syntax/generators/enumerator.rs deleted file mode 100644 index dc9cc03af..000000000 --- a/pipewire-spa-utils/build_modules/syntax/generators/enumerator.rs +++ /dev/null @@ -1,145 +0,0 @@ -use build_modules::syntax::utils::AttributeExt; -use indexmap::IndexMap; -use quote::ToTokens; -use quote::__private::TokenStream; -use syn::__private::quote::quote; -use syn::__private::TokenStream2; -use syn::punctuated::Punctuated; -use syn::token::{Brace, Enum, Eq, Pub}; -use syn::{Attribute, Expr, Fields, Generics, Ident, ItemEnum, Variant, Visibility}; -use debug; - -#[derive(Debug)] -pub struct EnumInfo { - pub ident: Ident, - pub attributes: Vec, - pub spa_type: Ident, - pub representation_type: String, - pub variants: IndexMap -} - -#[derive(Debug)] -pub struct EnumVariantInfo { - pub attributes: Vec, - pub fields: Fields, - pub ident: Ident, - pub discriminant: Expr -} - -impl From<&EnumVariantInfo> for Variant { - fn from(value: &EnumVariantInfo) -> Self { - Variant { - attrs: value.attributes.clone(), - ident: value.ident.clone(), - fields: value.fields.clone(), - discriminant: Some((Eq::default(), value.discriminant.clone())), - } - } -} - -impl EnumInfo { - pub fn generate(&self) -> String { - let mut variants = Punctuated::new(); - self.variants.iter() - .for_each(|(_, variant)| { - variants.push(variant.into()) - }); - let mut attributes = self.attributes.clone(); - attributes.push_one("repr", self.representation_type.as_str()); - attributes.push_one("derive", "Debug"); - attributes.push_one("derive", "Clone"); - attributes.push_one("derive", "Copy"); - attributes.push_one("derive", "Ord"); - attributes.push_one("derive", "PartialOrd"); - attributes.push_one("derive", "Eq"); - attributes.push_one("derive", "PartialEq"); - let item = ItemEnum { - attrs: attributes.clone(), - vis: Visibility::Public(Pub::default()), - enum_token: Enum::default(), - ident: self.ident.clone(), - generics: Generics::default(), - brace_token: Brace::default(), - variants, - }; - let import_quote = quote! { - use libspa::sys::*; - }; - let attributes_quote = self.attributes.to_token_stream(); - let item_quote = quote!(#item); - let item_ident_quote = item.ident.to_token_stream(); - let representation_type_quote = self.representation_type.parse::().unwrap(); - let spa_type_quote = self.spa_type.to_token_stream(); - let from_representation_to_variant_quote = self.variants.iter() - .map(|(_, variant)| { - let ident = variant.ident.to_token_stream(); - let discriminant = variant.discriminant.to_token_stream(); - let attributes = variant.attributes.to_token_stream(); - quote! { - #attributes - #discriminant => Self::#ident, - } - }) - .collect::(); - let from_representation_type_quote = quote! { - #attributes_quote - impl From<#representation_type_quote> for #item_ident_quote { - fn from(value: #representation_type_quote) -> Self { - let value: #spa_type_quote = value; - match value { - #from_representation_to_variant_quote - _ => panic!("Unknown variant") - } - } - } - }; - let to_representation_type_quote = quote! { - #attributes_quote - impl From<&#item_ident_quote> for #representation_type_quote { - fn from(value: &#item_ident_quote) -> Self { - let value: #spa_type_quote = value.into(); - value - } - } - - #attributes_quote - impl From<#item_ident_quote> for #representation_type_quote { - fn from(value: #item_ident_quote) -> Self { - let value: #spa_type_quote = value.into(); - value - } - } - }; - let from_variant_to_string_quote = self.variants.iter() - .map(|(_, variant)| { - let ident = variant.ident.to_token_stream(); - let ident_string = variant.ident.to_string(); - let attributes = variant.attributes.to_token_stream(); - quote! { - #attributes - Self::#ident => #ident_string.to_string(), - } - }) - .collect::(); - let to_string_quote = quote! { - #attributes_quote - impl #item_ident_quote { - fn to_string(&self) -> String { - match self { - #from_variant_to_string_quote - } - } - } - }; - let items = vec![ - import_quote.to_string(), - item_quote.to_string(), - from_representation_type_quote.to_string(), - to_representation_type_quote.to_string(), - to_string_quote.to_string(), - ]; - let items = items.join("\n"); - let file = syn::parse_file(items.as_str()).unwrap(); - prettyplease::unparse(&file) - } -} \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/syntax/generators/mod.rs b/pipewire-spa-utils/build_modules/syntax/generators/mod.rs deleted file mode 100644 index 8e0a632e0..000000000 --- a/pipewire-spa-utils/build_modules/syntax/generators/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod enumerator; \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/syntax/mod.rs b/pipewire-spa-utils/build_modules/syntax/mod.rs deleted file mode 100644 index 06fb5ca66..000000000 --- a/pipewire-spa-utils/build_modules/syntax/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod generators; -pub mod parsers; -pub mod utils; \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/syntax/parsers/mod.rs b/pipewire-spa-utils/build_modules/syntax/parsers/mod.rs deleted file mode 100644 index 74fc5e3b9..000000000 --- a/pipewire-spa-utils/build_modules/syntax/parsers/mod.rs +++ /dev/null @@ -1,59 +0,0 @@ -use syn::{Attribute, Fields, ImplItem, ImplItemConst, ItemImpl, ItemStruct, Path, Type}; - -pub struct StructVisitor<'a> { - item: &'a ItemStruct, -} - -impl<'a> StructVisitor<'a> { - pub fn new(item: &'a ItemStruct) -> Self { - Self { - item, - } - } - - pub fn fields(&self) -> Fields { - self.item.fields.clone() - } -} - -pub struct StructImplVisitor<'a> { - item: &'a ItemImpl -} - -impl<'a> StructImplVisitor<'a> { - pub fn new(item: &'a ItemImpl) -> Self { - Self { - item, - } - } - pub fn self_type(&self) -> Path { - match *self.item.self_ty.clone() { - Type::Path(value) => { - value.path.clone() - } - _ => panic!("Path expected") - } - } - - pub fn attributes(&self) -> Vec { - self.item.attrs.iter() - .map(move |attribute| { - attribute.clone() - }) - .collect::>() - } - - pub fn constants(&self) -> Vec { - self.item.items.iter() - .filter_map(move |item| { - match item { - ImplItem::Const(value) => { - Some(value.clone()) - } - &_ => return None - } - }) - .collect::>() - } -} - diff --git a/pipewire-spa-utils/build_modules/syntax/utils.rs b/pipewire-spa-utils/build_modules/syntax/utils.rs deleted file mode 100644 index fd2113a9c..000000000 --- a/pipewire-spa-utils/build_modules/syntax/utils.rs +++ /dev/null @@ -1,67 +0,0 @@ -use quote::ToTokens; -use quote::__private::TokenStream; -use std::str::FromStr; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::token::{Bracket, Paren, Pound}; -use syn::{AttrStyle, Attribute, Ident, MacroDelimiter, Meta, MetaList, PathArguments, PathSegment}; - -fn add_attribute(attrs: &mut Vec, ident: &str, value: &str) { - attrs.push( - Attribute { - pound_token: Pound::default(), - style: AttrStyle::Outer, - bracket_token: Bracket::default(), - meta: Meta::List(MetaList { - path: syn::Path { - leading_colon: None, - segments: { - let mut segments = Punctuated::default(); - let ident = ident; - segments.push(PathSegment { - ident: Ident::new(ident, ident.span()), - arguments: PathArguments::None, - }); - segments - }, - }, - delimiter: MacroDelimiter::Paren(Paren::default()), - tokens: TokenStream::from_str(value).unwrap(), - }), - } - ); -} - -pub trait AttributeExt { - fn push_one(&mut self, ident: &str, value: &str); - fn to_token_stream(&self) -> TokenStream; - fn contains(&self, ident: &String) -> bool; -} - -impl AttributeExt for Vec { - fn push_one(&mut self, ident: &str, value: &str) { - add_attribute(self, ident, value); - } - - fn to_token_stream(&self) -> TokenStream { - self.iter() - .map(|attr| attr.to_token_stream()) - .collect() - } - - fn contains(&self, ident: &String) -> bool { - self.iter().any(move |attribute| { - match &attribute.meta { - Meta::Path(value) => { - value.segments.iter().any(|segment| segment.ident == ident) - } - Meta::List(value) => { - value.path.segments.iter().any(|segment| segment.ident == ident) - } - Meta::NameValue(value) => { - value.path.segments.iter().any(|segment| segment.ident == ident) - } - } - }) - } -} \ No newline at end of file diff --git a/pipewire-spa-utils/build_modules/utils/mod.rs b/pipewire-spa-utils/build_modules/utils/mod.rs deleted file mode 100644 index 0736c9a4b..000000000 --- a/pipewire-spa-utils/build_modules/utils/mod.rs +++ /dev/null @@ -1,104 +0,0 @@ -use cargo_metadata::camino::Utf8PathBuf; -use cargo_metadata::{Message, Node, Package}; -use std::fs::File; -use std::io::{BufReader, Read}; -use std::path::PathBuf; -use std::process::{Command, Stdio}; - -#[macro_export] -macro_rules! debug { - ($($tokens: tt)*) => { - println!("cargo:warning={}", format!($($tokens)*)) - } -} - -pub struct PackageInfo { - pub src_path: PathBuf, - pub build_path: PathBuf, - pub features: Vec, -} - -pub fn map_package_info() -> PackageInfo { - let (package, resolve) = find_dependency( - "./Cargo.toml", - move |package| package.name == "libspa" - ); - let src_path = package.manifest_path.parent().unwrap().as_str(); - let src_path = PathBuf::from(src_path).join("src"); - let build_path = dependency_build_path(&package.manifest_path, &resolve).unwrap(); - PackageInfo { - src_path, - build_path, - features: resolve.features.clone(), - } -} - -fn find_dependency(manifest_path: &str, filter: F) -> (Package, Node) -where - F: Fn(&Package) -> bool -{ - let mut cmd = cargo_metadata::MetadataCommand::new(); - let metadata = cmd - .manifest_path(manifest_path) - .exec().unwrap(); - let package = metadata.packages - .iter() - .find(move |package| filter(package)) - .unwrap() - .clone(); - let package_id = package.id.clone(); - let resolve = metadata.resolve.as_ref().unwrap().nodes - .iter() - .find(move |node| { - node.id == package_id - }) - .unwrap() - .clone(); - (package, resolve) -} - -fn dependency_build_path(manifest_path: &Utf8PathBuf, node: &Node) -> Option { - let dependency = node.deps.iter() - .find(move |dependency| dependency.name == "spa_sys") - .and_then(move |dependency| Some(dependency.pkg.clone())) - .unwrap(); - let (package, _) = find_dependency( - manifest_path.as_ref(), - move |package| package.id == dependency - ); - let mut command = Command::new("cargo") - .current_dir(package.manifest_path.parent().unwrap()) - .args(&["check", "--message-format=json", "--quiet"]) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn() - .unwrap(); - command.wait().unwrap(); - let reader = BufReader::new(command.stdout.take().unwrap()); - for message in Message::parse_stream(reader) { - match message.ok().unwrap() { - Message::BuildScriptExecuted(script) => { - if script.package_id.repr.starts_with("path+file://"){ - return Some(script.out_dir.clone().as_std_path().to_path_buf()) - } - }, - _ => () - } - } - - None -} - -pub fn read_source_file(src_path: &PathBuf, file_path: &PathBuf) -> syn::File { - let path = src_path.join(file_path); - let mut file = File::open(path) - .expect("Unable to open file"); - - let mut src = String::new(); - file.read_to_string(&mut src) - .expect("Unable to read file"); - - let syntax = syn::parse_file(&src) - .expect("Unable to parse file"); - syntax -} \ No newline at end of file diff --git a/pipewire-spa-utils/src/audio/mod.rs b/pipewire-spa-utils/src/audio/mod.rs deleted file mode 100644 index af370afae..000000000 --- a/pipewire-spa-utils/src/audio/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -use libspa::pod::deserialize::DeserializeError; -use libspa::pod::deserialize::DeserializeSuccess; -use libspa::pod::deserialize::PodDeserialize; -use libspa::pod::deserialize::PodDeserializer; -use libspa::pod::deserialize::VecVisitor; -use libspa::utils::Id; -use std::convert::TryInto; -use std::ops::Deref; -use impl_array_id_deserializer; -use utils::IdOrEnumId; - -pub mod raw; - -include!(concat!(env!("OUT_DIR"), "/audio.rs")); - -#[derive(Debug, Clone)] -pub struct AudioSampleFormatEnum(IdOrEnumId); - -impl Deref for AudioSampleFormatEnum { - type Target = IdOrEnumId; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive(Debug, Clone)] -pub struct AudioChannelPosition(Vec); - -impl Default for AudioChannelPosition { - fn default() -> Self { - AudioChannelPosition(vec![]) - } -} - -impl AudioChannelPosition { - pub fn to_array(&self) -> [u32; N] { - let mut channels = self.0 - .iter() - .map(move |channel| *channel as u32) - .collect::>(); - if channels.len() < N { - channels.resize(N, AudioChannel::UNKNOWN as u32); - } - channels.try_into().unwrap() - } -} - -impl Deref for AudioChannelPosition { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - self.0.as_ref() - } -} - -impl_array_id_deserializer!(AudioChannelPosition, AudioChannel); \ No newline at end of file diff --git a/pipewire-spa-utils/src/audio/raw.rs b/pipewire-spa-utils/src/audio/raw.rs deleted file mode 100644 index b95e864c2..000000000 --- a/pipewire-spa-utils/src/audio/raw.rs +++ /dev/null @@ -1,63 +0,0 @@ -use audio::{AudioChannelPosition, AudioSampleFormat}; -use format::{MediaSubtype, MediaType}; -use libspa::pod::deserialize::{DeserializeError, DeserializeSuccess, ObjectPodDeserializer, PodDeserialize, PodDeserializer, Visitor}; -use utils::{IdOrEnumId, IntOrChoiceInt, IntOrRangeInt32}; - -#[derive(Debug, Clone)] -pub struct AudioInfoRaw { - pub media_type: MediaType, - pub media_subtype: MediaSubtype, - pub sample_format: IdOrEnumId, - pub sample_rate: IntOrRangeInt32, - pub channels: IntOrChoiceInt, - pub position: AudioChannelPosition -} - -impl<'de> PodDeserialize<'de> for AudioInfoRaw { - fn deserialize( - deserializer: PodDeserializer<'de>, - ) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> - where - Self: Sized, - { - struct EnumFormatVisitor; - - impl<'de> Visitor<'de> for EnumFormatVisitor { - type Value = AudioInfoRaw; - type ArrayElem = std::convert::Infallible; - - fn visit_object( - &self, - object_deserializer: &mut ObjectPodDeserializer<'de>, - ) -> Result> { - let media_type = object_deserializer - .deserialize_property_key(libspa::sys::SPA_FORMAT_mediaType)? - .0; - let media_subtype = object_deserializer - .deserialize_property_key(libspa::sys::SPA_FORMAT_mediaSubtype)? - .0; - let sample_format = object_deserializer - .deserialize_property_key(libspa::sys::SPA_FORMAT_AUDIO_format)? - .0; - let sample_rate = object_deserializer - .deserialize_property_key(libspa::sys::SPA_FORMAT_AUDIO_rate)? - .0; - let channels = object_deserializer - .deserialize_property_key(libspa::sys::SPA_FORMAT_AUDIO_channels)? - .0; - let position = object_deserializer - .deserialize_property_key(libspa::sys::SPA_FORMAT_AUDIO_position)? - .0; - Ok(AudioInfoRaw { - media_type, - media_subtype, - sample_format, - sample_rate, - channels, - position, - }) - } - } - deserializer.deserialize_object(EnumFormatVisitor) - } -} \ No newline at end of file diff --git a/pipewire-spa-utils/src/format/mod.rs b/pipewire-spa-utils/src/format/mod.rs deleted file mode 100644 index a79445434..000000000 --- a/pipewire-spa-utils/src/format/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -use libspa::pod::deserialize::DeserializeError; -use libspa::pod::deserialize::DeserializeSuccess; -use libspa::pod::deserialize::IdVisitor; -use libspa::pod::deserialize::PodDeserialize; -use libspa::pod::deserialize::PodDeserializer; -use libspa::utils::Id; -use ::impl_id_deserializer; - -include!(concat!(env!("OUT_DIR"), "/format.rs")); - -impl_id_deserializer!(MediaType); -impl_id_deserializer!(MediaSubtype); \ No newline at end of file diff --git a/pipewire-spa-utils/src/lib.rs b/pipewire-spa-utils/src/lib.rs deleted file mode 100644 index 06345d50e..000000000 --- a/pipewire-spa-utils/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -extern crate libspa; -mod macros; -pub mod format; -pub mod audio; -pub mod utils; \ No newline at end of file diff --git a/pipewire-spa-utils/src/macros/mod.rs b/pipewire-spa-utils/src/macros/mod.rs deleted file mode 100644 index 23bc4be95..000000000 --- a/pipewire-spa-utils/src/macros/mod.rs +++ /dev/null @@ -1,115 +0,0 @@ -#[macro_export] -macro_rules! impl_id_deserializer { - ( - $name:ident - ) => { - impl From for $name { - fn from(value: Id) -> Self { - value.0.into() - } - } - - impl<'de> PodDeserialize<'de> for $name { - fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> - where - Self: Sized - { - let res = deserializer.deserialize_id(IdVisitor)?; - Ok((res.0.into(), res.1)) - } - } - } -} - -#[macro_export] -macro_rules! impl_choice_id_deserializer { - ( - $name:ident - ) => { - impl From> for $name { - fn from(value: Choice) -> Self { - value.1.into() - } - } - - impl<'de> PodDeserialize<'de> for $name { - fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> - where - Self: Sized - { - let res = deserializer.deserialize_choice(ChoiceIdVisitor)?; - Ok((res.0.into(), res.1)) - } - } - } -} - -#[macro_export] -macro_rules! impl_choice_int_deserializer { - ( - $name:ident - ) => { - impl From> for $name { - fn from(value: Choice) -> Self { - value.1.into() - } - } - - impl<'de> PodDeserialize<'de> for $name { - fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> - where - Self: Sized - { - let res = deserializer.deserialize_choice(ChoiceIntVisitor)?; - Ok((res.0.into(), res.1)) - } - } - } -} - -#[macro_export] -macro_rules! impl_array_id_deserializer { - ( - $array_name:ident, - $item_name:ident - ) => { - impl From<&Id> for $item_name { - fn from(value: &Id) -> Self { - value.0.into() - } - } - - impl From> for $array_name { - fn from(value: Vec) -> Self { - $array_name(value.iter().map(|id| id.into()).collect()) - } - } - - impl<'de> PodDeserialize<'de> for $array_name { - fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> - where - Self: Sized - { - let res = deserializer.deserialize_array(VecVisitor::default())?; - Ok((res.0.into(), res.1)) - } - } - } -} - -#[macro_export] -macro_rules! impl_any_deserializer { - ( - $name:ident - ) => { - impl<'de> PodDeserialize<'de> for $name { - fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> - where - Self: Sized - { - let res = deserializer.deserialize_any()?; - Ok((res.0.into(), res.1)) - } - } - } -} \ No newline at end of file diff --git a/pipewire-spa-utils/src/utils/mod.rs b/pipewire-spa-utils/src/utils/mod.rs deleted file mode 100644 index 2b30c9dfb..000000000 --- a/pipewire-spa-utils/src/utils/mod.rs +++ /dev/null @@ -1,255 +0,0 @@ -use ::impl_any_deserializer; -use libspa::pod::deserialize::DeserializeError; -use libspa::pod::deserialize::DeserializeSuccess; -use libspa::pod::deserialize::PodDeserialize; -use libspa::pod::deserialize::PodDeserializer; -use libspa::pod::deserialize::{ChoiceIdVisitor, ChoiceIntVisitor}; -use libspa::pod::{ChoiceValue, Value}; -use libspa::utils::{Choice, ChoiceEnum, Id}; -use std::ops::Deref; -use impl_choice_int_deserializer; - -#[derive(Debug, Clone)] -pub struct IntOrChoiceInt(u32); - -impl From for IntOrChoiceInt { - fn from(value: u32) -> Self { - Self(value) - } -} - -impl From for IntOrChoiceInt { - fn from(value: ChoiceValue) -> Self { - match value { - ChoiceValue::Int(value) => value.into(), - _ => panic!("Expected Int or ChoiceValue::Int"), - } - } -} - -impl From> for IntOrChoiceInt { - fn from(value: Choice) -> Self { - match value.1 { - ChoiceEnum::None(value) => IntOrChoiceInt(value as u32), - _ => panic!("Expected ChoiceEnum::None"), - } - } -} - -impl From for IntOrChoiceInt { - fn from(value: Value) -> Self { - match value { - Value::Int(value) => Self(value as u32), - Value::Choice(value) => value.into(), - _ => panic!("Expected Int or Choice") - } - } -} - -impl Deref for IntOrChoiceInt { - type Target = u32; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl_any_deserializer!(IntOrChoiceInt); - -#[derive(Debug, Clone)] -pub struct RangeInt32 { - pub value: u32, - pub minimum: u32, - pub maximum: u32, -} - -impl RangeInt32 { - fn new(value: u32, minimum: u32, maximum: u32) -> Self { - Self { - value, - minimum, - maximum, - } - } -} - -impl From> for RangeInt32 { - fn from(value: ChoiceEnum) -> Self { - match value { - ChoiceEnum::Range { - default, min, max - } => RangeInt32::new( - default as u32, min as u32, max as u32, - ), - _ => panic!("Expected ChoiceEnum::Range") - } - } -} - -impl_choice_int_deserializer!(RangeInt32); - -#[derive(Debug, Clone)] -pub struct IntOrRangeInt32(RangeInt32); - -impl From for IntOrRangeInt32 { - fn from(value: u32) -> Self { - Self(RangeInt32::new(value, value, value)) - } -} - -impl From for IntOrRangeInt32 { - fn from(value: i32) -> Self { - Self(RangeInt32::new(value as u32, value as u32, value as u32)) - } -} - -impl From for IntOrRangeInt32 { - fn from(value: ChoiceValue) -> Self { - match value { - ChoiceValue::Int(value) => value.into(), - _ => panic!("Expected ChoiceValue::Int") - } - } -} - -impl From> for IntOrRangeInt32 { - - fn from(value: Choice) -> Self { - match value.1 { - ChoiceEnum::None(value) => { - Self(RangeInt32::new(value as u32, value as u32, value as u32)) - } - ChoiceEnum::Range { default, min, max } => { - Self(RangeInt32::new(default as u32, min as u32, max as u32)) - } - _ => panic!("Expected Choice::None or Choice::Range") - } - } -} - -impl From for IntOrRangeInt32 { - - fn from(value: Value) -> Self { - match value { - Value::Int(value) => Self::from(value), - Value::Choice(value) => value.into(), - _ => panic!("Expected Int or Choice") - } - } -} - -impl Deref for IntOrRangeInt32 { - type Target = RangeInt32; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl_any_deserializer!(IntOrRangeInt32); - -#[derive(Debug, Clone)] -pub struct EnumId { - pub default: T, - pub alternatives: Vec, -} - -impl EnumId { - fn new(default: T, mut alternatives: Vec) -> Self { - alternatives.sort_by(move |a, b| { - a.cmp(b) - }); - Self { - default, - alternatives, - } - } -} - -impl + Ord> From> for EnumId { - fn from(value: ChoiceEnum) -> Self { - match value { - ChoiceEnum::Enum { - default, alternatives - } => EnumId::new( - default.0.into(), - alternatives.into_iter() - .map(move |id| id.0.into()) - .collect(), - ), - _ => panic!("Expected ChoiceEnum::Enum") - } - } -} - -impl + Ord> From> for EnumId { - fn from(value: Choice) -> Self { - value.1.into() - } -} - -impl <'de, T: From + Ord> PodDeserialize<'de> for EnumId { - fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> - where - Self: Sized - { - let res = deserializer.deserialize_choice(ChoiceIdVisitor)?; - Ok((res.0.into(), res.1)) - } -} - -#[derive(Debug, Clone)] -pub struct IdOrEnumId(EnumId); - -impl + Ord> From for IdOrEnumId { - fn from(value: ChoiceValue) -> Self { - match value { - ChoiceValue::Id(value) => value.into(), - _ => panic!("Expected ChoiceValue::Id") - } - } -} - -impl + Ord> From> for IdOrEnumId { - fn from(value: Choice) -> Self { - match value.1 { - ChoiceEnum::Enum { default, alternatives } => { - Self(EnumId::new( - default.0.into(), - alternatives.into_iter() - .map(move |id| id.0.into()) - .collect::>() - )) - } - _ => panic!("Expected Choice::Enum") - } - } -} - -impl + Ord> From for IdOrEnumId { - fn from(value: Value) -> Self { - match value { - Value::Id(value) => Self(EnumId::new(value.0.into(), vec![value.0.into()])), - Value::Choice(value) => value.into(), - _ => panic!("Expected Id or Choice") - } - } -} - -impl Deref for IdOrEnumId { - type Target = EnumId; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl <'de, T: From + Ord> PodDeserialize<'de> for IdOrEnumId { - fn deserialize(deserializer: PodDeserializer<'de>) -> Result<(Self, DeserializeSuccess<'de>), DeserializeError<&'de [u8]>> - where - Self: Sized - { - let res = deserializer.deserialize_any()?; - Ok((res.0.into(), res.1)) - } -} \ No newline at end of file diff --git a/pipewire-test-utils/.containers/.digests b/pipewire-test-utils/.containers/.digests deleted file mode 100644 index e13625db0..000000000 --- a/pipewire-test-utils/.containers/.digests +++ /dev/null @@ -1,3 +0,0 @@ -pipewire-default=sha256:61a3bcf12683ea189b3cfdaa9eb0439f3a24b22e13a5336d0ec26121fc2d068e -pipewire-without-node=sha256:04d6951ebb1625424b7c2f5bc012ce7d9cbb3d47c2f4a66da97f8f2f35584916 -pipewire-without-session-manager=sha256:ccaf8f57bbc06494feed36bcd1addf6af36592c0f902e4d81cfd4fc4122e2785 diff --git a/pipewire-test-utils/.containers/.tmp/entrypoint.bash b/pipewire-test-utils/.containers/.tmp/entrypoint.bash deleted file mode 100644 index 2fa77d53d..000000000 --- a/pipewire-test-utils/.containers/.tmp/entrypoint.bash +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -mkdir --parents ${PIPEWIRE_RUNTIME_DIR} -mkdir --parents /etc/pipewire/pipewire.conf.d/ -cp /root/virtual.nodes.conf /etc/pipewire/pipewire.conf.d/virtual.nodes.conf -supervisord -c /root/supervisor.conf -rm --force --recursive ${PIPEWIRE_RUNTIME_DIR} \ No newline at end of file diff --git a/pipewire-test-utils/.containers/.tmp/healthcheck.bash b/pipewire-test-utils/.containers/.tmp/healthcheck.bash deleted file mode 100644 index 5cbb42d2b..000000000 --- a/pipewire-test-utils/.containers/.tmp/healthcheck.bash +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -e -(echo 'wait for pipewire') || exit 1 -(pw-cli ls 0 | grep --quiet 'id 0, type PipeWire:Interface:Core/4') || exit 1 -(echo 'wait for wireplumbler') || exit 1 -(wpctl info | grep --quiet 'WirePlumber') || exit 1 -(echo 'wait for PipeWire Pulse') || exit 1 -(pactl info | grep --quiet "$PULSE_RUNTIME_PATH/native") || exit 1 -(echo 'wait for test-sink') || exit 1 -(pactl set-default-sink 'test-sink') || exit 1 -(wpctl status | grep --quiet 'test-sink') || exit 1 -(echo 'wait for test-source') || exit 1 -(pactl set-default-source 'test-source') || exit 1 -(wpctl status | grep --quiet 'test-source') || exit 1 \ No newline at end of file diff --git a/pipewire-test-utils/.containers/.tmp/supervisor.conf b/pipewire-test-utils/.containers/.tmp/supervisor.conf deleted file mode 100644 index bf4250d13..000000000 --- a/pipewire-test-utils/.containers/.tmp/supervisor.conf +++ /dev/null @@ -1,21 +0,0 @@ -[supervisord] -nodaemon=true -logfile=/var/log/supervisor/supervisord.log -[program:pipewire] -command=pipewire -stdout_logfile=/tmp/pipewire.out.log -stderr_logfile=/tmp/pipewire.err.log -autostart=true -autorestart=true -[program:wireplumber] -command=wireplumber -stdout_logfile=/tmp/wireplumber.out.log -stderr_logfile=/tmp/wireplumber.err.log -autostart=true -autorestart=true -[program:pipewire-pulse] -command=pipewire-pulse -stdout_logfile=/tmp/pipewire-pulse.out.log -stderr_logfile=/tmp/pipewire-pulse.err.log -autostart=true -autorestart=true \ No newline at end of file diff --git a/pipewire-test-utils/.containers/pipewire.test.container b/pipewire-test-utils/.containers/pipewire.test.container deleted file mode 100644 index ae939246c..000000000 --- a/pipewire-test-utils/.containers/pipewire.test.container +++ /dev/null @@ -1,48 +0,0 @@ -from fedora:41 as base -run dnf update --assumeyes -run dnf install --assumeyes \ - supervisor \ - socat \ - procps-ng \ - htop \ - pipewire \ - pipewire-utils \ - pipewire-alsa \ - pipewire-pulse \ - pipewire-devel \ - wireplumber \ - pulseaudio-utils - -copy virtual.nodes.conf /root/virtual.nodes.conf - -run mkdir --parents /etc/wireplumber/wireplumber.conf.d -run touch /etc/wireplumber/wireplumber.conf.d/80-disable-dbus.conf -run tee /etc/wireplumber/wireplumber.conf.d/80-disable-dbus.conf <"] -description = "PipeWire Test Utils" -repository = "https://github.com/RustAudio/cpal/" -documentation = "" -license = "Apache-2.0" -keywords = ["pipewire", "test", "utils"] - -[dependencies] -pipewire-common = { version = "0.1", path = "../pipewire-common" } -bollard = { version = "0.18", features = ["buildkit"] } -futures = "0.3" -tar = "0.4" -flate2 = "1.0" -bytes = "1.9" -sha2 = "0.10" -uuid = { version = "1.12", features = ["v4"] } -tokio = { version = "1", features = ["full"] } -libc = "0.2" -rstest = "0.24" -ctor = "0.2" -http = "1.2" -serde = "1.0" -serde_json = "1.0" -serde_urlencoded = "0.7" -url = "2.5" -hex = "0.4" -ureq = "3.0" diff --git a/pipewire-test-utils/src/containers/container.rs b/pipewire-test-utils/src/containers/container.rs deleted file mode 100644 index 8d13f7618..000000000 --- a/pipewire-test-utils/src/containers/container.rs +++ /dev/null @@ -1,591 +0,0 @@ -use pipewire_common::utils::Backoff; -use bollard::container::{ListContainersOptions, LogOutput, RestartContainerOptions, UploadToContainerOptions}; -use bollard::exec::{CreateExecOptions, StartExecOptions, StartExecResults}; -use bollard::image::{BuildImageOptions, BuilderVersion}; -use bollard::{Docker}; -use bytes::Bytes; -use futures::StreamExt; -use std::collections::HashMap; -use std::{fs, io}; -use std::ffi::CString; -use std::fs::{File, OpenOptions}; -use std::io::{Read, Seek, SeekFrom, Write}; -use std::os::unix::fs::PermissionsExt; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, LazyLock}; -use bollard::errors::Error; -use bollard::models::{ContainerInspectResponse, ContainerState, ContainerSummary, Health, HealthStatusEnum, ImageInspect}; -use sha2::{Digest, Sha256}; -use tar::{Builder, Header}; -use tokio::runtime::Runtime; -use uuid::Uuid; -use crate::containers::options::{CreateContainerOptionsBuilder, StopContainerOptionsBuilder}; -use crate::environment::TEST_ENVIRONMENT; -use crate::HexSlice; - -pub(crate) static CONTAINER_PATH: LazyLock = LazyLock::new(|| { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .join("pipewire-test-utils") - .join(".containers") - .as_path() - .to_path_buf() -}); - -pub(crate) static CONTAINER_TMP_PATH: LazyLock = LazyLock::new(|| { - let path = CONTAINER_PATH - .join(".tmp") - .as_path() - .to_path_buf(); - fs::create_dir_all(&path).unwrap(); - path -}); - -pub(crate) static CONTAINER_DIGESTS_FILE_PATH: LazyLock = LazyLock::new(|| { - CONTAINER_PATH - .join(".digests") - .as_path() - .to_path_buf() -}); - -pub struct ImageRegistry { - images: HashMap, -} - -impl ImageRegistry { - pub fn new() -> Self { - let file = match fs::read_to_string(&*CONTAINER_DIGESTS_FILE_PATH) { - Ok(value) => value, - Err(_) => return Self { - images: HashMap::new(), - } - }; - let images = file.lines() - .into_iter() - .map(|line| { - let line_parts = line.split("=").collect::>(); - let image_name = line_parts[0]; - let container_file_digest = line_parts[1]; - (image_name.to_string(), container_file_digest.to_string().to_string()) - }) - .collect::>(); - Self { - images, - } - } - - pub fn push(&mut self, image_name: String, container_file_digest: String) { - if self.images.contains_key(&image_name) { - *self.images.get_mut(&image_name).unwrap() = container_file_digest - } - else { - self.images.insert(image_name, container_file_digest); - } - } - - pub fn is_build_needed(&self, image_name: &String, digest: &String) -> bool { - self.images.get(image_name).map_or(true, |entry| { - *entry != *digest - }) - } - - pub(crate) fn cleanup(&self) { - let mut file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(&*CONTAINER_DIGESTS_FILE_PATH) - .unwrap(); - for (image_name, container_file_digest) in self.images.iter() { - unsafe { - let format = CString::new("Registering image digests to further process: %s\n").unwrap(); - libc::printf(format.as_ptr() as *const i8, CString::new(image_name.clone()).unwrap()); - } - // println!("Registering image digests to further process: {}", image_name); - writeln!( - file, - "{}={}", - image_name, - container_file_digest - ).unwrap(); - } - } -} - -pub struct ContainerRegistry { - api: ContainerApi, - containers: Vec -} - -impl ContainerRegistry { - pub fn new(api: ContainerApi) -> Self { - let registry = Self { - api: api.clone(), - containers: Vec::new(), - }; - registry.clean(); - registry - } - - pub(crate) fn clean(&self) { - let containers = self.api.get_all().unwrap(); - for container in containers { - let container_id = container.id.unwrap(); - let inspect_result = match self.api.inspect(&container_id) { - Ok(value) => value, - Err(_) => continue - }; - if let Some(state) = inspect_result.state { - self.api.clean(&container_id.to_string(), &state); - } - } - } -} - -struct ImageContext { -} - -impl ImageContext { - fn create(container_file_path: &PathBuf) -> Result<(Bytes, String), Error> { - let excluded_filename = vec![ - ".digests", - ]; - let context_path = container_file_path.parent().unwrap(); - // Hasher is used for computing all context files hashes. - // In that way we can determine later with we build the image or not. - // This is better that just computing context archive hash which include data and metadata - // that can change regarding if context files had not changed in times. - let mut hasher = Sha256::new(); - let mut archive = tar::Builder::new(Vec::new()); - Self::read_directory( - &mut archive, - &mut hasher, - context_path, - context_path, - Some(&excluded_filename) - )?; - let uncompressed = archive.into_inner()?; - let mut compressed = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); - compressed.write_all(&uncompressed)?; - let compressed = compressed.finish()?; - let data = Bytes::from(compressed); - let hash_bytes = hasher.finalize().to_vec(); - let digest = HexSlice(hash_bytes.as_slice()); - let digest = format!("sha256:{}", digest); - Ok((data, digest)) - } - - fn read_directory( - archive: &mut Builder>, - hasher: &mut impl Write, - root: &Path, - directory: &Path, - excluded_filenames: Option<&Vec<&str>> - ) -> io::Result<()> { - if directory.is_dir() { - for entry in fs::read_dir(directory)? { - let entry = entry?; - let path = entry.path(); - let filename = path.file_name().unwrap().to_str().unwrap(); - if path.is_dir() { - Self::read_directory(archive, hasher, root, &path, excluded_filenames)?; - } - else if path.is_file() { - if excluded_filenames.as_ref().unwrap().contains(&filename) { - continue; - } - let mut file = File::open(&path)?; - io::copy(&mut file, hasher)?; - file.seek(SeekFrom::Start(0))?; - let mut header = Header::new_gnu(); - let metadata = file.metadata()?; - header.set_path(path.strip_prefix(root).unwrap())?; - header.set_size(metadata.len()); - header.set_mode(metadata.permissions().mode()); - header.set_mtime(metadata.modified()?.elapsed().unwrap().as_secs()); - header.set_cksum(); - archive.append(&header, &mut file)?; - } - } - } - Ok(()) - } -} - -pub struct ImageApi { - runtime: Arc, - api: Arc -} - -impl ImageApi { - pub fn new(runtime: Arc, api: Arc) -> Self { - Self { - runtime, - api, - } - } - - pub fn inspect(&self, image_name: &String) -> Result { - let result = self.api.inspect_image(image_name.as_str()); - self.runtime.block_on(result) - } - - pub fn build( - &self, - container_file_path: &PathBuf, - image_name: &String, - image_tag: &String - ) { - let tag = format!("{}:{}", image_name, image_tag); - let options = BuildImageOptions { - dockerfile: container_file_path.file_name().unwrap().to_str().unwrap(), - t: tag.as_str(), - session: Some(Uuid::new_v4().to_string()), - version: BuilderVersion::BuilderBuildKit, - ..Default::default() - }; - let (context, context_digest) = ImageContext::create(&container_file_path).unwrap(); - let mut environment = TEST_ENVIRONMENT.lock().unwrap(); - println!("Container image digest: {}", context_digest); - if environment.container_image_registry.is_build_needed(&image_name, &context_digest) == false { - println!("Skip build container image: {}", tag); - return; - } - println!("Build container image: {}", tag); - let mut stream = self.api.build_image(options, None, Some(context)); - while let Some(message) = self.runtime.block_on(stream.next()) { - match message { - Ok(message) => { - if let Some(stream) = message.stream { - if cfg!(debug_assertions) { - print!("{}", stream) - } - } - else if let Some(error) = message.error { - panic!("{}", error); - } - } - Err(value) => { - panic!("Error during image build: {:?}", value); - } - } - }; - environment.container_image_registry.push( - image_name.clone(), - context_digest.clone() - ); - } -} - -impl Clone for ImageApi { - fn clone(&self) -> Self { - Self { - runtime: self.runtime.clone(), - api: self.api.clone(), - } - } -} - -pub struct ContainerApi { - runtime: Arc, - api: Arc -} - -impl ContainerApi { - pub fn new(runtime: Arc, api: Arc) -> Self { - Self { - runtime, - api, - } - } - - pub(self) fn get_all(&self) -> Result, Error>{ - let mut filter = HashMap::new(); - filter.insert("label", vec!["test.container=true"]); - let options = ListContainersOptions { - all: true, - filters: filter, - ..Default::default() - }; - let call = self.api.list_containers(Some(options)); - self.runtime.block_on(call) - } - - pub(self) fn clean(&self, id: &String, state: &ContainerState) { - println!("Clean container with id {}", id); - if state.running.unwrap() { - let stop_options = StopContainerOptionsBuilder::default().build(); - let call = self.api.stop_container(id, Some(stop_options)); - self.runtime.block_on(call).unwrap(); - } - let call = self.api.remove_container(id, None); - self.runtime.block_on(call).unwrap(); - } - - pub fn create(&self, options: &mut CreateContainerOptionsBuilder) -> String { - let options = options - .with_label("test.container", true.to_string()) - .build(); - println!("Create container with image {}", options.image.as_ref().unwrap()); - let call = self.api.create_container::(None, options); - let result = self.runtime.block_on(call).unwrap(); - result.id - } - - pub fn start(&self, id: &String) { - println!("Start container with id {}", id); - let call = self.api.start_container::(id, None); - self.runtime.block_on(call).unwrap(); - } - - pub fn stop(&self, id: &String, options: &mut StopContainerOptionsBuilder) { - println!("Stop container with id {}", id); - let options = options.build(); - let call = self.api.stop_container(id, Some(options)); - self.runtime.block_on(call).unwrap(); - } - - pub fn restart(&self, id: &String) { - println!("Restart container with id {}", id); - let options = RestartContainerOptions { - t: 0, - }; - let call = self.api.restart_container(id, Some(options)); - self.runtime.block_on(call).unwrap(); - } - - pub fn remove(&self, id: &String) { - println!("Remove container with id {}", id); - let call = self.api.remove_container(id, None); - self.runtime.block_on(call).unwrap(); - } - - pub fn inspect(&self, id: &String) -> Result { - let call = self.api.inspect_container(id, None); - self.runtime.block_on(call).map_err(|error| { - pipewire_common::error::Error { - description: error.to_string(), - } - }) - } - - pub fn upload(&self, id: &String, path: &str, archive: Bytes) { - let options = UploadToContainerOptions { - path: path.to_string(), - no_overwrite_dir_non_dir: true.to_string(), - }; - let call = self.api.upload_to_container(id, Some(options), archive); - self.runtime.block_on(call).unwrap(); - } - - pub fn wait_healthy(&self, id: &String) { - println!("Wait container with id {} to be healthy", id); - let operation = || { - let response = self.inspect(id); - match response { - Ok(value) => { - let state = value.state.unwrap(); - let health = state.health.unwrap(); - match health { - Health { status, .. } => { - match status.unwrap() { - HealthStatusEnum::HEALTHY => Ok(()), - _ => Err(pipewire_common::error::Error { - description: "Container not yet healthy".to_string(), - }) - } - } - } - } - Err(value) => Err(pipewire_common::error::Error { - description: format!("Container {} not ready: {}", id, value), - }) - } - }; - let mut backoff = Backoff::default(); - backoff.retry(operation).unwrap() - } - - pub fn exec( - &self, - id: &String, - command: Vec<&str>, - detach: bool, - expected_exit_code: u32, - ) -> Result, pipewire_common::error::Error> { - let create_exec_options = CreateExecOptions { - attach_stdout: Some(true), - attach_stderr: Some(true), - tty: Some(true), - cmd: Some(command), - ..Default::default() - }; - let call = self.api.create_exec(id.as_str(), create_exec_options); - let create_exec_result = self.runtime.block_on(call).unwrap(); - let exec_id = create_exec_result.id; - let start_exec_options = StartExecOptions { - detach, - tty: true, - ..Default::default() - }; - let call = self.api.start_exec(exec_id.as_str(), Some(start_exec_options)); - let start_exec_result = self.runtime.block_on(call).unwrap(); - let mut output_result: Vec = Vec::new(); - if let StartExecResults::Attached { mut output, .. } = start_exec_result { - while let Some(Ok(message)) = self.runtime.block_on(output.next()) { - match message { - LogOutput::StdOut { message } => { - output_result.push( - String::from_utf8(message.to_vec()).unwrap() - ) - } - LogOutput::StdErr { message } => { - eprint!("{}", String::from_utf8(message.to_vec()).unwrap()) - } - LogOutput::Console { message } => { - output_result.push( - String::from_utf8(message.to_vec()).unwrap() - ) - } - _ => {} - } - } - let call = self.api.inspect_exec(exec_id.as_str()); - let exec_inspect_result = self.runtime.block_on(call).unwrap(); - let exit_code = exec_inspect_result.exit_code.unwrap(); - if exit_code != expected_exit_code as i64 { - return Err(pipewire_common::error::Error { - description: format!("Unexpected exit code: {exit_code}"), - }); - } - let output_result = output_result.iter() - .flat_map(move |output| { - output.split('\n') - .map(move |line| line.trim().to_string()) - .collect::>() - }) - .collect::>(); - Ok(output_result) - } else { - Ok(output_result) - } - } - - pub fn top(&self, id: &String) -> HashMap { - let call = self.api.top_processes::<&str>(id, None); - let result = self.runtime.block_on(call).unwrap(); - let titles = result.titles.unwrap(); - let pid_column_index = titles.iter().position(move |title| *title == "PID").unwrap(); - let cmd_column_index = titles.iter().position(move |title| *title == "CMD").unwrap(); - let processes = result.processes.unwrap().iter() - .map(|process| { - let pid = process.get(pid_column_index).unwrap(); - let cmd = process.get(cmd_column_index).unwrap(); - (cmd.clone(), pid.clone()) - }) - .collect::>(); - processes - } - - pub fn wait_for_pid(&self, id: &String, process_name: &str) -> u32 { - let operation = || { - let result = self.top(id); - let pid = result.iter() - .map(move |(cmd, pid)| { - let cmd = cmd.split(" ").collect::>(); - let cmd = *cmd.first().unwrap(); - (cmd, pid.clone()) - }) - .filter(move |(cmd, _)| **cmd == *process_name) - .map(|(_, pid)| pid.parse::().unwrap()) - .collect::>(); - match pid.first() { - Some(value) => Ok(value.clone()), - None => Err(pipewire_common::error::Error { - description: "Process not yet spawned".to_string(), - }) - } - }; - let mut backoff = Backoff::default(); - backoff.retry(operation).unwrap() - } - - fn wait_for_file_type(&self, id: &String, file_type: &str, path: &PathBuf) { - let file_type_argument = match file_type { - "file" => "-f", - "socket" => "-S", - _ => panic!("Cannot determine file type"), - }; - let operation = || { - self.exec( - id, - vec![ - "test", file_type_argument, path.to_str().unwrap() - ], - false, - 0 - )?; - Ok::<(), pipewire_common::error::Error>(()) - }; - let mut backoff = Backoff::default(); - backoff.retry(operation).unwrap() - } - - pub fn wait_for_file(&self, id: &String, path: &PathBuf) { - self.wait_for_file_type(id, "file", path); - } - - pub fn wait_for_socket_file(&self, id: &String, path: &PathBuf) { - self.wait_for_file_type(id, "socket", path); - } - - pub fn wait_for_socket_listening(&self, id: &String, path: &PathBuf) { - let operation = || { - self.exec( - id, - vec![ - "socat", "-u", "OPEN:/dev/null", - format!("UNIX-CONNECT:{}", path.to_str().unwrap()).as_str() - ], - false, - 0 - )?; - Ok::<(), pipewire_common::error::Error>(()) - }; - let mut backoff = Backoff::default(); - backoff.retry(operation).unwrap() - } - - pub fn wait_for_command_output(&self, id: &String, command: Vec<&str>, expected_output: &str) { - let operation = || { - let command_output = self.exec( - id, - command.clone(), - false, - 0 - )?; - return if command_output.iter().any(|output| { - let output = output.trim(); - output == expected_output - }) { - Ok(()) - } else { - Err(pipewire_common::error::Error { - description: format!("Unexpected output {}", expected_output) - }) - }; - }; - let mut backoff = Backoff::default(); - backoff.retry(operation).unwrap() - } -} - -impl Clone for ContainerApi { - fn clone(&self) -> Self { - Self { - runtime: self.runtime.clone(), - api: self.api.clone(), - } - } -} \ No newline at end of file diff --git a/pipewire-test-utils/src/containers/mod.rs b/pipewire-test-utils/src/containers/mod.rs deleted file mode 100644 index 40c67f24d..000000000 --- a/pipewire-test-utils/src/containers/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod container; -pub mod options; -pub mod sync_api; \ No newline at end of file diff --git a/pipewire-test-utils/src/containers/options.rs b/pipewire-test-utils/src/containers/options.rs deleted file mode 100644 index 9211172b2..000000000 --- a/pipewire-test-utils/src/containers/options.rs +++ /dev/null @@ -1,269 +0,0 @@ -use bollard::container::{Config, StopContainerOptions}; -use bollard::models::{HealthConfig, HostConfig, Mount, MountBindOptions, MountTypeEnum, ResourcesUlimits}; -use std::collections::HashMap; -use std::time::Duration; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Unit { - Bytes, - KB, - MB, - GB, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Size { - value: f64, - unit: Unit, -} - -impl Size { - const fn new(value: f64, unit: Unit) -> Self { - Self { value, unit } - } - - pub const fn from_bytes(bytes: f64) -> Self { - Self::new(bytes, Unit::Bytes) - } - - pub const fn from_kb(kb: f64) -> Self { - Self::new(kb, Unit::KB) - } - - pub const fn from_mb(mb: f64) -> Self { - Self::new(mb, Unit::MB) - } - - pub const fn from_gb(gb: f64) -> Self { - Self::new(gb, Unit::GB) - } -} - -impl From for Size { - fn from(value: String) -> Self { - let value = value.trim(); - let unit = value.chars().last().unwrap(); - let unit = match unit { - 'b' => Unit::Bytes, - 'k' => Unit::KB, - 'm' => Unit::MB, - 'g' => Unit::GB, - _ => panic!("Invalid unit {:?}. Only b, k, m, g are supported.", unit), - }; - let value = value.chars().take(value.len() - 2).collect::(); - let value = value.parse::().unwrap(); - Self::new(value, unit) - } -} - -impl From for u64 { - fn from(value: Size) -> Self { - match value.unit { - Unit::Bytes => value.value as u64, - Unit::KB => (value.value * 1024.0) as u64, - Unit::MB => (value.value * 1024.0 * 1024.0) as u64, - Unit::GB => (value.value * 1024.0 * 1024.0 * 1024.0) as u64, - } - } -} - -impl From for i64 { - fn from(value: Size) -> Self { - let value: u64 = value.into(); - value as i64 - } -} - -pub struct CreateContainerOptionsBuilder { - image: Option, - environment: Option>, - volumes: Option>, - labels: Option>, - entrypoint: Option>, - healthcheck: Option>, - cpus: Option, - memory_swap: Option, - memory: Option -} - -impl Default for CreateContainerOptionsBuilder { - fn default() -> Self { - Self { - image: None, - environment: None, - volumes: None, - labels: None, - entrypoint: None, - healthcheck: None, - cpus: None, - memory_swap: None, - memory: None, - } - } -} - -impl CreateContainerOptionsBuilder { - pub fn with_image(&mut self, image: impl Into) -> &mut Self { - self.image = Some(image.into()); - self - } - - pub fn with_environment(&mut self, key: impl Into, value: impl Into) -> &mut Self { - if let None = self.environment { - self.environment = Some(HashMap::new()); - } - if let Some(environment) = self.environment.as_mut() { - environment.insert(key.into(), value.into()); - } - self - } - - pub fn with_volume(&mut self, name: impl Into, container_path: impl Into) -> &mut Self { - if let None = self.volumes { - self.volumes = Some(HashMap::new()); - } - if let Some(volumes) = self.volumes.as_mut() { - volumes.insert(name.into(), container_path.into()); - } - self - } - - pub fn with_label(&mut self, key: impl Into, value: impl Into) -> &mut Self { - if let None = self.labels { - self.labels = Some(HashMap::new()); - } - if let Some(labels) = self.labels.as_mut() { - labels.insert(key.into(), value.into()); - } - self - } - - pub fn with_entrypoint(&mut self, value: impl Into) -> &mut Self { - if let None = self.entrypoint { - self.entrypoint = Some(Vec::new()); - } - if let Some(entrypoint) = self.entrypoint.as_mut() { - let mut value = value.into().split(" ") - .map(|s| s.to_string()) - .collect::>(); - entrypoint.append(&mut value); - } - self - } - - pub fn with_healthcheck_command(&mut self, value: impl Into) -> &mut Self { - if let None = self.healthcheck { - self.healthcheck = Some(Vec::new()); - } - if let Some(healthcheck) = self.healthcheck.as_mut() { - healthcheck.clear(); - healthcheck.push("CMD".to_string()); - healthcheck.push(value.into()); - } - self - } - - pub fn with_healthcheck_command_shell(&mut self, value: impl Into) -> &mut Self { - if let None = self.healthcheck { - self.healthcheck = Some(Vec::new()); - } - if let Some(healthcheck) = self.healthcheck.as_mut() { - healthcheck.clear(); - healthcheck.push("CMD-SHELL".to_string()); - healthcheck.push(value.into()); - } - self - } - - pub fn with_cpus(&mut self, cpus: f64) -> &mut Self { - self.cpus = Some(cpus); - self - } - - pub fn with_memory_swap(&mut self, memory_swap: Size) -> &mut Self { - self.memory_swap = Some(memory_swap); - self - } - - pub fn with_memory(&mut self, memory: Size) -> &mut Self { - self.memory = Some(memory); - self - } - - pub fn build(&self) -> Config { - if self.image.is_none() { - panic!("Image is required"); - } - let mut builder = Config::default(); - builder.image = self.image.clone(); - builder.host_config = Some(HostConfig::default()); - if let Some(environment) = self.environment.as_ref() { - let environment = environment.iter() - .map(|(k, v)| format!("{}={}", k, v)) - .collect::>(); - builder.env = Some(environment); - } - if let Some(volumes) = self.volumes.as_ref() { - let mut volumes = volumes.iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>(); - let host_config = builder.host_config.as_mut().unwrap(); - if let None = host_config.binds { - host_config.binds = Some(Vec::new()) - } - if let Some(ref mut binds) = &mut host_config.binds { - binds.append(&mut volumes) - } - } - if let Some(labels) = self.labels.as_ref() { - builder.labels = Some(labels.clone()); - } - if let Some(entrypoint) = self.entrypoint.as_ref() { - builder.entrypoint = Some(entrypoint.clone()); - } - if let Some(healthcheck) = self.healthcheck.as_ref() { - builder.healthcheck = Some(HealthConfig { - test: Some(healthcheck.clone()), - ..HealthConfig::default() - }); - } - if let Some(cpus) = self.cpus.clone() { - let host_config = builder.host_config.as_mut().unwrap(); - host_config.nano_cpus = Some((1_000_000_000.0 * cpus) as i64); - } - if let Some(memory_swap) = self.memory_swap.clone() { - let host_config = builder.host_config.as_mut().unwrap(); - host_config.memory_swap = Some(memory_swap.into()); - } - if let Some(memory) = self.memory.clone() { - let host_config = builder.host_config.as_mut().unwrap(); - host_config.memory = Some(memory.into()); - } - builder - } -} - -pub struct StopContainerOptionsBuilder { - wait: Option, -} - -impl Default for StopContainerOptionsBuilder { - fn default() -> Self { - Self { - wait: Some(Duration::from_secs(0)), - } - } -} - -impl StopContainerOptionsBuilder { - pub fn with_wait(&mut self, time: Duration) -> &mut Self { - self.wait = Some(time); - self - } - - pub fn build(&self) -> StopContainerOptions { - let mut builder = StopContainerOptions::default(); - builder.t = self.wait.unwrap().as_secs() as i64; - builder - } -} \ No newline at end of file diff --git a/pipewire-test-utils/src/containers/sync_api.rs b/pipewire-test-utils/src/containers/sync_api.rs deleted file mode 100644 index aae966767..000000000 --- a/pipewire-test-utils/src/containers/sync_api.rs +++ /dev/null @@ -1,414 +0,0 @@ -use bollard::container::{RemoveContainerOptions, StopContainerOptions}; -use bollard::errors::Error; -use bollard::errors::Error::{DockerResponseServerError, RequestTimeoutError}; -use bollard::{ClientVersion}; -use bytes::{Buf, Bytes}; -use http::header::CONTENT_TYPE; -use http::{Method, Request, Response, StatusCode, Version}; -use pipewire_common::utils::Backoff; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use std::ffi::OsStr; -use std::sync::atomic::AtomicUsize; -use std::sync::Arc; -use std::{fmt, io}; -use std::path::Path; -use http::request::Builder; -use ureq::{Agent, Body, RequestBuilder, SendBody}; -use ureq::middleware::MiddlewareNext; -use ureq::typestate::{WithBody, WithoutBody}; -use url::Url; - -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -struct DockerServerErrorMessage { - message: String, -} - -pub trait Connector { - fn execute(&self, request: &mut Request) -> http::Result>; -} - -pub struct HttpConnector { - agent: Agent -} - -impl HttpConnector { - pub fn new() -> Self { - let config = Agent::config_builder() - .build(); - let agent = config.into(); - Self { - agent, - } - } - - fn request_builder(&self, request: &mut Request) -> ureq::RequestBuilder { - if request.method() == http::Method::GET { - self.agent.get(request.uri()) - .force_send_body() - } - else if request.method() == http::Method::POST { - self.agent.post(request.uri()) - } - else if request.method() == http::Method::PUT { - self.agent.put(request.uri()) - } - else if request.method() == http::Method::PATCH { - self.agent.patch(request.uri()) - } - else if request.method() == http::Method::DELETE { - self.agent.delete(request.uri()).force_send_body() - } - else if request.method() == http::Method::HEAD { - self.agent.head(request.uri()).force_send_body() - } - else if request.method() == http::Method::OPTIONS { - self.agent.options(request.uri()).force_send_body() - } - else { - panic!("Not supported HTTP method") - } - } -} - -impl Connector for HttpConnector -{ - fn execute(&self, mut request: &mut Request) -> http::Result> { - let mut builder = self.request_builder(request) - .version(Version::HTTP_11) - .uri(request.uri()); - for (key, value) in request.headers() { - builder = builder.header(key.as_str(), value.to_str().unwrap()); - } - let mut data = Vec::new(); - let mut bytes = request.body_mut().reader(); - io::copy(&mut bytes, &mut data).unwrap(); - let body = Body::builder() - .data(data); - let mut response = builder.send(body).unwrap(); - let mut builder = http::Response::builder() - .status(response.status()) - .version(response.version()); - for (key, value) in response.headers().iter() { - builder = builder.header(key.as_str(), value.to_str().unwrap()); - } - let mut data = Vec::new(); - let mut reader = response.body_mut().as_reader(); - io::copy(&mut reader, &mut data).unwrap(); - let bytes = Bytes::from(data); - builder.body(bytes) - } -} - -pub struct UnixConnector { - -} - -impl UnixConnector { - pub fn new() -> Self { - Self { - - } - } -} - -impl Connector for UnixConnector { - fn execute(&self, request: &mut Request) -> http::Result> { - todo!() - } -} - -pub struct Client - where C: Connector -{ - connector: C, -} - -impl Client -where - C: Connector -{ - pub fn new(connector: C) -> Self { - Self { - connector, - } - } - - pub fn execute(&self, request: &mut Request) -> http::Result> { - self.connector.execute(request) - } -} - -pub(crate) enum Transport { - Http { - client: Client, - }, - Unix { - client: Client, - }, -} - -impl fmt::Debug for Transport { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Transport::Http { .. } => write!(f, "HTTP"), - Transport::Unix { .. } => write!(f, "Unix"), - } - } -} - -#[derive(Debug, Clone)] -pub(crate) enum ClientType { - Unix, - Http, -} - -#[derive(Debug)] -pub struct Uri<'a> { - encoded: Cow<'a, str>, -} - -impl<'a> Uri<'a> { - pub(crate) fn parse( - socket: &'a str, - client_type: &ClientType, - path: &'a str, - query: Option, - client_version: &ClientVersion, - ) -> Result - where - O: serde::ser::Serialize, - { - let host_str = format!( - "{}://{}/v{}.{}{}", - Uri::socket_scheme(client_type), - Uri::socket_host(socket, client_type), - client_version.major_version, - client_version.minor_version, - path - ); - let mut url = Url::parse(host_str.as_ref())?; - url = url.join(path)?; - - if let Some(pairs) = query { - let qs = serde_urlencoded::to_string(pairs)?; - url.set_query(Some(&qs)); - } - Ok(Uri { - encoded: Cow::Owned(url.as_str().to_owned()), - }) - } - - fn socket_host

(socket: P, client_type: &ClientType) -> String - where - P: AsRef, - { - match client_type { - ClientType::Http => socket.as_ref().to_string_lossy().into_owned(), - ClientType::Unix => hex::encode(socket.as_ref().to_string_lossy().as_bytes()), - } - } - - fn socket_scheme(client_type: &'a ClientType) -> &'a str { - match client_type { - ClientType::Http => "http", - ClientType::Unix => "unix", - } - } -} - -pub struct SyncContainerApi { - pub(crate) transport: Arc, - pub(crate) client_type: ClientType, - pub(crate) client_addr: String, - pub(crate) client_timeout: u64, - pub(crate) version: Arc<(AtomicUsize, AtomicUsize)>, -} - -impl SyncContainerApi { - pub fn connect_with_http( - addr: &str, - timeout: u64, - client_version: &ClientVersion, - ) -> Result { - let client_addr = addr.replacen("tcp://", "", 1).replacen("http://", "", 1); - let http_connector = HttpConnector::new(); - let client = Client::new(http_connector); - let transport = Transport::Http { client }; - let docker = Self { - transport: Arc::new(transport), - client_type: ClientType::Http, - client_addr, - client_timeout: timeout, - version: Arc::new(( - AtomicUsize::new(client_version.major_version), - AtomicUsize::new(client_version.minor_version), - )), - }; - Ok(docker) - } - - pub fn connect_with_socket( - path: &str, - timeout: u64, - client_version: &ClientVersion, - ) -> Result { - let clean_path = path.trim_start_matches("unix://"); - if !std::path::Path::new(clean_path).exists() { - return Err(Error::SocketNotFoundError(clean_path.to_string())); - } - let docker = Self::connect_with_unix(path, timeout, client_version)?; - Ok(docker) - } - - pub fn connect_with_unix( - path: &str, - timeout: u64, - client_version: &ClientVersion, - ) -> Result { - let client_addr = path.replacen("unix://", "", 1); - if !Path::new(&client_addr).exists() { - return Err(Error::SocketNotFoundError(client_addr)); - } - let unix_connector = UnixConnector::new(); - let client = Client::new(unix_connector); - let transport = Transport::Unix { client }; - let docker = Self { - transport: Arc::new(transport), - client_type: ClientType::Unix, - client_addr, - client_timeout: timeout, - version: Arc::new(( - AtomicUsize::new(client_version.major_version), - AtomicUsize::new(client_version.minor_version), - )), - }; - Ok(docker) - } - - pub fn client_version(&self) -> ClientVersion { - self.version.as_ref().into() - } - - pub fn stop( - &self, - container_name: &str, - options: Option, - ) -> Result<(), Error> { - let url = format!("/containers/{container_name}/stop"); - - let req = self.build_request( - &url, - Builder::new().method(Method::POST), - options, - Ok(Bytes::new()), - ); - self.process_request(req)?; - Ok(()) - } - - pub fn remove( - &self, - container_name: &str, - options: Option, - ) -> Result<(), Error> { - let url = format!("/containers/{container_name}"); - let req = self.build_request( - &url, - Builder::new().method(Method::DELETE), - options, - Ok(Bytes::new()), - ); - self.process_request(req)?; - Ok(()) - } - - pub(crate) fn build_request( - &self, - path: &str, - builder: Builder, - query: Option, - payload: Result, - ) -> Result, Error> - where - O: Serialize, - { - let uri = Uri::parse( - &self.client_addr, - &self.client_type, - path, - query, - &self.client_version(), - )?; - Ok(builder - .uri(uri.encoded.to_string()) - .header(CONTENT_TYPE, "application/json") - .body(payload?)?) - } - - pub(crate) fn process_request( - &self, - request: Result, Error>, - ) -> Result, Error> { - let transport = self.transport.clone(); - let timeout = self.client_timeout; - - let mut request = request?; - let response = Self::execute_request(transport, &mut request, timeout)?; - - let status = response.status(); - match status { - // Status code 200 - 299 or 304 - s if s.is_success() || s == StatusCode::NOT_MODIFIED => Ok(response), - - StatusCode::SWITCHING_PROTOCOLS => Ok(response), - - // All other status codes - _ => { - let contents = Self::decode_into_string(response)?; - - let mut message = String::new(); - if !contents.is_empty() { - message = serde_json::from_str::(&contents) - .map(|msg| msg.message) - .or_else(|e| { - if e.is_data() || e.is_syntax() { - Ok(contents) - } else { - Err(e) - } - })?; - } - Err(DockerResponseServerError { - status_code: status.as_u16(), - message, - }) - } - } - } - - fn execute_request( - transport: Arc, - request: &mut Request, - timeout: u64, - ) -> Result, Error> { - let operation = || { - let request = match *transport { - Transport::Http { ref client } => client.execute(request), - Transport::Unix { ref client } => client.execute(request), - }; - let request = request.map_err(Error::from); - match request { - Ok(value) => Ok(value), - Err(value) => Err(value), - } - }; - let mut backoff = Backoff::constant((timeout * 1000) as u128); - backoff.retry(operation).map_err(|_| RequestTimeoutError) - } - - fn decode_into_string(response: Response) -> Result { - let body = response.into_body(); - Ok(String::from_utf8_lossy(&body).to_string()) - } -} \ No newline at end of file diff --git a/pipewire-test-utils/src/environment.rs b/pipewire-test-utils/src/environment.rs deleted file mode 100644 index f40fce89f..000000000 --- a/pipewire-test-utils/src/environment.rs +++ /dev/null @@ -1,231 +0,0 @@ -use std::sync::{Arc, LazyLock, Mutex}; -use std::time::Duration; -use bollard::{Docker, API_DEFAULT_VERSION}; -use ctor::{ctor, dtor}; -use libc::{atexit, signal, SIGABRT, SIGINT, SIGSEGV, SIGTERM}; -use tokio::runtime::{Runtime}; -use pipewire_common::error::Error; -use url::Url; -use crate::containers::container::{ContainerApi, ContainerRegistry, ImageApi, ImageRegistry}; -use crate::containers::options::{Size}; -use crate::containers::sync_api::SyncContainerApi; -use crate::server::{server_with_default_configuration, Server}; - -pub static SHARED_SERVER: LazyLock> = LazyLock::new(move || { - let server = server_with_default_configuration(); - server -}); - -pub static TEST_ENVIRONMENT: LazyLock> = LazyLock::new(|| { - unsafe { - signal(SIGINT, cleanup_test_environment as usize); - signal(SIGTERM, cleanup_test_environment as usize); - } - unsafe { libc::printf("Initialize test environment\n\0".as_ptr() as *const i8); }; - Mutex::new(Environment::from_env()) -}); - -#[dtor] -unsafe fn cleanup_test_environment() { - libc::printf("Cleaning test environment\n\0".as_ptr() as *const i8); - let environment = TEST_ENVIRONMENT.lock().unwrap(); - environment.container_image_registry.cleanup(); - SHARED_SERVER.cleanup(); -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TestTarget { - Local, - Container, -} - -impl From for TestTarget { - fn from(value: String) -> Self { - match value.as_str() { - "local" => TestTarget::Local, - "container" => TestTarget::Container, - _ => panic!("Unknown test target {}", value), - } - } -} - -pub struct Environment { - pub runtime: Arc, - pub container_api: ContainerApi, - pub container_image_api: ImageApi, - pub container_api_timeout: Duration, - pub container_registry: ContainerRegistry, - pub container_image_registry: ImageRegistry, - pub container_cpu: f64, - pub container_memory: Size, - pub container_memory_swap: Size, - pub test_target: TestTarget, - pub client_timeout: Duration, -} - -impl Environment { - pub fn from_env() -> Self { - let default = Self::default(); - let container_api_timeout = match std::env::var("CONTAINER_API_TIMEOUT") { - Ok(value) => Self::parse_duration(value), - Err(_) => default.container_api_timeout - }; - let container_cpu = match std::env::var("CONTAINER_CPU") { - Ok(value) => value.parse::().unwrap(), - Err(_) => default.container_cpu, - }; - let container_memory = match std::env::var("CONTAINER_MEMORY") { - Ok(value) => value.into(), - Err(_) => default.container_memory - }; - let container_memory_swap = match std::env::var("CONTAINER_MEMORY_SWAP") { - Ok(value) => value.into(), - Err(_) => default.container_memory - }; - let test_target = match std::env::var("TEST_TARGET") { - Ok(value) => value.into(), - Err(_) => default.test_target.clone(), - }; - Self { - runtime: default.runtime.clone(), - container_api: default.container_api, - container_image_api: default.container_image_api, - container_api_timeout, - container_registry: default.container_registry, - container_image_registry: default.container_image_registry, - container_cpu, - container_memory, - container_memory_swap, - test_target, - client_timeout: default.client_timeout, - } - } - - fn parse_duration(value: String) -> Duration { - let value = value.trim(); - let suffix_length = value.strip_suffix("ms") - .map_or_else(|| 1, |_| 2); - let suffix_start_index = value.len() - suffix_length; - let unit = value.get(suffix_start_index..).unwrap(); - let value = value.get(..suffix_start_index) - .unwrap() - .parse::() - .unwrap(); - match unit { - "ms" => Duration::from_millis(value), - "s" => Duration::from_secs(value), - "m" => Duration::from_secs(value * 60), - _ => panic!("Invalid unit {:?}. Only ms, s, m are supported.", unit), - } - } - - fn parse_container_host( - on_http: impl FnOnce(&String) -> T, - on_socket: impl FnOnce(&String) -> T, - ) -> Result, Error> { - const DOCKER_HOST_ENVIRONMENT_KEY: &str = "DOCKER_HOST"; - const CONTAINER_HOST_ENVIRONMENT_KEY: &str = "CONTAINER_HOST"; - - let docker_host = std::env::var(DOCKER_HOST_ENVIRONMENT_KEY); - let container_host = std::env::var(CONTAINER_HOST_ENVIRONMENT_KEY); - let host = match (docker_host, container_host) { - (Ok(value), Ok(_)) => value, - (Ok(value), Err(_)) => value, - (Err(_), Ok(value)) => value, - (Err(_), Err(_)) => return Err(Error { - description: format!( - "${} or ${} should be set.", - DOCKER_HOST_ENVIRONMENT_KEY, CONTAINER_HOST_ENVIRONMENT_KEY - ) - }), - }; - let host_url = Url::parse(host.as_str()).unwrap(); - let api = match host_url.scheme() { - "http" | "tcp" => on_http(&host), - "unix" => on_socket(&host), - _ => return Err(Error { - description: format!("Unsupported uri format {}", host_url), - }), - }; - Ok(Arc::new(api)) - } - - pub fn create_runtime() -> Arc { - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .max_blocking_threads(1) - .worker_threads(1) - .build() - .unwrap(); - Arc::new(runtime) - } - - pub fn create_docker_api(timeout: &Duration) -> Arc { - let on_http = |host: &String| { - Docker::connect_with_http( - host.as_str(), - timeout.as_secs(), - API_DEFAULT_VERSION - ).unwrap() - }; - let on_socket = |host: &String| { - Docker::connect_with_unix( - host.as_str(), - timeout.as_secs(), - API_DEFAULT_VERSION - ).unwrap() - }; - match Self::parse_container_host(on_http, on_socket) { - Ok(value) => value, - Err(value) => panic!("{}", value), - } - } - - pub fn create_docker_sync_api(timeout: &Duration) -> Arc { - let on_http = |host: &String| { - SyncContainerApi::connect_with_http( - host.as_str(), - timeout.as_secs(), - API_DEFAULT_VERSION - ).unwrap() - }; - let on_socket = |host: &String| { - SyncContainerApi::connect_with_unix( - host.as_str(), - timeout.as_secs(), - API_DEFAULT_VERSION - ).unwrap() - }; - match Self::parse_container_host(on_http, on_socket) { - Ok(value) => value, - Err(value) => panic!("{}", value), - } - } - - pub fn create_container_api(runtime: Arc, api: Arc) -> ContainerApi { - ContainerApi::new(runtime.clone(), api.clone()) - } -} - -impl Default for Environment { - fn default() -> Self { - let timeout = Duration::from_secs(30); - let runtime = Self::create_runtime(); - let api_runtime = Self::create_runtime(); - let docker_api = Self::create_docker_api(&timeout); - let container_api = Self::create_container_api(api_runtime.clone(), docker_api.clone()); - Self { - runtime, - container_api: container_api.clone(), - container_image_api: ImageApi::new(api_runtime.clone(), docker_api), - container_api_timeout: timeout, - container_registry: ContainerRegistry::new(container_api.clone()), - container_image_registry: ImageRegistry::new(), - container_cpu: 2.0, - container_memory: Size::from_mb(100.0), - container_memory_swap: Size::from_mb(100.0), - test_target: TestTarget::Container, - client_timeout: Duration::from_secs(3), - } - } -} \ No newline at end of file diff --git a/pipewire-test-utils/src/lib.rs b/pipewire-test-utils/src/lib.rs deleted file mode 100644 index 475e6b167..000000000 --- a/pipewire-test-utils/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::fmt; -use std::fmt::Display; - -pub mod containers; -pub mod server; -pub mod environment; - -pub(crate) struct HexSlice<'a>(&'a [u8]); - -impl<'a> Display for HexSlice<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for &byte in self.0 { - write!(f, "{:0>2x}", byte)?; - } - Ok(()) - } -} \ No newline at end of file diff --git a/pipewire-test-utils/src/server.rs b/pipewire-test-utils/src/server.rs deleted file mode 100644 index 910b03c7a..000000000 --- a/pipewire-test-utils/src/server.rs +++ /dev/null @@ -1,730 +0,0 @@ -use crate::containers::container::{ContainerApi, ImageApi, CONTAINER_PATH, CONTAINER_TMP_PATH}; -use crate::containers::options::{CreateContainerOptionsBuilder, StopContainerOptionsBuilder}; -use crate::environment::{Environment, TestTarget, TEST_ENVIRONMENT}; -use bytes::Bytes; -use pipewire_common::constants::*; -use pipewire_common::impl_callback; -use pipewire_common::error::Error; -use rstest::fixture; -use std::fmt::{Debug, Formatter}; -use std::fs; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, LazyLock, Mutex}; -use std::time::Duration; -use bollard::container::RemoveContainerOptions; -use tar::{Builder, EntryType, Header}; -use tokio::runtime::Runtime; -use uuid::Uuid; - -static CONTAINER_FILE_PATH: LazyLock = LazyLock::new(|| { - CONTAINER_PATH - .clone() - .join("pipewire.test.container") - .as_path() - .to_path_buf() -}); - -static PIPEWIRE_SERVICE: LazyLock = LazyLock::new(move || { - let service = Service::new( - "pipewire".to_string(), - |server| { - server.spawn("pipewire") - .build(); - }, - |server| { - server.wait_for_pipewire() - }, - ); - service -}); - -static WIREPLUMBER_SERVICE: LazyLock = LazyLock::new(move || { - let service = Service::new( - "wireplumber".to_string(), - |server| { - server.spawn("wireplumber") - .build(); - }, - |server| { - server.wait_for_wireplumber() - }, - ); - service -}); - -static PULSE_SERVICE: LazyLock = LazyLock::new(move || { - let service = Service::new( - "pulse".to_string(), - |server| { - server.spawn("pipewire-pulse") - .build(); - }, - |server| { - server.wait_for_pulse(); - } - ); - service -}); - -struct SpawnCommandBuilder<'a> { - configuration: &'a mut Vec>, - command: Option, - arguments: Option>, - realtime_priority: Option, - user: Option, - auto_start: Option, - auto_restart: Option, - start_retries: Option, -} - -impl <'a> SpawnCommandBuilder<'a> { - pub fn new(configuration: &'a mut Vec>) -> Self { - Self { - configuration, - command: None, - arguments: None, - user: None, - realtime_priority: None, - auto_start: None, - auto_restart: None, - start_retries: None, - } - } - - pub fn with_command(&mut self, command: &str) -> &mut Self { - self.command = Some(command.to_string()); - self - } - - pub fn with_arguments(&mut self, arguments: Vec<&str>) -> &mut Self { - self.arguments = Some(arguments.iter().map(|s| s.to_string()).collect()); - self - } - - pub fn with_realtime_priority(&mut self, priority: u32) -> &mut Self { - self.realtime_priority = Some(priority); - self - } - - pub fn with_user(&mut self, user: &str) -> &mut Self { - self.user = Some(user.to_string()); - self - } - - pub fn with_auto_start(&mut self, auto_start: bool) -> &mut Self { - self.auto_start = Some(auto_start); - self - } - - pub fn with_auto_restart(&mut self, auto_restart: bool) -> &mut Self { - self.auto_restart = Some(auto_restart); - self - } - - pub fn with_start_retries(&mut self, start_retries: u32) -> &mut Self { - self.start_retries = Some(start_retries); - self - } - - pub fn build(&mut self) { - if self.command.is_none() { - panic!("Command is required"); - } - let command = self.command.as_ref().unwrap(); - let process_name = PathBuf::from(command); - let process_name = process_name.file_name().unwrap().to_str().unwrap(); - let command = match self.arguments.as_ref() { - Some(value) => format!("{} {}", command, value.join(" ")), - None => command.to_string(), - }; - let command = match self.realtime_priority.as_ref() { - Some(value) => format!("chrt {} {}", value, command), - None => command, - }; - let mut configuration = Vec::new(); - configuration.append(&mut vec![ - format!("[program:{}]", process_name), - format!("command={}", command), - format!("stdout_logfile=/tmp/{}.out.log", process_name), - format!("stderr_logfile=/tmp/{}.err.log", process_name), - ]); - match self.user.as_ref() { - Some(value) => configuration.push(format!("user={}", value.to_string()).to_string()), - None => {} - }; - match self.auto_start { - Some(value) => configuration.push(format!("autostart={}", value.to_string()).to_string()), - None => {} - }; - match self.auto_restart { - Some(value) => configuration.push(format!("autorestart={}", value.to_string()).to_string()), - None => {} - }; - match self.start_retries { - Some(value) => configuration.push(format!("startretries={}", value.to_string()).to_string()), - None => {} - }; - self.configuration.push(configuration) - } -} - -impl_callback!( - Fn => (), - LifeCycleCallback, - server : &mut ServerApi -); - -pub struct Service { - name: String, - entrypoint: LifeCycleCallback, - healthcheck: LifeCycleCallback, -} - -impl Service { - pub fn new( - name: String, - entrypoint: impl Fn(&mut ServerApi) + Sync + Send + 'static, - healthcheck: impl Fn(&mut ServerApi) + Sync + Send + 'static, - ) -> Self { - Self { - name, - entrypoint: LifeCycleCallback::from(entrypoint), - healthcheck: LifeCycleCallback::from(healthcheck), - } - } - - pub fn entrypoint(&mut self, server: &mut ServerApi) { - self.entrypoint.call(server); - } - - pub fn healthcheck(&mut self, server: &mut ServerApi) { - self.healthcheck.call(server); - } -} - -impl Clone for Service { - fn clone(&self) -> Self { - Self { - name: self.name.clone(), - entrypoint: self.entrypoint.clone(), - healthcheck: self.healthcheck.clone(), - } - } -} - -pub struct ServerApi { - name: String, - tag: String, - socket_id: Uuid, - container_file_path: PathBuf, - image_api: ImageApi, - container_api: ContainerApi, - container: Option, - configuration: Vec>, - entrypoint: Vec>, - healthcheck: Vec>, - post_start: Vec>, -} - -impl ServerApi { - pub(self) fn new( - name: String, - container_file_path: PathBuf, - ) -> Self { - let environment = TEST_ENVIRONMENT.lock().unwrap(); - Self { - name, - tag: "latest".to_string(), - socket_id: Uuid::new_v4(), - container_file_path, - image_api: environment.container_image_api.clone(), - container_api: environment.container_api.clone(), - container: None, - configuration: Vec::new(), - entrypoint: Vec::new(), - healthcheck: Vec::new(), - post_start: Vec::new(), - } - } - - fn socket_location(&self) -> PathBuf { - Path::new("/run/pipewire-sockets").join(self.socket_id.to_string()).to_path_buf() - } - - fn socket_name(&self) -> String { - format!("{}", self.socket_id) - } - - fn build(&self) { - self.generate_configuration_file(); - self.generate_entrypoint_script(); - self.generate_healthcheck_script(); - self.image_api.build( - &self.container_file_path, - &self.name, - &self.tag, - ); - } - - fn create(&mut self) { - let environment = TEST_ENVIRONMENT.lock().unwrap(); - let socket_location = self.socket_location(); - let socket_name = self.socket_name().to_string(); - let pulse_socket_location = socket_location.join("pulse"); - let mut create_options = CreateContainerOptionsBuilder::default(); - create_options - .with_image(format!("{}:{}", self.name, self.tag)) - .with_environment(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, socket_location.to_str().unwrap()) - .with_environment(PIPEWIRE_CORE_ENVIRONMENT_KEY, socket_name.clone()) - .with_environment(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, socket_name.clone()) - .with_environment(PULSE_RUNTIME_PATH_ENVIRONMENT_KEY, pulse_socket_location.to_str().unwrap()) - .with_environment("DISABLE_RTKIT", "y") - .with_environment("DISPLAY", ":0.0") - .with_volume("pipewire-sockets", socket_location.parent().unwrap().to_str().unwrap()) - .with_cpus(environment.container_cpu) - .with_memory_swap(environment.container_memory_swap) - .with_memory(environment.container_memory_swap); - drop(environment); - self.container = Some(self.container_api.create(&mut create_options)); - } - - fn start(&self) { - self.container_api.start(self.container.as_ref().unwrap()); - self.container_api.wait_healthy(self.container.as_ref().unwrap()); - } - - fn stop(&self) { - let mut options = StopContainerOptionsBuilder::default(); - self.container_api.stop(self.container.as_ref().unwrap(), &mut options); - } - - fn restart(&self) { - self.container_api.restart(self.container.as_ref().unwrap()) - } - - fn cleanup(&self) { - let docker_api = Environment::create_docker_sync_api(&Duration::from_millis(100)); - let stop_options = StopContainerOptionsBuilder::default().build(); - docker_api.stop(&self.container.as_ref().unwrap(), Some(stop_options)).unwrap(); - let remove_options = RemoveContainerOptions { - ..Default::default() - }; - docker_api.remove(&self.container.as_ref().unwrap(), Some(remove_options)).unwrap(); - } - - fn spawn(&mut self, command: &str) -> SpawnCommandBuilder<'_> { - let mut builder = SpawnCommandBuilder::new(&mut self.configuration); - builder.with_command(command) - .with_auto_start(true) - .with_auto_restart(true); - builder - } - - fn spawn_wait_loop(&mut self) { - self.entrypoint.push(vec![ - "supervisord".to_string(), - "-c".to_string(), - "/root/supervisor.conf".to_string(), - ]); - } - - fn create_folder(&mut self, path: &PathBuf) { - self.entrypoint.push(vec![ - "mkdir".to_string(), - "--parents".to_string(), - path.to_str().unwrap().to_string(), - ]); - } - - fn create_socket_folder(&mut self) { - self.entrypoint.push(vec![ - "mkdir".to_string(), - "--parents".to_string(), - format!("${{{}}}", PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY), - ]); - } - - fn remove_socket_folder(&mut self) { - self.entrypoint.push(vec![ - "rm".to_string(), - "--force".to_string(), - "--recursive".to_string(), - format!("${{{}}}", PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY), - ]); - } - - fn set_virtual_nodes_configuration(&mut self) { - self.entrypoint.push(vec![ - "mkdir".to_string(), - "--parents".to_string(), - "/etc/pipewire/pipewire.conf.d/".to_string(), - ]); - self.entrypoint.push(vec![ - "cp".to_string(), - "/root/virtual.nodes.conf".to_string(), - "/etc/pipewire/pipewire.conf.d/virtual.nodes.conf".to_string(), - ]); - } - - fn set_default_nodes(&mut self) { - self.post_start.push(vec![ - "echo".to_string(), - "'wait for test-sink'".to_string() - ]); - self.post_start.push(vec![ - "pactl".to_string(), - "set-default-sink".to_string(), - "'test-sink'".to_string(), - ]); - self.post_start.push(vec![ - "wpctl".to_string(), - "status".to_string(), - "|".to_string(), - "grep".to_string(), - "--quiet".to_string(), - "'test-sink'".to_string() - ]); - self.post_start.push(vec![ - "echo".to_string(), - "'wait for test-source'".to_string() - ]); - self.post_start.push(vec![ - "pactl".to_string(), - "set-default-source".to_string(), - "'test-source'".to_string(), - ]); - self.post_start.push(vec![ - "wpctl".to_string(), - "status".to_string(), - "|".to_string(), - "grep".to_string(), - "--quiet".to_string(), - "'test-source'".to_string() - ]); - } - - fn wait_for_pipewire(&mut self) { - self.healthcheck.push(vec![ - "echo".to_string(), - "'wait for pipewire'".to_string() - ]); - self.healthcheck.push(vec![ - "pw-cli".to_string(), - "ls".to_string(), - "0".to_string(), - "|".to_string(), - "grep".to_string(), - "--quiet".to_string(), - "'id 0, type PipeWire:Interface:Core/4'".to_string() - ]) - } - - fn wait_for_wireplumber(&mut self) { - self.healthcheck.push(vec![ - "echo".to_string(), - "'wait for wireplumbler'".to_string() - ]); - self.healthcheck.push(vec![ - "wpctl".to_string(), - "info".to_string(), - "|".to_string(), - "grep".to_string(), - "--quiet".to_string(), - "'WirePlumber'".to_string() - ]) - } - - fn wait_for_pulse(&mut self) { - self.healthcheck.push(vec![ - "echo".to_string(), - "'wait for PipeWire Pulse'".to_string() - ]); - self.healthcheck.push(vec![ - "pactl".to_string(), - "info".to_string(), - "|".to_string(), - "grep".to_string(), - "--quiet".to_string(), - "\"$PULSE_RUNTIME_PATH/native\"".to_string() - ]) - } - - fn generate_configuration_file(&self) { - let mut configuration = self.configuration.iter() - .map(|e| e.join("\n")) - .collect::>(); - configuration.insert(0, "[supervisord]".to_string()); - configuration.insert(1, "nodaemon=true".to_string()); - configuration.insert(2, "logfile=/var/log/supervisor/supervisord.log".to_string()); - let configuration = configuration.join("\n"); - let mut file = fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(CONTAINER_TMP_PATH.join("supervisor.conf")) - .unwrap(); - file.write(configuration.as_bytes()).unwrap(); - file.flush().unwrap(); - } - - fn generate_entrypoint_script(&self) { - let mut script = self.entrypoint.iter() - .map(|command| command.join(" ")) - .collect::>(); - script.insert(0, "#!/bin/bash".to_string()); - let script = script.join("\n"); - let mut file = fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(CONTAINER_TMP_PATH.join("entrypoint.bash")) - .unwrap(); - file.write(script.as_bytes()).unwrap(); - file.flush().unwrap(); - } - - fn generate_healthcheck_script(&self) { - let mut script = self.healthcheck.iter() - .map(|command| { - format!("({}) || exit 1", command.join(" ")) - }) - .collect::>(); - script.insert(0, "#!/bin/bash".to_string()); - script.insert(1, "set -e".to_string()); - let mut post_start_script = self.post_start.iter() - .map(|command| { - format!("({}) || exit 1", command.join(" ")) - }) - .collect::>(); - script.append(&mut post_start_script); - let script = script.join("\n"); - let mut file = fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(CONTAINER_TMP_PATH.join("healthcheck.bash")) - .unwrap(); - file.write(script.as_bytes()).unwrap(); - file.flush().unwrap(); - } -} - -impl Drop for ServerApi { - fn drop(&mut self) { - if self.container.is_none() { - return; - } - let mut stop_options = StopContainerOptionsBuilder::default(); - stop_options.with_wait(Duration::from_millis(0)); - self.container_api.stop(self.container.as_ref().unwrap(), &mut stop_options); - self.container_api.remove(self.container.as_ref().unwrap()); - } -} - -impl Clone for ServerApi { - fn clone(&self) -> Self { - Self { - name: self.name.clone(), - tag: self.tag.clone(), - socket_id: self.socket_id.clone(), - container_file_path: self.container_file_path.clone(), - image_api: self.image_api.clone(), - container_api: self.container_api.clone(), - container: self.container.clone(), - configuration: self.configuration.clone(), - entrypoint: self.entrypoint.clone(), - healthcheck: self.healthcheck.clone(), - post_start: self.post_start.clone(), - } - } -} - -pub struct ContainerizedServer { - api: ServerApi, - services: Vec, - pre_entrypoint: Option, - post_start: Option, -} - -impl ContainerizedServer { - pub(self) fn new( - name: String, - container_file_path: PathBuf, - services: Vec, - pre_entrypoint: Option, - post_start: Option, - ) -> Self { - Self { - api: ServerApi::new(name, container_file_path), - services, - pre_entrypoint, - post_start, - } - } - - pub fn build(&mut self) { - self.api.create_socket_folder(); - match &self.pre_entrypoint { - Some(value) => value.call(&mut self.api), - None => {} - } - for service in &mut self.services { - service.entrypoint.call(&mut self.api); - service.healthcheck.call(&mut self.api); - } - match &self.post_start { - Some(value) => value.call(&mut self.api), - None => {} - } - self.api.spawn_wait_loop(); - self.api.remove_socket_folder(); - self.api.build(); - } - - pub fn create(&mut self) { - self.api.create(); - } - - pub fn start(&mut self) { - self.api.start() - } - - pub fn stop(&mut self) { - self.api.stop() - } - - pub fn restart(&mut self) { - self.api.restart(); - } - - pub fn set_socket_env_vars(&self) { - std::env::set_var(PIPEWIRE_RUNTIME_DIR_ENVIRONMENT_KEY, self.api.socket_location()); - std::env::set_var(PIPEWIRE_REMOTE_ENVIRONMENT_KEY, self.api.socket_name()); - } - - pub(self) fn cleanup(&self) { - self.api.cleanup(); - } -} - -impl Clone for ContainerizedServer { - fn clone(&self) -> Self { - Self { - api: self.api.clone(), - services: self.services.clone(), - pre_entrypoint: self.pre_entrypoint.clone(), - post_start: self.post_start.clone(), - } - } -} - -impl Drop for ContainerizedServer { - fn drop(&mut self) { - self.stop(); - } -} - -pub struct LocalServer {} - -pub enum Server { - Containerized(ContainerizedServer), - Local -} - -impl Server { - pub fn start(&mut self) { - match self { - Server::Containerized(value) => { - value.start(); - } - Server::Local => {} - } - } - - pub fn clone(&self) -> Self { - match self { - Server::Containerized(value) => Server::Containerized(value.clone()), - Server::Local => Server::Local - } - } - - pub fn cleanup(&self) { - match self { - Server::Containerized(value) => value.cleanup(), - Server::Local => {} - } - } -} - -#[fixture] -pub fn server_with_default_configuration() -> Arc { - let services = vec![ - PIPEWIRE_SERVICE.clone(), - WIREPLUMBER_SERVICE.clone(), - PULSE_SERVICE.clone(), - ]; - let mut server = ContainerizedServer::new( - "pipewire-default".to_string(), - CONTAINER_FILE_PATH.clone(), - services, - Some(LifeCycleCallback::from(|server: &mut ServerApi| { - server.set_virtual_nodes_configuration(); - })), - Some(LifeCycleCallback::from(|server: &mut ServerApi| { - server.set_default_nodes(); - })), - ); - let environment = TEST_ENVIRONMENT.lock().unwrap(); - let test_target = environment.test_target.clone(); - drop(environment); - match test_target { - TestTarget::Local => Arc::new(Server::Local), - TestTarget::Container => { - server.build(); - server.create(); - server.start(); - server.set_socket_env_vars(); - Arc::new(Server::Containerized(server)) - } - } -} - -#[fixture] -pub fn server_without_session_manager() -> Arc { - let services = vec![ - PIPEWIRE_SERVICE.clone(), - ]; - let mut server = ContainerizedServer::new( - "pipewire-without-session-manager".to_string(), - CONTAINER_FILE_PATH.clone(), - services, - None, - None, - ); - server.build(); - server.create(); - server.start(); - server.set_socket_env_vars(); - Arc::new(Server::Containerized(server)) -} - -#[fixture] -pub fn server_without_node() -> Arc { - let services = vec![ - PIPEWIRE_SERVICE.clone(), - WIREPLUMBER_SERVICE.clone(), - ]; - let mut server = ContainerizedServer::new( - "pipewire-without-node".to_string(), - CONTAINER_FILE_PATH.clone(), - services, - None, - None, - ); - server.build(); - server.create(); - server.start(); - server.set_socket_env_vars(); - Arc::new(Server::Containerized(server)) -} \ No newline at end of file diff --git a/src/host/pipewire/host.rs b/src/host/pipewire/host.rs index c2816b1c0..de984e1f9 100644 --- a/src/host/pipewire/host.rs +++ b/src/host/pipewire/host.rs @@ -1,7 +1,10 @@ use crate::traits::HostTrait; use crate::{BackendSpecificError, DevicesError, HostUnavailable, SupportedStreamConfigRange}; use std::rc::Rc; +use std::sync::Arc; +use std::time::Duration; use pipewire_client::{Direction, PipewireClient}; +use tokio::runtime::Runtime; use crate::host::pipewire::Device; pub type SupportedInputConfigs = std::vec::IntoIter; @@ -10,18 +13,24 @@ pub type Devices = std::vec::IntoIter; #[derive(Debug)] pub struct Host { + runtime: Arc, client: Rc, } impl Host { pub fn new() -> Result { - let client = PipewireClient::new() + let timeout = Duration::from_secs(30); + let runtime = Arc::new(Runtime::new().unwrap()); + let client = PipewireClient::new(runtime.clone(), timeout) .map_err(move |error| { eprintln!("{}", error.description); HostUnavailable })?; let client = Rc::new(client); - let host = Host { client }; + let host = Host { + runtime, + client + }; Ok(host) } From 135eb4c6c9f29582d0e9eaeeed5986fce34f59e0 Mon Sep 17 00:00:00 2001 From: Alexis Bekhdadi Date: Mon, 3 Mar 2025 18:38:29 +0000 Subject: [PATCH 17/17] Add PipeWire information in README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b5ac67097..da1f7aeac 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This library currently supports the following: Currently, supported hosts include: -- Linux (via ALSA or JACK) +- Linux (via ALSA, JACK or PipeWire) - Windows (via WASAPI by default, see ASIO instructions below) - macOS (via CoreAudio) - iOS (via CoreAudio) @@ -27,6 +27,10 @@ Note that on Linux, the ALSA development files are required. These are provided as part of the `libasound2-dev` package on Debian and Ubuntu distributions and `alsa-lib-devel` on Fedora. +When building with the `pipewire` feature flag, development files for PipeWire and Clang are required: +- On Debian and Ubuntu: install the `libpipewire-0.3-dev` and `libclang-19-dev` packages. +- On Fedora: install the `pipewire-devel` and `clang-devel` packages. + ## Compiling for Web Assembly If you are interested in using CPAL with WASM, please see [this guide](https://github.com/RustAudio/cpal/wiki/Setting-up-a-new-CPAL-WASM-project) in our Wiki which walks through setting up a new project from scratch. @@ -36,6 +40,7 @@ If you are interested in using CPAL with WASM, please see [this guide](https://g Some audio backends are optional and will only be compiled with a [feature flag](https://doc.rust-lang.org/cargo/reference/features.html). - JACK (on Linux): `jack` +- PipeWire (on Linux): `pipewire` (currently in testing, feel free to share your feedback!) - ASIO (on Windows): `asio` Oboe can either use a shared or static runtime. The static runtime is used by default, but activating the