1 unstable release

Uses new Rust 2024

new 0.1.0-alpha.1 Feb 27, 2026

#179 in WebSocket

Download history 83/week @ 2026-02-25

83 downloads per month

MIT license

215KB
4K SLoC

Qonductor

UNDER ACTIVE DEVELOPMENT, EVERYTHING WILL BREAK

Rust implementation of the Qobuz Connect protocol.

Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                            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     │
                                                └──────────────────────────────┘

Device Discovery Flow

1. Device Registration

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.

2. Device Selection (Qobuz App → Your Device)

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 }      │                       │                      │
      │<────────────────────│                       │                      │

3. WebSocket Session Creation

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()    │
       │                     │                      │   }                  │
       │                     │                      │<────────────────────>

4. Event Flow

Events from Qobuz server flow to user code:

┌─────────────┐      ┌───────────────┐      ┌─────────────┐       ┌──────────┐
│  Qobuz WS   │      │ SessionRunner │      │   Manager   │       │   User   │
└──────┬──────┘      └───────┬───────┘      └──────┬──────┘       └────┬─────┘
       │                     │                     │                   │
       │  PlaybackCommand    │                     │                   │
       │────────────────────>│                     │                   │
       │                     │                     │                   │
       │                     │  event_tx.send()    │                   │
       │                     │────────────────────>│                   │
       │                     │                     │                   │
       │                     │                     │  events.recv()    │
       │                     │                     │──────────────────>│
       │                     │                     │                   │
       │                     │                     │  SessionEvent::   │
       │                     │                     │  PlaybackCommand  │
       │                     │                     │──────────────────>

Usage

use qonductor::{
    SessionManager, DeviceConfig, SessionEvent, Command, Notification,
    ActivationState, msg, PlayingState, BufferState,
    msg::{PositionExt, QueueRendererStateExt},
};

#[tokio::main]
async fn main() -> qonductor::Result<()> {
    // Start the session manager (HTTP server + mDNS)
    let mut manager = SessionManager::start(7864).await?;

    // Register device and get session handle for bidirectional communication
    let mut session = manager.add_device(
        DeviceConfig::new("Living Room Speaker", "your_app_id")
    ).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.playing_state, cmd.current_position);
                    let mut response = msg::QueueRendererState::default();
                    response.set_state(PlayingState::Playing).set_buffer(BufferState::Ok);
                    respond.send(response);
                }
                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(())
}

Key Types

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, 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

How It Works

  1. mDNS Advertisement: Each device is advertised via _qobuz-connect._tcp with a unique path in the TXT record.

  2. HTTP Endpoints: A single HTTP server handles all devices via parameterized routes (/devices/{uuid}/*). Qobuz apps hit these endpoints when the device is selected.

  3. 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.

  4. Actor Pattern: Each WebSocket session runs in its own spawned task, communicating with the manager via channels.

Building

cargo build
cargo run --example discovery_server

License

MIT

Dependencies

~15–35MB
~449K SLoC