#[cfg(feature = "lua")]
use wireplumber::lua::LuaVariant;
#[cfg(feature = "spa-json")]
use wireplumber::spa::json::SpaJson;
use {
anyhow::{format_err, Context, Result},
clap::{Parser, ValueEnum},
glib::Variant,
std::{cell::RefCell, env, fs, path::Path, rc::Rc},
wireplumber::{
log::{info, warning},
plugin::*,
prelude::*,
pw::{self, Properties},
},
};
const LOG_DOMAIN: &'static str = "wpexec.rs";
#[derive(ValueEnum, Copy, Clone, Debug)]
enum ModuleType {
Lua,
LuaConfig,
Wireplumber,
Pipewire,
}
#[cfg_attr(
doc,
doc = "Command-line arguments parsed via [clap](https://docs.rs/clap/latest/clap/)"
)]
#[derive(Parser, Debug)]
#[clap(version, disable_version_flag(true))]
struct Args {
#[clap(value_enum, short = 't', long = "type", default_value = "lua")]
module_type: ModuleType,
#[clap(short = 'J', long = "json")]
json_arg: Option<String>,
#[clap(short = 'V', long = "variant")]
variant_arg: Option<String>,
#[clap(short, long = "plugin")]
plugins: Vec<String>,
module: Option<String>,
}
async fn main_async(core: &Core, args: &Args) -> Result<()> {
let path = args.module().expect("already checked this in main()");
let path = {
let path = Path::new(path);
match args.module_type.module_dir() {
Some(module_dir) if path.is_absolute() => {
let path = fs::canonicalize(path)?;
let (dir, file) = (path.parent().unwrap(), path.file_name().unwrap());
env::set_var(module_dir, dir);
Some(file.to_string_lossy().into_owned())
},
_ => None,
}
}
.unwrap_or(path.into());
if let Some(module) = args.module_type.loader_module() {
core
.load_component(module, ComponentLoader::TYPE_WIREPLUMBER_MODULE, None)
.with_context(|| format!("failed to load the {:?} scripting module", args.module_type))?;
}
match args.module_type.is_lua() {
#[cfg(feature = "lua")]
true => {
let variant_args = args.lua_variant()?;
core
.load_lua_script(&path, variant_args)
.context("failed to load the lua script")?;
},
_ => {
let variant_args = args.args()?;
core
.load_component(&path, args.module_type.loader_type(), variant_args.as_ref())
.with_context(|| format!("failed to load {path} as a {}", args.module_type.loader_type()))?;
},
}
core.connect_future().await?;
let plugin_names = args.plugins();
for plugin_name in &plugin_names {
let p = Plugin::find(&core, plugin_name).ok_or_else(|| format_err!("plugin {plugin_name} not found"))?;
p.activate_future(PluginFeatures::ENABLED)
.await
.with_context(|| format!("failed to activate {plugin_name:?} plugin"))?;
}
if plugin_names.is_empty() {
info!(domain: LOG_DOMAIN, "skipped activation, no plugin specified");
}
if args.module_type.is_lua() {
if let Some(script) = Plugin::find(&core, &format!("script:{path}")) {
script
.activate_future(PluginFeatures::ENABLED)
.await
.with_context(|| format!("failed to activate script:{path}"))?;
}
}
Ok(())
}
fn main() -> Result<()> {
wireplumber::Log::set_default_level("3");
let args = Args::parse();
if args.module().is_none() {
return Err(format_err!("no default module available for {:?}", args.module_type))
}
let _ = match args.module_type.is_lua() {
#[cfg(feature = "lua")]
true => args.lua_variant().map(drop)?,
_ => args.args().map(drop)?,
};
Core::init();
let main_res = Rc::new(RefCell::new(None));
let props = Properties::new();
props.insert(pw::PW_KEY_APP_NAME, LOG_DOMAIN);
Core::run(Some(props), |context, mainloop, core| {
ctrlc::set_handler({
let mainloop = mainloop.clone();
move || mainloop.quit()
})
.unwrap();
let main_res = main_res.clone();
context.spawn_local(async move {
let res = main_async(&core, &args).await;
if res.is_err() {
mainloop.quit();
}
*main_res.borrow_mut() = Some(res)
});
});
let main_res = main_res.borrow_mut().take();
match main_res {
Some(res) => res,
None => Err(format_err!("could not connect to pipewire")),
}
}
impl ModuleType {
fn is_lua(&self) -> bool {
matches!(self, ModuleType::Lua)
}
fn module_dir(&self) -> Option<&'static str> {
match self {
ModuleType::Lua | ModuleType::LuaConfig => None,
ModuleType::Wireplumber => Some(ComponentLoader::DIR_WIREPLUMBER_MODULE),
ModuleType::Pipewire => Some(ComponentLoader::DIR_PIPEWIRE_MODULE),
}
}
fn loader_module(&self) -> Option<&'static str> {
match self {
ModuleType::Lua | ModuleType::LuaConfig => Some(ComponentLoader::MODULE_LOADER_LUA),
_ => None,
}
}
fn loader_type(&self) -> &'static str {
match self {
ModuleType::Lua => ComponentLoader::TYPE_LUA_SCRIPT,
ModuleType::LuaConfig => ComponentLoader::TYPE_LUA_CONFIG,
ModuleType::Wireplumber => ComponentLoader::TYPE_WIREPLUMBER_MODULE,
ModuleType::Pipewire => ComponentLoader::TYPE_PIPEWIRE_MODULE,
}
}
}
impl Args {
fn plugins(&self) -> Vec<&str> {
match self.module_type {
ModuleType::Lua => vec![ComponentLoader::PLUGIN_LOADER_LUA],
ModuleType::Wireplumber if self.plugins.is_empty() && self.module.is_none() => vec!["static-link"],
ModuleType::Wireplumber | ModuleType::LuaConfig => self.plugins.iter().map(|s| s.as_str()).collect(),
ModuleType::Pipewire => todo!(),
}
}
fn module(&self) -> Option<&str> {
match self.module {
Some(ref module) => Some(module),
None => match self.module_type {
ModuleType::Lua => Some(concat!(env!("CARGO_MANIFEST_DIR"), "/script.lua")),
ModuleType::Wireplumber => {
let module_path = concat!(env!("OUT_DIR"), "/../../../examples/libstatic_link_module.so"); if fs::metadata(module_path).is_err() {
warning!(
domain: LOG_DOMAIN,
"example module not found, try: cargo build -p wp-examples --example static-link-module"
);
}
Some(module_path)
},
_ => None,
},
}
}
#[cfg(feature = "lua")]
fn lua_variant(&self) -> Result<Option<LuaVariant>> {
#[allow(unreachable_patterns)]
match (&self.variant_arg, &self.json_arg) {
(None, None) => Ok(None),
(Some(v), _) => Variant::parse(None, v)
.map_err(Into::into)
.and_then(|v| LuaVariant::convert_from(&v).map_err(Into::into))
.map(Some),
#[cfg(feature = "spa-json")]
(None, Some(json)) => SpaJson::deserialize_from_string(json).map_err(Into::into).map(Some),
#[cfg(feature = "serde_json")]
(None, Some(json)) => serde_json::from_str(json).map_err(Into::into).map(Some),
(None, Some(..)) => panic!("spa-json or serde_json feature required to parse JSON arguments"),
}
}
fn args(&self) -> Result<Option<Variant>> {
#[allow(unreachable_patterns)]
match (&self.variant_arg, &self.json_arg) {
(None, None) => Ok(None),
(Some(v), _) => Variant::parse(None, v).map_err(Into::into).map(Some),
#[cfg(feature = "lua")]
(None, Some(..)) => self.lua_variant().map(|v| v.map(|v| v.into())),
#[cfg(feature = "spa-json")]
(None, Some(json)) => SpaJson::from_string(json).parse_variant().map_err(Into::into).map(Some),
(None, Some(..)) => panic!("spa-json or lua feature is required to convert JSON to Variant"),
}
}
}