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 = "resampler")]
55pub mod resampler;
56
57use tokio::sync::mpsc;
58
59const EVENT_CHANNEL_SIZE: usize = 256;
60const COMMAND_CHANNEL_SIZE: usize = 64;
61const AUDIO_CHANNEL_SIZE: usize = 256;
62
63#[cfg(feature = "custom-protocol")]
65pub use snapcast_proto::CustomMessage;
66pub use snapcast_proto::SampleFormat;
67pub use snapcast_proto::{DEFAULT_STREAM_PORT, PROTOCOL_VERSION};
68
69#[derive(Debug, Clone)]
71pub struct AudioFrame {
72 pub samples: Vec<f32>,
74 pub sample_rate: u32,
76 pub channels: u16,
78 pub timestamp_usec: i64,
80}
81
82#[derive(Debug, Clone)]
84pub enum ClientEvent {
85 Connected {
87 host: String,
89 port: u16,
91 },
92 Disconnected {
94 reason: String,
96 },
97 StreamStarted {
99 codec: String,
101 format: SampleFormat,
103 },
104 ServerSettings {
106 buffer_ms: i32,
108 latency: i32,
110 volume: u16,
112 muted: bool,
114 },
115 VolumeChanged {
117 volume: u16,
119 muted: bool,
121 },
122 TimeSyncComplete {
124 diff_ms: f64,
126 },
127 #[cfg(feature = "custom-protocol")]
128 CustomMessage(snapcast_proto::CustomMessage),
130}
131
132#[derive(Debug, Clone)]
134pub enum ClientCommand {
135 SetVolume {
137 volume: u16,
139 muted: bool,
141 },
142 #[cfg(feature = "custom-protocol")]
144 SendCustom(snapcast_proto::CustomMessage),
145 Stop,
147}
148
149#[derive(Debug, Clone)]
151pub struct ClientConfig {
152 pub scheme: String,
154 pub host: String,
156 pub port: u16,
158 pub auth: Option<crate::config::Auth>,
160 #[cfg(feature = "tls")]
162 pub server_certificate: Option<std::path::PathBuf>,
163 #[cfg(feature = "tls")]
165 pub certificate: Option<std::path::PathBuf>,
166 #[cfg(feature = "tls")]
168 pub certificate_key: Option<std::path::PathBuf>,
169 #[cfg(feature = "tls")]
171 pub key_password: Option<String>,
172 pub instance: u32,
174 pub host_id: String,
176 pub latency: i32,
178 pub client_name: String,
180 #[cfg(feature = "encryption")]
182 pub encryption_psk: Option<String>,
183}
184
185impl Default for ClientConfig {
186 fn default() -> Self {
187 Self {
188 scheme: snapcast_proto::SCHEME_TCP.into(),
189 host: String::new(),
190 port: snapcast_proto::DEFAULT_STREAM_PORT,
191 auth: None,
192 #[cfg(feature = "tls")]
193 server_certificate: None,
194 #[cfg(feature = "tls")]
195 certificate: None,
196 #[cfg(feature = "tls")]
197 certificate_key: None,
198 #[cfg(feature = "tls")]
199 key_password: None,
200 instance: 1,
201 host_id: String::new(),
202 latency: 0,
203 client_name: snapcast_proto::DEFAULT_CLIENT_NAME.into(),
204 #[cfg(feature = "encryption")]
205 encryption_psk: None,
206 }
207 }
208}
209
210pub struct SnapClient {
212 config: ClientConfig,
213 event_tx: mpsc::Sender<ClientEvent>,
214 command_tx: mpsc::Sender<ClientCommand>,
215 command_rx: Option<mpsc::Receiver<ClientCommand>>,
216 audio_tx: mpsc::Sender<AudioFrame>,
217 pub time_provider: std::sync::Arc<std::sync::Mutex<time_provider::TimeProvider>>,
219 pub stream: std::sync::Arc<std::sync::Mutex<stream::Stream>>,
221}
222
223impl SnapClient {
224 pub fn new(
226 config: ClientConfig,
227 ) -> (
228 Self,
229 mpsc::Receiver<ClientEvent>,
230 mpsc::Receiver<AudioFrame>,
231 ) {
232 let (event_tx, event_rx) = mpsc::channel(EVENT_CHANNEL_SIZE);
233 let (command_tx, command_rx) = mpsc::channel(COMMAND_CHANNEL_SIZE);
234 let (audio_tx, audio_rx) = mpsc::channel(AUDIO_CHANNEL_SIZE);
235 let time_provider =
236 std::sync::Arc::new(std::sync::Mutex::new(time_provider::TimeProvider::new()));
237 let stream = std::sync::Arc::new(std::sync::Mutex::new(stream::Stream::new(
238 SampleFormat::default(),
239 )));
240 let client = Self {
241 config,
242 event_tx,
243 command_tx,
244 command_rx: Some(command_rx),
245 audio_tx,
246 time_provider,
247 stream,
248 };
249 (client, event_rx, audio_rx)
250 }
251
252 pub fn command_sender(&self) -> mpsc::Sender<ClientCommand> {
254 self.command_tx.clone()
255 }
256
257 pub async fn run(&mut self) -> anyhow::Result<()> {
259 let command_rx = self
260 .command_rx
261 .take()
262 .ok_or_else(|| anyhow::anyhow!("run() already called"))?;
263
264 let mut ctrl = controller::Controller::new(
265 self.config.clone(),
266 self.event_tx.clone(),
267 command_rx,
268 self.audio_tx.clone(),
269 std::sync::Arc::clone(&self.time_provider),
270 std::sync::Arc::clone(&self.stream),
271 )?;
272 ctrl.run().await
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[tokio::test]
281 async fn run_rejects_websocket_audio_scheme() {
282 let config = ClientConfig {
283 scheme: snapcast_proto::SCHEME_WS.into(),
284 host: "localhost".into(),
285 port: snapcast_proto::DEFAULT_HTTP_PORT,
286 ..ClientConfig::default()
287 };
288 let (mut client, _events, _audio_rx) = SnapClient::new(config);
289
290 let err = client.run().await.unwrap_err();
291 assert!(err.to_string().contains("websocket audio transport"));
292 }
293}