Bluetooth Media Control Service (MCS) – Part 2
State machine, control opcodes, search, BlueZ code, and SDP interoperability — all explained step by step.
The Media State Machine
A media player is always in exactly one of four states. Here is how transitions happen between those states. Read each arrow label to understand what triggers the move.
|
Track becomes invalid
|
|||
|
▶️
PLAYING
0x01
|
💤
INACTIVE
0x00
|
||
|
FF or Rew ↓
|
← Play
Pause →
|
Select Valid Track
|
|
|
⏩
SEEKING
0x03
|
← FF or Rew
Pause / Stop →
|
⏸️
PAUSED
0x02
|
← Next Track / Prev Track / Goto Track commands also land in Paused (Track Position = 0) |
|
Play ↑
|
Stop opcode: Playing/Seeking/Paused → Paused (Track Position resets to 0) | ||
| From State | Trigger | To State |
|---|---|---|
| Any (except Inactive) | Current track becomes invalid | Inactive |
| Inactive | Valid track selected | Paused |
| Paused / Seeking | Play opcode | Playing |
| Playing | Pause opcode | Paused |
| Seeking | Pause opcode | Paused (position = where seeking stopped) |
| Playing / Paused | Fast Forward or Fast Rewind opcode | Seeking |
| Seeking | Play opcode | Playing |
| Playing / Paused / Seeking | Stop opcode | Paused (position reset to 0) |
Media Control Point — All 21 Opcodes
The Media Control Point is the “remote control” characteristic. You write an opcode byte (plus optional parameters) to it, and the server performs the action then sends a notification back with the result.
uint8_t opcode; /* 1 byte: the command */
int32_t parameter; /* 0 or 4 bytes: for Move Relative, Goto Segment, Goto Track, Goto Group */
/* Response notification format: */
uint8_t requested_opcode; /* echo of what was sent */
uint8_t result_code; /* 0x01=SUCCESS 0x02=NOT_SUPPORTED 0x03=INACTIVE 0x04=CANNOT_COMPLETE */
| Opcode | Name | Parameters | What It Does |
|---|---|---|---|
| 0x01 | Play | None | Start playing. Paused/Seeking → Playing. Already Playing → no change. |
| 0x02 | Pause | None | Pause. Playing → Paused. Seeking → Paused at current seek position. Already Paused → no change. |
| 0x03 | Fast Rewind | None | Begin seeking backwards. Each additional call may increase rewind speed. |
| 0x04 | Fast Forward | None | Begin seeking forward. Each additional call may increase FF speed. |
| 0x05 | Stop | None | Stop all activity → Paused state. Track position resets to 0. |
| Opcode | Name | Parameters | What It Does |
|---|---|---|---|
| 0x10 | Move Relative | offset (sint32) | Jump forward/back by offset (in 0.01s). Clamped to 0 or track end — no wrapping. |
| Opcode | Name | Params | What It Does |
|---|---|---|---|
| 0x20 | Previous Segment | None | Go to start of previous chapter (like double-tap back on earbuds) |
| 0x21 | Next Segment | None | Go to start of next chapter |
| 0x22 | First Segment | None | Jump to very first chapter of the track |
| 0x23 | Last Segment | None | Jump to last chapter of the track |
| 0x24 | Goto Segment | n (sint32) | Jump to nth chapter. Positive n = from start; negative n = from end; n=0 = no change. |
| Opcode | Name | Params | What It Does |
|---|---|---|---|
| 0x30 | Previous Track | None | Previous track per playing order. Position → 0. |
| 0x31 | Next Track | None | Next track per playing order. Position → 0. |
| 0x32 | First Track | None | Jump to first track. Position → 0. |
| 0x33 | Last Track | None | Jump to last track. Position → 0. |
| 0x34 | Goto Track | n (sint32) | Jump to nth track. Positive = from first; negative = from last; n=0 = no change. Position → 0. |
| Opcode | Name | Params | What It Does |
|---|---|---|---|
| 0x40 | Previous Group | None | Previous group within parent group |
| 0x41 | Next Group | None | Next group within parent group |
| 0x42 | First Group | None | Jump to first group in parent |
| 0x43 | Last Group | None | Jump to last group in parent |
| 0x44 | Goto Group | n (sint32) | Jump to nth group. Positive = from first; negative = from last; n=0 = no change. |
Media Control Point Opcodes Supported — Bitmask
Before sending any opcode, a smart client reads the Media Control Point Opcodes Supported characteristic — a 32-bit bitmask that tells you which opcodes this server actually implements. If bit N is set, opcode N is supported.
uint32_t supported = read_gatt_characteristic(MCS_OPCODES_SUPPORTED_UUID);
if (supported & 0x00000001) printf(“Play supported\n”);
if (supported & 0x00000002) printf(“Pause supported\n”);
if (supported & 0x00001000) printf(“Next Track supported\n”);
if (supported & 0x00100000) printf(“Goto Group supported\n”);
| Bit Value | Opcode Name | Bit Value | Opcode Name |
|---|---|---|---|
| 0x00000001 | Play | 0x00000800 | Previous Track |
| 0x00000002 | Pause | 0x00001000 | Next Track |
| 0x00000004 | Fast Rewind | 0x00002000 | First Track |
| 0x00000008 | Fast Forward | 0x00004000 | Last Track |
| 0x00000010 | Stop | 0x00008000 | Goto Track |
| 0x00000020 | Move Relative | 0x00010000 | Previous Group |
| 0x00000040 | Previous Segment | 0x00020000 | Next Group |
| 0x00000080 | Next Segment | 0x00040000 | First Group |
| 0x00000100 | First Segment | 0x00080000 | Last Group |
| 0x00000200 | Last Segment | 0x00100000 | Goto Group |
| 0x00000400 | Goto Segment | 0x00200000+ | RFU |
Search Control Point — Searching Your Media Library
The Search Control Point lets a client trigger a search over the media library. Search criteria are encoded as TLV (Type-Length-Value) items chained together into a single write (max 64 bytes).
|
1 octet
Length
(= size of Type + Parameter)
|
1 octet
Type
(search category)
|
Length – 1 octets
Parameter
(search string or empty)
|
| Type Value | Search Category | Parameter |
|---|---|---|
| 0x01 | Track Name | UTF-8 string (e.g. “Bohemian”) |
| 0x02 | Artist Name | UTF-8 string (e.g. “Queen”) |
| 0x03 | Album Name | UTF-8 string |
| 0x04 | Group Name | UTF-8 string |
| 0x05 | Earliest Year | UTF-8 string (e.g. “1990”) |
| 0x06 | Latest Year | UTF-8 string (e.g. “1999”) |
| 0x07 | Genre | UTF-8 string (e.g. “Rock”) |
| 0x08 | Only Tracks | None (filter to tracks only) |
| 0x09 | Only Groups | None (filter to groups/albums only) |
uint8_t search[] = {
5, 0x05, ‘1’,’9′,’9′,’9′, /* Earliest Year = 1999 */
5, 0x06, ‘1’,’9′,’9′,’9′, /* Latest Year = 1999 */
4, 0x07, ‘P’,’o’,’p’, /* Genre = “Pop” */
1, 0x08 /* Only Tracks (no parameter) */
};
write_gatt_characteristic(SEARCH_CONTROL_POINT_UUID, search, sizeof(search));
BlueZ — Interacting with MCS from Linux
BlueZ (the Linux Bluetooth stack) supports MCS as part of its LE Audio implementation. Here’s how you can interact with MCS characteristics using BlueZ’s D-Bus API and the bluetoothctl tool.
$ bluetoothctl
[bluetooth]# scan on
[bluetooth]# connect AA:BB:CC:DD:EE:FF
# List GATT services — look for MCS UUID
[bluetooth]# menu gatt
[bluetooth]# list-attributes AA:BB:CC:DD:EE:FF
# MCS Primary Service UUID: 1848 (Media Control Service)
# GMCS Primary Service UUID: 1849 (Generic Media Control Service)
# Media Control Point UUID: 2BC1
# Media State UUID: 2BA3
# Track Title UUID: 2B97
/org/bluez/hci0/dev_.../service.../char...) with the actual paths from your BlueZ instance. Use bluetoothctl → menu gatt → list-attributes to find them after connecting.[bluetooth]# select-attribute /org/bluez/hci0/dev_…/service001a/char0020
[bluetooth]# notify on
[CHG] Attribute … Value: 0x01 ← Playing
[CHG] Attribute … Value: 0x02 ← Paused
# Read Track Title
[bluetooth]# select-attribute /org/bluez/hci0/dev_…/service001a/char001b
[bluetooth]# read
Attribute Value: 53 68 61 70 65 20 6f 66 20 59 6f 75 (= “Shape of You”)
SDP Interoperability (BR/EDR Devices)
MCS is a GATT service, but it can also be discoverable via SDP on Bluetooth Classic (BR/EDR) devices. This is useful for devices that support both BR/EDR and BLE (dual-mode). The SDP record points to the same GATT/ATT endpoint.
| SDP Record Field | Value | Status |
|---|---|---|
| Service Class #0 | «Generic Media Control» | M |
| Service Class #1 | «Media Control» | C.1 |
| Protocol #0 | «L2CAP» (PSM = ATT) | M |
| Protocol #1 | «ATT» | M |
| Additional Protocol (EATT) | «L2CAP» (PSM = EATT) | C.2 |
| BrowseGroupList | PublicBrowseRoot | M |
Putting It All Together — A Complete Playback Flow
| CLIENT (Earbuds) | SERVER (Phone) | |
|
1
|
Connect & Bond — Earbuds initiate BLE connection + pairing (encryption enabled) | |
|
2
|
GATT Discovery — find MCS service and all characteristics | ← ATT_READ_BY_GROUP_TYPE_REQ |
|
3
|
Subscribe to notifications — write CCCD on: Track Changed, Media State, Track Title | ← ATT_WRITE_REQ (CCCD = 0x0001) |
|
4
|
Read initial state: Track Title, Duration, Media State | ← ATT_READ_REQ → Response with values |
|
5
|
User presses Play on earbuds → earbuds write opcode 0x01 to Media Control Point | |
|
6
|
Receives notification: Media Control Point result = 0x01 01 (opcode Play, SUCCESS) | Phone plays audio. Sends notification → Media State = 0x01 (Playing) |
|
7
|
Song changes automatically → Server sends Track Changed notification (zero-length), then updates Track Title | |
|
8
|
Receives Track Changed notification → reads new Track Title → updates display | Sends Track Title notification with new song name |
