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