Rust implementation of the Qobuz Connect protocol.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SessionManager β
β - Main entry point β
β - Owns DeviceRegistry and SessionHandles β
β - Routes events to user via mpsc channel β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β owns β owns
βΌ βΌ
βββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ
β DeviceRegistry β β SessionHandle (per session)β
β - 1 HTTP server (axum) β β - Lightweight handle β
β - N mDNS announcements β β - Sends commands via channelβ
β - Emits DeviceSelected β β - Spawns SessionRunner task β
βββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ
β Per-Device mDNS Service β β SessionRunner (spawned) β
β _qobuz-connect._tcp.local β β - Owns WebSocket connection β
β TXT: path, device_uuid β β - tokio::select! loop β
βββββββββββββββββββββββββββββββ β - Handles WS + commands β
ββββββββββββββββββββββββββββββββ
When you call manager.add_device(config):
ββββββββββββ ββββββββββββββββββ βββββββββββββββββββ
β User β β DeviceRegistry β β mDNS (Avahi) β
ββββββ¬ββββββ βββββββββ¬βββββββββ ββββββββββ¬βββββββββ
β β β
β add_device(config) β β
βββββββββββββββββββββββ>β β
β β β
β β Register service β
β β "_qobuz-connect._tcp" β
β βββββββββββββββββββββββββββ>β
β β β
β β TXT records: β
β β - path=/devices/{uuid} β
β β - device_uuid={uuid} β
β β - type=SPEAKER β
β βββββββββββββββββββββββββββ>β
β β β
β Ok(()) β β
β<βββββββββββββββββββββββ β
The device is now discoverable on the local network via mDNS/Zeroconf.
When a user selects your device in the Qobuz app:
ββββββββββββββ ββββββββββββββββββ ββββββββββββββββββ βββββββββββββββ
β Qobuz App β β HTTP Server β β DeviceRegistry β β Manager β
βββββββ¬βββββββ βββββββββ¬βββββββββ βββββββββ¬βββββββββ ββββββββ¬βββββββ
β β β β
β GET /devices/{uuid}/get-display-info β β
βββββββββββββββββββββ>β β β
β { name, type } β β β
β<βββββββββββββββββββββ β β
β β β β
β GET /devices/{uuid}/get-connect-info β β
βββββββββββββββββββββ>β β β
β { app_id } β β β
β<βββββββββββββββββββββ β β
β β β β
β POST /devices/{uuid}/connect-to-qconnect β β
β { session_id, jwt_qconnect, jwt_api } β β
βββββββββββββββββββββ>β β β
β β DeviceSelected β β
β βββββββββββββββββββββββ>β β
β β β DeviceSelected β
β β ββββββββββββββββββββββ>β
β { success } β β β
β<βββββββββββββββββββββ β β
When SessionManager receives DeviceSelected:
βββββββββββββββ βββββββββββββββββ ββββββββββββββββββ βββββββββββββββ
β Manager β β SessionHandle β β SessionRunner β β Qobuz WS β
ββββββββ¬βββββββ βββββββββ¬ββββββββ βββββββββ¬βββββββββ ββββββββ¬βββββββ
β β β β
β SessionHandle::connect(session_info, device_config) β
βββββββββββββββββββββ>β β β
β β β β
β β Connect WebSocket β β
β βββββββββββββββββββββββββββββββββββββββββββββ>β
β β β β
β β Subscribe + Join β β
β βββββββββββββββββββββββββββββββββββββββββββββ>β
β β β β
β β Spawn runner task β β
β ββββββββββββββββββββββ>β β
β β β β
β SessionHandle β β tokio::select! { β
β<βββββββββββββββββββββ β ws.recv() β
β β β cmd_rx.recv() β
β β β } β
β β β<ββββββββββββββββββββ>β
Events from Qobuz server flow to user code:
βββββββββββββββ βββββββββββββββββ βββββββββββββββ ββββββββββββ
β Qobuz WS β β SessionRunner β β Manager β β User β
ββββββββ¬βββββββ βββββββββ¬ββββββββ ββββββββ¬βββββββ ββββββ¬ββββββ
β β β β
β PlaybackCommand β β β
βββββββββββββββββββββ>β β β
β β β β
β β event_tx.send() β β
β βββββββββββββββββββββ>β β
β β β β
β β β events.recv() β
β β βββββββββββββββββββ>β
β β β β
β β β SessionEvent:: β
β β β PlaybackCommand β
β β βββββββββββββββββββ>β
use qonductor::{
SessionManager, DeviceConfig, SessionEvent, Command, Notification,
ActivationState, msg, PlayingState, BufferState,
msg::{PositionExt, QueueRendererStateExt, SetStateExt, report::VolumeChanged},
};
#[tokio::main]
async fn main() -> qonductor::Result<()> {
// Start the session manager (HTTP server + mDNS)
let mut manager = SessionManager::start(7864, "your_app_id").await?;
// Register device and get session handle for bidirectional communication
let mut session = manager.add_device(
DeviceConfig::new("Living Room Speaker")
).await?;
// Spawn manager to handle device selections
tokio::spawn(async move { manager.run().await });
// Handle events for this device
while let Some(event) = session.recv().await {
match event {
// Commands require a response via the Responder
SessionEvent::Command(cmd) => match cmd {
Command::SetState { cmd, respond } => {
println!("Play {:?} at {:?}ms", cmd.state(), cmd.current_position);
let mut response = msg::QueueRendererState {
current_position: Some(msg::Position::now(cmd.current_position.unwrap_or(0))),
..Default::default()
};
response
.set_state(cmd.state().unwrap_or(PlayingState::Stopped))
.set_buffer(BufferState::Ok);
respond.send(response);
}
Command::SetVolume { cmd, respond } => {
println!("Volume: {:?}", cmd.volume);
respond.send(VolumeChanged { volume: cmd.volume });
}
Command::SetActive { respond, .. } => {
println!("Device activated!");
respond.send(ActivationState {
muted: false,
volume: 100,
max_quality: 4,
playback: msg::QueueRendererState::default(),
});
}
Command::Heartbeat { respond } => {
respond.send(None); // or Some(state) if playing
}
},
// Notifications are informational (use _ => for forward compatibility)
SessionEvent::Notification(n) => match n {
Notification::Connected => println!("Connected!"),
Notification::DeviceRegistered { renderer_id, .. } => {
println!("Registered as renderer {}", renderer_id);
}
Notification::QueueState(queue) => {
println!("Queue has {} tracks", queue.tracks.len());
}
_ => {}
},
}
}
Ok(())
}| Type | Description |
|---|---|
SessionManager |
Main entry point. Manages devices and sessions. |
DeviceConfig |
Configuration for a discoverable device. |
DeviceSession |
Bidirectional session handle returned by add_device(). |
SessionEvent |
Wrapper: Command(Command) or Notification(Notification) |
Command |
Events requiring response: SetState, SetVolume, SetActive, Heartbeat |
Notification |
Informational events: Connected, QueueState, etc. |
Responder<T> |
Used to send required responses back to the server. |
PlayingState |
Playback state: Playing, Paused, Stopped |
-
mDNS Advertisement: Each device is advertised via
_qobuz-connect._tcpwith a unique path in the TXT record. -
HTTP Endpoints: A single HTTP server handles all devices via parameterized routes (
/devices/{uuid}/*). Qobuz apps hit these endpoints when the device is selected. -
1:1 Device-Session Mapping: When a device is selected in the Qobuz app, a dedicated session is created for that device between Qonductor and the Qobuz servers.
-
Actor Pattern: Each WebSocket session runs in its own spawned task, communicating with the manager via channels.
cargo build
cargo run --example discovery_serverMIT