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

scrcpy implements its device message protocol using a custom binary format over the control socket, with message definitions in device_msg.h, deserialization logic in device_msg.c, and asynchronous handling via a dedicated receiver thread in 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). 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, preventing memory exhaustion from malformed packets.

Message Definitions and Data Structures

The canonical message definitions reside in 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, 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.

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 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:

#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:

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, 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 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, 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. This restriction prevents memory exhaustion attacks and ensures that the static receive buffer in 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 returns 0 when the buffer contains insufficient bytes to complete a message. The receiver thread in 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, 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. Developers integrating with scrcpy should reference the C structures in device_msg.h and the parsing logic in device_msg.c rather than external documentation that may become outdated.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →