use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::{OnceLock, mpsc};
use anyhow::{Context, anyhow};
use log::Level::{Debug, Trace, Warn};
use log::{debug, error, info, log};
use pw::keys;
use pw::registry::{GlobalObject, Listener, RegistryBox};
use pw::spa::utils::dict::DictRef;
use pw::types::ObjectType;
use uuid::Uuid;
use crate::common::audiofox::AUDIOFOX;
use crate::common::result::{Level, LeveledResult, Result};
use crate::common::source::AudioSource;
use crate::platform::linux::command::{CmdListener, Response, StreamStates};
use crate::platform::linux::wrapper::{Channel, Direction};
pub static LINK_FACTORY: OnceLock<Box<str>> = OnceLock::new();
pub type RegId = u32;
pub type PortName = Rc<str>;
pub type RegName = Rc<str>;
#[derive(Default)]
pub struct RegNode {
pub name: RegName,
pub out_left: PortName,
pub out_right: PortName,
}
#[derive(Default)]
pub struct RegState {
pub nodes: RefCell<HashMap<RegId, RegNode>>,
init: Cell<bool>,
recon: Cell<bool>,
}
struct RegPort {
node_id: u32,
name: PortName,
direction: Direction,
channel: Channel,
}
impl RegPort {
fn get_info(id: u32, props: &DictRef) -> Result<Self> {
let node_id = props.get(*keys::NODE_ID).with_context(|| {
format!("port id={id}: missing required property node.id")
})?.parse::<u32>().context("failed to parse node.id as u32")?;
let name = props.get(*keys::PORT_NAME).with_context(|| {
format!("port node.id={node_id}: missing required property node.name")
})?.into();
let direction = props.get(*keys::PORT_DIRECTION).with_context(|| {
format!("port node.id={node_id}, name={name}: missing required property port.direction")
})?.parse::<Direction>()?;
let channel = props.get(*keys::AUDIO_CHANNEL).with_context(|| {
format!("port node.id={node_id}, name={name}, direction={direction}: missing required property audio.channel")
})?.parse::<Channel>()?;
Ok(Self { node_id, name, direction, channel })
}
}
impl RegState {
pub fn monitor(
self: Rc<Self>, registry: &RegistryBox, streams: &Rc<StreamStates>,
sender: &mpsc::Sender<Response>, listener: &Rc<CmdListener>,
) -> Listener {
registry
.add_listener_local()
.global(self.clone().start(streams.clone(), sender.clone(), listener.clone()))
.global_remove(self.on_remove())
.register()
}
fn start(
self: Rc<Self>, streams: Rc<StreamStates>, sender: mpsc::Sender<Response>,
listener: Rc<CmdListener>,
) -> impl Fn(&GlobalObject<&DictRef>) {
move |global| {
if let Err(err) = self.on_add(global, &streams, &sender, &listener) {
log!(err.level, "{}", err.error);
}
}
}
fn on_add(
&self, global: &GlobalObject<&DictRef>, streams: &StreamStates,
sender: &mpsc::Sender<Response>, listener: &CmdListener,
) -> LeveledResult<()> {
let Some(props) = global.props else {
send_logged!(sender, Response::Error(anyhow!("global object has no properties")));
return Ok(());
};
if global.type_ == ObjectType::Node {
let id = global.id;
let name = props.get(*keys::NODE_NAME).with_context(|| {
format!("skipping node id={id}: missing required property node.name")
}).level(Trace)?;
let class = props.get(*keys::MEDIA_CLASS).with_context(|| {
format!("node id={id}, name={name}: missing required property media.class")
}).level(Trace)?;
if name == AUDIOFOX && let Some(session) = props.get(*keys::NODE_SESSION) {
self.init_stream_node(id, session, streams)?;
}
else if class.contains("Stream/Output/Audio") {
info!("adding node id={id}, name={name}");
self.nodes.borrow_mut().insert(id, RegNode { name: name.into(), ..RegNode::default() });
if streams.borrow().values().flat_map(|s| s.names.iter())
.any(|n| n.as_ref() == name) {
self.recon.set(true);
}
}
else {
debug!("node id={id}, name={name}, class={class}: unsupported media.class");
}
}
else if global.type_ == ObjectType::Port {
let port = RegPort::get_info(global.id, props).level(Trace)?;
if self.init.get() {
self.init_stream_port(port, streams, sender);
}
else {
let RegPort { node_id, name, channel, direction } = port;
let mut nodes = self.nodes.borrow_mut();
let reg_node = nodes.get_mut(&node_id).with_context(|| {
format!("port id={node_id}, name={name}, channel={channel}: not associated with capturable node")
}).level(Trace)?;
if direction == Direction::Out {
match channel {
Channel::FL => reg_node.out_left = name,
Channel::FR => reg_node.out_right = name,
}
}
if self.recon.get() && !(reg_node.out_left.is_empty() || reg_node.out_right.is_empty()) {
let name = reg_node.name.clone();
drop(nodes);
self.reconnect(node_id, &name, streams, listener)?;
}
}
}
else if LINK_FACTORY.get().is_none() && global.type_ == ObjectType::Factory {
if props.get(*keys::FACTORY_TYPE_NAME) == Some(ObjectType::Link.to_str()) {
if let Some(name) = props.get(*keys::FACTORY_NAME) {
LINK_FACTORY.get_or_init(|| name.into());
}
}
}
Ok(())
}
fn on_remove(self: Rc<Self>) -> impl Fn(u32) {
move |id| {
if let Some(node) = self.nodes.borrow_mut().remove(&id) {
info!("removing node id={id}, name={}", node.name);
}
}
}
fn init_stream_node(&self, id: u32, session: &str, streams: &StreamStates) -> LeveledResult<()> {
let uuid = Uuid::from_str(session).with_context(|| {
format!("node id={id}, name={AUDIOFOX}: NODE_SESSION is not a valid UUID")
}).level(Debug)?;
let mut streams = streams.borrow_mut();
let (uuid, stream) = streams.last_mut().with_context(|| {
format!("candidate id={id}, uuid={uuid}: no links active or initializing")
}).level(Debug)?;
if uuid.eq(uuid) {
if stream.id == RegId::default() {
self.init.set(true);
stream.id = id;
info!("audiofox created successfully with id={id}");
} else {
error!("candidate id={id}, uuid={uuid}: uuid matches existing audiofox id={}", stream.id);
}
} else {
debug!("candidate id={id}: unrecognized uuid");
}
Ok(())
}
fn init_stream_port(&self, port: RegPort, streams: &StreamStates, sender: &mpsc::Sender<Response>) {
if port.direction == Direction::In {
let mut streams = streams.borrow_mut();
if let Some((_uuid, stream)) = streams.last_mut() {
if stream.id == port.node_id {
match port.channel {
Channel::FL => stream.in_left = port.name,
Channel::FR => stream.in_right = port.name,
}
}
if !(stream.in_left.is_empty() || stream.in_right.is_empty()) {
info!("audiofox node ready with 2 input ports");
send_logged!(sender, Response::Init);
self.init.set(false);
}
} else {
send_logged!(sender, Response::Error(anyhow!("no links active or initializing")));
self.init.set(false);
}
}
}
fn reconnect(&self, id: RegId, name: &RegName, streams: &StreamStates, listener: &CmdListener) -> LeveledResult<()> {
self.recon.set(false);
let uuids: Vec<_> = streams.borrow()
.iter()
.filter_map(|(uuid, state)| {
state.names.contains(name).then_some(*uuid)
}).collect();
for uuid in uuids {
let node = &[AudioSource { id: id.into(), name: name.as_ref().into() }];
listener.on_link(uuid, node).with_context(|| {
format!("node id={id}, name={name}: failed to reconnect to stream uuid={uuid}")
}).level(Warn)?;
}
Ok(())
}
}