# 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 ```protobuf 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) ```protobuf 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) ```protobuf message SetVolumeMessage { int32 volume = 1; // absolute volume (0-100) int32 volume_delta = 2; // relative change } ``` ### Renderer.SetActiveMessage (43 - SRVR_RNDR_SET_ACTIVE) ```protobuf message SetActiveMessage { bool active = 1; } ``` ### Common.QueueTrackWithContext ```protobuf message QueueTrackWithContext { int32 queue_item_id = 1; int32 track_id = 2; bytes context_uuid = 3; // optional } ``` ### Common.PlaybackPosition ```protobuf message PlaybackPosition { fixed64 timestamp = 1; // epoch millis int32 value = 2; // position in seconds } ``` ### RendererState (sent by renderer to report its state) ```protobuf 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) ```protobuf 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) ```protobuf message StateUpdatedMessage { RendererState state = 1; } ``` ### Renderer.VolumeChangedMessage (25 - RNDR_SRVR_VOLUME_CHANGED) ```protobuf message VolumeChangedMessage { int32 volume = 1; } ``` ### Controller.JoinSessionMessage (61 - CTRL_SRVR_JOIN_SESSION) ```protobuf message JoinSessionMessage { DeviceInfo device_info = 2; } ``` ### Common.DeviceInfo ```protobuf 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 ```protobuf 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