Files
qobuzd/PROTOCOL.md
joren 3a0d6e0240 Initial commit: QobuzD - Qobuz Connect renderer for Linux
Rust-based QConnect renderer with:
- QConnect WebSocket protocol (hand-rolled protobuf)
- Audio playback via Symphonia + cpal
- Play, pause, resume, volume, skip support
- Correct BufferState/PlayingState enum values per proto spec
- Server-driven queue management (no local queue)
- Periodic position reporting for track-end detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 20:38:54 +02:00

5.2 KiB

QConnect Protocol Reference

Reverse-engineered from Qobuz Android app (JADX decompilation) and Burp traffic analysis.

Transport Layer

WebSocket binary frames with custom framing:

  • Frame = type_byte + varint(body_length) + body
  • Frame type 1 = Auth (field 1=msg_id, field 3=jwt)
  • Frame type 2 = Subscribe (field 1=msg_id, field 3=1)
  • Frame type 6 = Data payload (field 1=msg_id, field 2=timestamp, field 3=1, field 5=\x02, field 7=qconnect_container)

QConnect Container (inside frame body field 7)

  • Field 3 = serialized QConnect Message

QConnect Message

message Message {
    MessageType message_type = 1;  // field number of payload == message_type value
    oneof payload {
        // Renderer -> Server
        Renderer.JoinSessionMessage     rndr_srvr_join_session     = 21;
        Renderer.StateUpdatedMessage    rndr_srvr_state_updated    = 23;
        Renderer.VolumeChangedMessage   rndr_srvr_volume_changed   = 25;

        // Server -> Renderer
        Renderer.SetStateMessage        srvr_rndr_set_state        = 41;
        Renderer.SetVolumeMessage       srvr_rndr_set_volume       = 42;
        Renderer.SetActiveMessage       srvr_rndr_set_active       = 43;
        Renderer.SetMaxAudioQualityMsg  srvr_rndr_set_max_quality  = 44;
        Renderer.SetLoopModeMessage     srvr_rndr_set_loop_mode    = 45;
        Renderer.SetShuffleModeMessage  srvr_rndr_set_shuffle_mode = 46;
        Renderer.MuteVolumeMessage      srvr_rndr_mute_volume      = 47;

        // Controller -> Server
        Controller.JoinSessionMessage   ctrl_srvr_join_session     = 61;
        Controller.SetPlayerStateMsg    ctrl_srvr_set_player_state = 62;
        Controller.QueueLoadTracksMsg   ctrl_srvr_queue_load_tracks= 66;

        // Server -> Controller
        // SessionStateMessage                                     = 81;
    }
}

Message Definitions

Renderer.SetStateMessage (41 - SRVR_RNDR_SET_STATE)

message SetStateMessage {
    PlayingState playing_state = 1;     // enum: 0=UNKNOWN, 1=STOPPED, 2=PLAYING, 3=PAUSED
    int32 current_position = 2;         // playback position (seconds)
    QueueVersion queue_version = 3;     // sub-message (field 1 = major version)
    QueueTrackWithContext current_track = 4;
    QueueTrackWithContext next_track = 5;
}

Renderer.SetVolumeMessage (42 - SRVR_RNDR_SET_VOLUME)

message SetVolumeMessage {
    int32 volume = 1;        // absolute volume (0-100)
    int32 volume_delta = 2;  // relative change
}

Renderer.SetActiveMessage (43 - SRVR_RNDR_SET_ACTIVE)

message SetActiveMessage {
    bool active = 1;
}

Common.QueueTrackWithContext

message QueueTrackWithContext {
    int32 queue_item_id = 1;
    int32 track_id = 2;
    bytes context_uuid = 3;  // optional
}

Common.PlaybackPosition

message PlaybackPosition {
    fixed64 timestamp = 1;  // epoch millis
    int32 value = 2;        // position in seconds
}

RendererState (sent by renderer to report its state)

message RendererState {
    PlayingState playing_state = 1;
    BufferState buffer_state = 2;          // enum: 0=UNKNOWN, 1=BUFFERING, 2=OK, 3=ERROR, 4=UNDERRUN
    PlaybackPosition current_position = 3;
    int32 duration = 4;                    // seconds
    QueueVersion queue_version = 5;        // sub-message (field 1 = major)
    int32 current_queue_item_id = 6;
    int32 next_queue_item_id = 7;
}

Renderer.JoinSessionMessage (21 - RNDR_SRVR_JOIN_SESSION)

message JoinSessionMessage {
    bytes session_uuid = 1;
    DeviceInfo device_info = 2;
    RendererState initial_state = 4;
    bool is_active = 5;
}

Renderer.StateUpdatedMessage (23 - RNDR_SRVR_STATE_UPDATED)

message StateUpdatedMessage {
    RendererState state = 1;
}

Renderer.VolumeChangedMessage (25 - RNDR_SRVR_VOLUME_CHANGED)

message VolumeChangedMessage {
    int32 volume = 1;
}

Controller.JoinSessionMessage (61 - CTRL_SRVR_JOIN_SESSION)

message JoinSessionMessage {
    DeviceInfo device_info = 2;
}

Common.DeviceInfo

message DeviceInfo {
    bytes device_uuid = 1;
    string friendly_name = 2;
    string brand = 3;
    string model = 4;
    string serial_number = 5;
    DeviceType type = 6;          // enum: 5=COMPUTER
    Capabilities capabilities = 7;
    string software_version = 8;
}

Common.Capabilities

message Capabilities {
    int32 audio_quality = 1;
    int32 field_2 = 2;  // observed value: 4
    int32 field_3 = 3;  // observed value: 2
}

PlayingState Enum

  • 0 = UNKNOWN (idle sync, ignore)
  • 1 = STOPPED
  • 2 = PLAYING
  • 3 = PAUSED

Connection Flow

  1. Get JWT + WS endpoint via GET /api.json/0.2/qws/getToken
  2. Open ctrl WebSocket, send Auth (frame type 1) + Subscribe (frame type 2) + CTRL_SRVR_JOIN_SESSION (61)
  3. Receive SRVR_CTRL_SESSION_STATE (81) containing session_uuid
  4. Open renderer WebSocket, send Auth + Subscribe + RNDR_SRVR_JOIN_SESSION (21) with session_uuid
  5. Receive SET_ACTIVE (43) confirming join
  6. Enter main loop: handle SET_STATE (41), SET_VOLUME (42), etc.
  7. Report state changes via STATE_UPDATED (23) and VOLUME_CHANGED (25)
  8. Send heartbeat state updates every 5 seconds