wayle-audio 0.1.4

PulseAudio service with reactive state
Documentation
use std::{borrow::Cow, collections::HashMap};

use libpulse_binding::{def::PortAvailable, format::Info as PulseFormatInfo, proplist::Proplist};

use crate::types::{device::DevicePort, format::AudioFormat, stream::MediaInfo};

pub(super) fn cow_to_string(cow: Option<&Cow<'_, str>>) -> String {
    cow.map(|s| s.to_string()).unwrap_or_default()
}

pub(super) fn collect_proplist(proplist: &Proplist) -> HashMap<String, String> {
    proplist
        .iter()
        .filter_map(|key| proplist.get_str(&key).map(|value| (key.to_string(), value)))
        .collect()
}

pub(super) fn convert_formats(formats: &[PulseFormatInfo]) -> Vec<AudioFormat> {
    formats
        .iter()
        .map(|format_info| AudioFormat {
            encoding: format!("{:?}", format_info.get_encoding()),
            properties: collect_proplist(format_info.get_properties()),
        })
        .collect()
}

pub(super) fn convert_ports(ports: &[impl PulsePort]) -> Vec<DevicePort> {
    ports
        .iter()
        .map(|port| DevicePort {
            name: cow_to_string(port.port_name()),
            description: cow_to_string(port.port_description()),
            priority: port.port_priority(),
            available: port.port_available() != PortAvailable::No,
        })
        .collect()
}

pub(super) fn active_port_name(active_port: &Option<Box<impl PulsePort>>) -> Option<String> {
    active_port
        .as_ref()
        .and_then(|port| port.port_name().map(|name| name.to_string()))
}

pub(super) fn extract_media_info(proplist: &Proplist) -> MediaInfo {
    MediaInfo {
        title: proplist.get_str("media.title"),
        artist: proplist.get_str("media.artist"),
        album: proplist.get_str("media.album"),
        icon_name: proplist.get_str("application.icon_name"),
    }
}

pub(super) trait PulsePort {
    fn port_name(&self) -> Option<&Cow<'_, str>>;
    fn port_description(&self) -> Option<&Cow<'_, str>>;
    fn port_priority(&self) -> u32;
    fn port_available(&self) -> PortAvailable;
}

impl PulsePort for libpulse_binding::context::introspect::SinkPortInfo<'_> {
    fn port_name(&self) -> Option<&Cow<'_, str>> {
        self.name.as_ref()
    }

    fn port_description(&self) -> Option<&Cow<'_, str>> {
        self.description.as_ref()
    }

    fn port_priority(&self) -> u32 {
        self.priority
    }

    fn port_available(&self) -> PortAvailable {
        self.available
    }
}

impl PulsePort for libpulse_binding::context::introspect::SourcePortInfo<'_> {
    fn port_name(&self) -> Option<&Cow<'_, str>> {
        self.name.as_ref()
    }

    fn port_description(&self) -> Option<&Cow<'_, str>> {
        self.description.as_ref()
    }

    fn port_priority(&self) -> u32 {
        self.priority
    }

    fn port_available(&self) -> PortAvailable {
        self.available
    }
}

#[cfg(test)]
mod tests {
    use libpulse_binding::format::{Encoding, Info as PulseFormatInfo};

    use super::*;

    fn format_with_encoding(encoding: Encoding) -> PulseFormatInfo {
        let mut format_info = PulseFormatInfo::new().expect("format info should allocate");
        format_info.set_encoding(encoding);
        format_info
    }

    #[test]
    fn convert_formats_supports_hdmi_encoded_formats() {
        let truehd = format_with_encoding(Encoding::TRUEHD_IEC61937);
        let dtshd = format_with_encoding(Encoding::DTSHD_IEC61937);

        let formats = convert_formats(&[truehd, dtshd]);

        assert_eq!(formats[0].encoding, "TRUEHD_IEC61937");
        assert_eq!(formats[1].encoding, "DTSHD_IEC61937");
    }
}