#![deny(unsafe_code)]
#![warn(clippy::redundant_closure)]
#![warn(clippy::implicit_clone)]
#![warn(clippy::uninlined_format_args)]
#![warn(missing_docs)]
pub mod config;
pub mod connection;
pub(crate) mod controller;
#[cfg(feature = "encryption")]
pub(crate) mod crypto;
pub mod decoder;
pub(crate) mod double_buffer;
pub mod stream;
pub mod time_provider;
#[cfg(feature = "mdns")]
pub mod discovery;
#[cfg(feature = "resampler")]
pub mod resampler;
use tokio::sync::mpsc;
const EVENT_CHANNEL_SIZE: usize = 256;
const COMMAND_CHANNEL_SIZE: usize = 64;
const AUDIO_CHANNEL_SIZE: usize = 256;
#[cfg(feature = "custom-protocol")]
pub use snapcast_proto::CustomMessage;
pub use snapcast_proto::SampleFormat;
pub use snapcast_proto::{DEFAULT_STREAM_PORT, PROTOCOL_VERSION};
#[derive(Debug, Clone)]
pub struct AudioFrame {
pub samples: Vec<f32>,
pub sample_rate: u32,
pub channels: u16,
pub timestamp_usec: i64,
}
#[derive(Debug, Clone)]
pub enum ClientEvent {
Connected {
host: String,
port: u16,
},
Disconnected {
reason: String,
},
StreamStarted {
codec: String,
format: SampleFormat,
},
ServerSettings {
buffer_ms: i32,
latency: i32,
volume: u16,
muted: bool,
},
VolumeChanged {
volume: u16,
muted: bool,
},
TimeSyncComplete {
diff_ms: f64,
},
#[cfg(feature = "custom-protocol")]
CustomMessage(snapcast_proto::CustomMessage),
}
#[derive(Debug, Clone)]
pub enum ClientCommand {
SetVolume {
volume: u16,
muted: bool,
},
#[cfg(feature = "custom-protocol")]
SendCustom(snapcast_proto::CustomMessage),
Stop,
}
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub scheme: String,
pub host: String,
pub port: u16,
pub auth: Option<crate::config::Auth>,
#[cfg(feature = "tls")]
pub server_certificate: Option<std::path::PathBuf>,
#[cfg(feature = "tls")]
pub certificate: Option<std::path::PathBuf>,
#[cfg(feature = "tls")]
pub certificate_key: Option<std::path::PathBuf>,
#[cfg(feature = "tls")]
pub key_password: Option<String>,
pub instance: u32,
pub host_id: String,
pub latency: i32,
pub mdns_service_type: String,
pub client_name: String,
#[cfg(feature = "encryption")]
pub encryption_psk: Option<String>,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
scheme: "tcp".into(),
host: String::new(),
port: snapcast_proto::DEFAULT_STREAM_PORT,
auth: None,
#[cfg(feature = "tls")]
server_certificate: None,
#[cfg(feature = "tls")]
certificate: None,
#[cfg(feature = "tls")]
certificate_key: None,
#[cfg(feature = "tls")]
key_password: None,
instance: 1,
host_id: String::new(),
latency: 0,
mdns_service_type: "_snapcast._tcp.local.".into(),
client_name: "Snapclient".into(),
#[cfg(feature = "encryption")]
encryption_psk: None,
}
}
}
pub struct SnapClient {
config: ClientConfig,
event_tx: mpsc::Sender<ClientEvent>,
command_tx: mpsc::Sender<ClientCommand>,
command_rx: Option<mpsc::Receiver<ClientCommand>>,
pub time_provider: std::sync::Arc<std::sync::Mutex<time_provider::TimeProvider>>,
pub stream: std::sync::Arc<std::sync::Mutex<stream::Stream>>,
}
impl SnapClient {
pub fn new(
config: ClientConfig,
) -> (
Self,
mpsc::Receiver<ClientEvent>,
mpsc::Receiver<AudioFrame>,
) {
let (event_tx, event_rx) = mpsc::channel(EVENT_CHANNEL_SIZE);
let (command_tx, command_rx) = mpsc::channel(COMMAND_CHANNEL_SIZE);
let (_audio_tx, audio_rx) = mpsc::channel(AUDIO_CHANNEL_SIZE);
let time_provider =
std::sync::Arc::new(std::sync::Mutex::new(time_provider::TimeProvider::new()));
let stream = std::sync::Arc::new(std::sync::Mutex::new(stream::Stream::new(
SampleFormat::default(),
)));
let client = Self {
config,
event_tx,
command_tx,
command_rx: Some(command_rx),
time_provider,
stream,
};
(client, event_rx, audio_rx)
}
pub fn command_sender(&self) -> mpsc::Sender<ClientCommand> {
self.command_tx.clone()
}
pub async fn run(&mut self) -> anyhow::Result<()> {
let command_rx = self
.command_rx
.take()
.ok_or_else(|| anyhow::anyhow!("run() already called"))?;
let mut ctrl = controller::Controller::new(
self.config.clone(),
self.event_tx.clone(),
command_rx,
std::sync::Arc::clone(&self.time_provider),
std::sync::Arc::clone(&self.stream),
);
ctrl.run().await
}
}