1#![deny(unsafe_code)]
2#![warn(clippy::redundant_closure)]
3#![warn(clippy::implicit_clone)]
4#![warn(clippy::uninlined_format_args)]
5#![warn(missing_docs)]
6
7pub mod config;
45pub mod connection;
46pub(crate) mod controller;
47#[cfg(feature = "encryption")]
48pub(crate) mod crypto;
49pub mod decoder;
50pub(crate) mod double_buffer;
51pub mod stream;
52pub mod time_provider;
53
54#[cfg(feature = "mdns")]
55pub mod discovery;
56
57#[cfg(feature = "resampler")]
58pub mod resampler;
59
60use tokio::sync::mpsc;
61
62const EVENT_CHANNEL_SIZE: usize = 256;
63const COMMAND_CHANNEL_SIZE: usize = 64;
64const AUDIO_CHANNEL_SIZE: usize = 256;
65
66#[cfg(feature = "custom-protocol")]
68pub use snapcast_proto::CustomMessage;
69pub use snapcast_proto::SampleFormat;
70pub use snapcast_proto::{DEFAULT_STREAM_PORT, PROTOCOL_VERSION};
71
72#[derive(Debug, Clone)]
74pub struct AudioFrame {
75 pub samples: Vec<f32>,
77 pub sample_rate: u32,
79 pub channels: u16,
81 pub timestamp_usec: i64,
83}
84
85#[derive(Debug, Clone)]
87pub enum ClientEvent {
88 Connected {
90 host: String,
92 port: u16,
94 },
95 Disconnected {
97 reason: String,
99 },
100 StreamStarted {
102 codec: String,
104 format: SampleFormat,
106 },
107 ServerSettings {
109 buffer_ms: i32,
111 latency: i32,
113 volume: u16,
115 muted: bool,
117 },
118 VolumeChanged {
120 volume: u16,
122 muted: bool,
124 },
125 TimeSyncComplete {
127 diff_ms: f64,
129 },
130 #[cfg(feature = "custom-protocol")]
131 CustomMessage(snapcast_proto::CustomMessage),
133}
134
135#[derive(Debug, Clone)]
137pub enum ClientCommand {
138 SetVolume {
140 volume: u16,
142 muted: bool,
144 },
145 #[cfg(feature = "custom-protocol")]
147 SendCustom(snapcast_proto::CustomMessage),
148 Stop,
150}
151
152#[derive(Debug, Clone)]
154pub struct ClientConfig {
155 pub scheme: String,
157 pub host: String,
159 pub port: u16,
161 pub auth: Option<crate::config::Auth>,
163 #[cfg(feature = "tls")]
165 pub server_certificate: Option<std::path::PathBuf>,
166 #[cfg(feature = "tls")]
168 pub certificate: Option<std::path::PathBuf>,
169 #[cfg(feature = "tls")]
171 pub certificate_key: Option<std::path::PathBuf>,
172 #[cfg(feature = "tls")]
174 pub key_password: Option<String>,
175 pub instance: u32,
177 pub host_id: String,
179 pub latency: i32,
181 pub mdns_service_type: String,
183 pub client_name: String,
185 #[cfg(feature = "encryption")]
187 pub encryption_psk: Option<String>,
188}
189
190impl Default for ClientConfig {
191 fn default() -> Self {
192 Self {
193 scheme: snapcast_proto::SCHEME_TCP.into(),
194 host: String::new(),
195 port: snapcast_proto::DEFAULT_STREAM_PORT,
196 auth: None,
197 #[cfg(feature = "tls")]
198 server_certificate: None,
199 #[cfg(feature = "tls")]
200 certificate: None,
201 #[cfg(feature = "tls")]
202 certificate_key: None,
203 #[cfg(feature = "tls")]
204 key_password: None,
205 instance: 1,
206 host_id: String::new(),
207 latency: 0,
208 mdns_service_type: snapcast_proto::DEFAULT_MDNS_SERVICE_TYPE.into(),
209 client_name: snapcast_proto::DEFAULT_CLIENT_NAME.into(),
210 #[cfg(feature = "encryption")]
211 encryption_psk: None,
212 }
213 }
214}
215
216pub struct SnapClient {
218 config: ClientConfig,
219 event_tx: mpsc::Sender<ClientEvent>,
220 command_tx: mpsc::Sender<ClientCommand>,
221 command_rx: Option<mpsc::Receiver<ClientCommand>>,
222 audio_tx: mpsc::Sender<AudioFrame>,
223 pub time_provider: std::sync::Arc<std::sync::Mutex<time_provider::TimeProvider>>,
225 pub stream: std::sync::Arc<std::sync::Mutex<stream::Stream>>,
227}
228
229impl SnapClient {
230 pub fn new(
232 config: ClientConfig,
233 ) -> (
234 Self,
235 mpsc::Receiver<ClientEvent>,
236 mpsc::Receiver<AudioFrame>,
237 ) {
238 let (event_tx, event_rx) = mpsc::channel(EVENT_CHANNEL_SIZE);
239 let (command_tx, command_rx) = mpsc::channel(COMMAND_CHANNEL_SIZE);
240 let (audio_tx, audio_rx) = mpsc::channel(AUDIO_CHANNEL_SIZE);
241 let time_provider =
242 std::sync::Arc::new(std::sync::Mutex::new(time_provider::TimeProvider::new()));
243 let stream = std::sync::Arc::new(std::sync::Mutex::new(stream::Stream::new(
244 SampleFormat::default(),
245 )));
246 let client = Self {
247 config,
248 event_tx,
249 command_tx,
250 command_rx: Some(command_rx),
251 audio_tx,
252 time_provider,
253 stream,
254 };
255 (client, event_rx, audio_rx)
256 }
257
258 pub fn command_sender(&self) -> mpsc::Sender<ClientCommand> {
260 self.command_tx.clone()
261 }
262
263 pub async fn run(&mut self) -> anyhow::Result<()> {
265 let command_rx = self
266 .command_rx
267 .take()
268 .ok_or_else(|| anyhow::anyhow!("run() already called"))?;
269
270 let mut ctrl = controller::Controller::new(
271 self.config.clone(),
272 self.event_tx.clone(),
273 command_rx,
274 self.audio_tx.clone(),
275 std::sync::Arc::clone(&self.time_provider),
276 std::sync::Arc::clone(&self.stream),
277 )?;
278 ctrl.run().await
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[tokio::test]
287 async fn run_rejects_websocket_audio_scheme() {
288 let config = ClientConfig {
289 scheme: snapcast_proto::SCHEME_WS.into(),
290 host: "localhost".into(),
291 port: snapcast_proto::DEFAULT_HTTP_PORT,
292 ..ClientConfig::default()
293 };
294 let (mut client, _events, _audio_rx) = SnapClient::new(config);
295
296 let err = client.run().await.unwrap_err();
297 assert!(err.to_string().contains("websocket audio transport"));
298 }
299}