# How scrcpy Implements the Device Message Protocol: Binary Deserialization and Control Socket Architecture

> Discover how scrcpy implements its device message protocol using a custom binary format and control socket architecture. Learn about deserialization and asynchronous handling.

- Repository: [Genymobile/scrcpy](https://github.com/Genymobile/scrcpy)
- Tags: internals
- Published: 2026-02-25

---

**scrcpy implements its device message protocol using a custom binary format over the control socket, with message definitions in [`device_msg.h`](https://github.com/Genymobile/scrcpy/blob/main/device_msg.h), deserialization logic in [`device_msg.c`](https://github.com/Genymobile/scrcpy/blob/main/device_msg.c), and asynchronous handling via a dedicated receiver thread in [`receiver.c`](https://github.com/Genymobile/scrcpy/blob/main/receiver.c).**

The scrcpy device message protocol enables the Android server to push events—such as clipboard updates and UHID output—to the desktop client in real time. This lightweight, custom binary protocol operates over a dedicated control socket separate from the video stream, using a concise framing format implemented in the Genymobile/scrcpy repository.

## Protocol Architecture and Binary Layout

All device messages share a uniform binary header followed by type-specific payload data. The protocol uses big-endian byte order for multi-byte fields to ensure cross-platform consistency between the Android server and desktop clients.

The frame structure is:

```

+--------+-------------------+
| Type   | Payload (variable)|
| 1 byte |                  |
+--------+-------------------+

```

The `type` field contains one of the values defined in `enum sc_device_msg_type` (located in [`app/src/device_msg.h`](https://github.com/Genymobile/scrcpy/blob/main/app/src/device_msg.h)). The payload structure varies by message type:

| Message Type | Payload Structure |
|--------------|-------------------|
| `DEVICE_MSG_TYPE_CLIPBOARD` | 4-byte big-endian length **N**, followed by **N** bytes of UTF-8 text. |
| `DEVICE_MSG_TYPE_ACK_CLIPBOARD` | 8-byte big-endian sequence number for synchronization acknowledgment. |
| `DEVICE_MSG_TYPE_UHID_OUTPUT` | 2-byte big-endian **id**, 2-byte big-endian **size S**, followed by **S** bytes of raw HID data. |

The protocol enforces a maximum message size of `DEVICE_MSG_MAX_SIZE` (256 KB) defined in [`device_msg.h`](https://github.com/Genymobile/scrcpy/blob/main/device_msg.h), preventing memory exhaustion from malformed packets.

## Message Definitions and Data Structures

The canonical message definitions reside in [`app/src/device_msg.h`](https://github.com/Genymobile/scrcpy/blob/main/app/src/device_msg.h). This header declares the message type enumeration and the union structure used to hold deserialized data.

Key definitions include:

- `enum sc_device_msg_type` – Identifies the three supported message categories.
- `struct sc_device_msg` – A tagged union containing the type discriminator and payload-specific data structures.
- `DEVICE_MSG_MAX_SIZE` – Compile-time constant limiting buffer allocations.

The header also provides function prototypes for `sc_device_msg_deserialize()` and `sc_device_msg_destroy()`, establishing the contract for memory management: deserialization allocates buffers for variable-length fields, while destruction releases them.

## Deserialization Implementation

The core parsing logic lives in [`app/src/device_msg.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/device_msg.c), specifically within the `sc_device_msg_deserialize()` function. This implementation follows a streaming parser pattern suitable for network data that may arrive in arbitrary chunks.

```c
ssize_t
sc_device_msg_deserialize(const uint8_t *buf, size_t len,
                          struct sc_device_msg *msg);

```

The function returns:
- **Positive value**: Number of bytes consumed from the buffer.
- **Zero**: Insufficient data (incomplete message); caller should buffer more bytes.
- **Negative value**: Fatal error (malformed data or allocation failure).

The deserialization process branches on the message type byte:

1. **Clipboard Messages**: Verifies at least 5 bytes present (1 type + 4 length), reads the 32-bit big-endian length using `sc_read32be()`, allocates `msg->clipboard.text`, copies the payload, and NUL-terminates the string.

2. **ACK Clipboard**: Verifies 9 bytes available, reads the 64-bit sequence number via `sc_read64be()`.

3. **UHID Output**: Verifies minimum 5 bytes, extracts 16-bit `id` and `size` using `sc_read16be()`, allocates `msg->uhid_output.data`, and copies the raw HID payload.

Memory ownership transfers to the caller upon successful return. The caller must eventually invoke `sc_device_msg_destroy()` to free allocated text or data buffers, preventing leaks in long-running sessions.

## Receiver Thread and Message Dispatch

The [`app/src/receiver.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/receiver.c) module implements the asynchronous consumption layer. A dedicated background thread manages the control socket, buffering incoming bytes and dispatching complete messages to the main application thread.

The receiver operates through three coordinated stages:

**1. Network Buffering**

The thread maintains a static buffer `buf[DEVICE_MSG_MAX_SIZE]` and repeatedly calls `net_recv()` on the control socket. Incoming data appends to the buffer, with the receiver tracking the fill level.

**2. Message Extraction**

After each successful read, the receiver invokes `process_msgs()`, which loops calling `sc_device_msg_deserialize()` until the function returns 0 (incomplete message) or -1 (error). Each successful deserialization yields the number of bytes consumed, allowing the buffer to shift remaining data to the front.

**3. Thread-Safe Dispatch**

Complete messages pass to `process_msg()`, which posts tasks to the main thread using `sc_post_to_main_thread()`:

- **Clipboard updates** trigger `task_set_clipboard()`, updating the desktop clipboard with the Android device content.
- **ACK messages** invoke `sc_acksync_ack()` to unblock pending clipboard synchronization requests.
- **UHID output** schedules `task_uhid_output()`, forwarding raw HID reports to the host input subsystem.

This architecture ensures that UI-related operations execute on the main thread while network I/O remains non-blocking on the background thread.

## Practical Code Examples

### Parsing Device Messages Manually

For applications integrating scrcpy's protocol directly, the deserialization API provides standalone message parsing:

```c
#include "device_msg.h"
#include <stdio.h>

void handle_msg(const uint8_t *data, size_t size)
{
    struct sc_device_msg msg;
    ssize_t n = sc_device_msg_deserialize(data, size, &msg);
    if (n <= 0) {
        fprintf(stderr, "Incomplete or error\n");
        return;
    }

    switch (msg.type) {
    case DEVICE_MSG_TYPE_CLIPBOARD:
        printf("Clipboard: %s\n", msg.clipboard.text);
        break;
    case DEVICE_MSG_TYPE_ACK_CLIPBOARD:
        printf("Ack seq: %lu\n", (unsigned long)msg.ack_clipboard.sequence);
        break;
    case DEVICE_MSG_TYPE_UHID_OUTPUT:
        printf("UHID id=%u size=%u\n",
               msg.uhid_output.id, msg.uhid_output.size);
        break;
    }

    sc_device_msg_destroy(&msg);   /* free allocated buffers */
}

```

### Integrating the Receiver Component

Standard scrcpy usage involves initializing the receiver thread to handle background message processing:

```c
struct sc_receiver receiver;
sc_socket control_sock = /* already connected control socket */;
struct sc_receiver_callbacks cbs = {
    .on_ended = on_receiver_end,   // user-defined callback
};
sc_receiver_init(&receiver, control_sock, &cbs, NULL);
sc_receiver_start(&receiver);

/* … later … */
sc_receiver_join(&receiver);
sc_receiver_destroy(&receiver);

```

The `on_receiver_end` callback receives a boolean parameter indicating whether the receiver stopped due to an error or normal termination.

## Summary

- **scrcpy device message protocol** uses a compact binary format with a 1-byte type header followed by type-specific payloads, supporting clipboard updates, synchronization ACKs, and UHID output events.
- **Message definitions** reside in [`app/src/device_msg.h`](https://github.com/Genymobile/scrcpy/blob/main/app/src/device_msg.h), establishing the `sc_device_msg` union and the `sc_device_msg_type` enumeration for the three supported message categories.
- **Deserialization logic** in [`app/src/device_msg.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/device_msg.c) implements streaming parsing via `sc_device_msg_deserialize()`, handling big-endian byte order and returning bytes consumed, zero for incomplete data, or negative for errors.
- **Asynchronous handling** occurs in [`app/src/receiver.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/receiver.c), where a background thread buffers socket data, extracts complete messages, and dispatches them to the main thread using `sc_post_to_main_thread()` for UI-safe processing.
- **Memory management** requires callers to invoke `sc_device_msg_destroy()` after processing to free dynamically allocated text or data buffers within clipboard and UHID messages.

## Frequently Asked Questions

### What is the maximum size of a device message in scrcpy?

The protocol enforces a hard limit of **256 KB** defined by the `DEVICE_MSG_MAX_SIZE` constant in [`app/src/device_msg.h`](https://github.com/Genymobile/scrcpy/blob/main/app/src/device_msg.h). This restriction prevents memory exhaustion attacks and ensures that the static receive buffer in [`receiver.c`](https://github.com/Genymobile/scrcpy/blob/main/receiver.c) remains bounded during socket operations.

### How does scrcpy handle incomplete messages during network reads?

The `sc_device_msg_deserialize()` function in [`app/src/device_msg.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/device_msg.c) returns **0** when the buffer contains insufficient bytes to complete a message. The receiver thread in [`app/src/receiver.c`](https://github.com/Genymobile/scrcpy/blob/main/app/src/receiver.c) accumulates additional data from the socket and retries deserialization, ensuring that fragmented TCP packets do not corrupt the protocol state.

### What types of messages can the Android server send to the desktop client?

According to [`app/src/device_msg.h`](https://github.com/Genymobile/scrcpy/blob/main/app/src/device_msg.h), the server can transmit three message types: **DEVICE_MSG_TYPE_CLIPBOARD** (text content updates), **DEVICE_MSG_TYPE_ACK_CLIPBOARD** (sequence acknowledgments for synchronization), and **DEVICE_MSG_TYPE_UHID_OUTPUT** (raw HID report data for virtual input devices). Each type uses a distinct binary payload structure parsed by the deserialization layer.

### Is the scrcpy device message protocol publicly documented?

The protocol is **deliberately undocumented** in the public API; the only authoritative specifications are the source code implementation and the unit tests in [`app/tests/test_device_msg_deserialize.c`](https://github.com/Genymobile/scrcpy/blob/main/app/tests/test_device_msg_deserialize.c). Developers integrating with scrcpy should reference the C structures in [`device_msg.h`](https://github.com/Genymobile/scrcpy/blob/main/device_msg.h) and the parsing logic in [`device_msg.c`](https://github.com/Genymobile/scrcpy/blob/main/device_msg.c) rather than external documentation that may become outdated.