How scrcpy's Client-Server Architecture Works Internally: A Deep Dive into the Source Code

scrcpy operates through a lightweight client-server architecture where the desktop client pushes a Java server to the Android device, establishes three ADB tunnel sockets for video, audio, and control, and streams encoded media while relaying input events back to the device.

The Genymobile/scrcpy project implements this architecture by splitting functionality across two distinct binaries: a native desktop client and a Java-based Android server. This design requires no root privileges or device-side installation, leveraging only standard ADB and Android APIs to achieve low-latency screen mirroring.

The Two-Sided Architecture

scrcpy's implementation cleanly separates concerns between the controlling desktop machine and the controlled Android device.

Desktop Client (app/src/)

The desktop component resides in the app/src/ directory, with entry points in app/src/main.c and server management logic in app/src/server.c. This binary handles:

  • Parsing command-line arguments and initializing SDL/FFmpeg
  • Pushing the server JAR to the device via ADB
  • Establishing forward tunnels for the three communication channels
  • Decoding video (FFmpeg) and audio streams for display/playback
  • Capturing user input and serializing it to the control channel

Android Server (server/src/)

The device-side component is a Java archive located at scrcpy-server.jar, with sources in server/src/main/java/com/genymobile/scrcpy/. Key classes include:

Phase 1: Server Installation and Launch

The handshake begins with the desktop client preparing the environment on the Android device.

Pushing the Server JAR

The function push_server() in app/src/server.c copies the server binary to /data/local/tmp/:

bool ok = sc_adb_push(intr, serial, server_path,
                      SC_DEVICE_SERVER_PATH, 0);   // src/server.c#L67-L68

Building the app_process Command

The execute_server() function constructs the command line to launch the Java server via Android's app_process wrapper:

cmd[count++] = "app_process";
cmd[count++] = "/";                     // unused directory argument
cmd[count++] = "com.genymobile.scrcpy.Server";
cmd[count++] = SCRCPY_VERSION;          // src/server.c#L47-L50

This command includes the socket identifier (scid) parameter, which both sides use to agree on socket names.

Creating ADB Forward Tunnels

For each channel (video, audio, control), the client issues adb forward commands to map local TCP ports to abstract Unix sockets on the device. The socket name derives from the scid:

private static String getSocketName(int scid) {
    return scid == -1 ? "scrcpy"
                      : "scrcpy_" + String.format("_%08x", scid);
}   // server/src/main/java/com/genymobile/scrcpy/server/Server.java#L47-L55

Phase 2: Server Bootstrap on Device

Once launched, the Java server initializes its communication endpoints.

Opening the Desktop Connection

The Server class calls DesktopConnection.open(), passing the scid and feature flags:

DesktopConnection connection = DesktopConnection.open(
        scid, tunnelForward, video, audio, control, sendDummyByte);

Establishing Socket Connections

DesktopConnection.java creates three LocalSocket instances connected to the abstract socket name:

LocalSocket videoSocket = connect(socketName);
LocalSocket audioSocket = connect(socketName);
LocalSocket controlSocket = connect(socketName);
// server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java#L56-L99

These sockets expose raw file descriptors that the server uses for streaming.

Phase 3: Data Flow and Streaming

With connections established, the system enters its operational state, handling three distinct data streams.

Video Stream

The video channel carries H.264 encoded frames from the device to the desktop:

  • Server side: SurfaceEncoder captures the display surface, encodes via MediaCodec, and writes packets to a Streamer object
  • Streamer: Writes to the video file descriptor using Streamer.writePacket
  • Client side: VideoDecoder (FFmpeg) reads from the TCP socket, decodes frames, and renders via SDL

Audio Stream

The audio channel transmits Opus-encoded audio (or raw PCM):

  • Server side: AudioCapture records system audio, AudioEncoder encodes it, and a Streamer forwards packets to the audio fd
  • Client side: AudioPlayer reads the socket, decodes, and plays through the OS audio API

Control Stream

The control channel handles bidirectional command and input:

  • Desktop to Device: ControlSender serializes user actions (touch, key presses, clipboard) into DeviceMessage objects and writes them through the control fd
  • Device to Desktop: Controller reads ControlMessage objects via ControlChannel and injects events using Android's InputManager

The protocol uses length-prefixed binary messages defined in ControlMessage.java, DeviceMessage.java, and their respective Reader/Writer classes.

Phase 4: Shutdown Sequence

When the desktop client terminates, it triggers a clean shutdown:

  1. The client closes its TCP sockets

  2. DesktopConnection.shutdown() on the server closes the three LocalSocket instances:

    public void shutdown() throws IOException {
        if (videoSocket != null) { videoSocket.shutdownInput(); videoSocket.shutdownOutput(); }
        if (audioSocket != null) { audioSocket.shutdownInput(); audioSocket.shutdownOutput(); }
        if (controlSocket != null) { controlSocket.shutdownInput(); controlSocket.shutdownOutput(); }
    }   // DesktopConnection.java#L28-L41
  3. Socket closure causes the Looper to quit (Looper.getMainLooper().quitSafely()), stopping encoders and the controller

  4. The Java process terminates, and the client removes the temporary JAR from the device

Summary

  • scrcpy's client-server architecture splits functionality between a native desktop client and a Java server pushed temporarily to the Android device.
  • Three ADB tunnels (video, audio, control) connect the sides using abstract Unix sockets identified by a unique scid.
  • Media streaming uses SurfaceEncoder and AudioEncoder on the device, with FFmpeg/SDL decoding on the desktop.
  • Input injection travels from desktop to device via a binary protocol implemented in ControlChannel and Controller.
  • No installation required: The server runs via app_process and cleans up on exit, requiring only standard ADB access.

Frequently Asked Questions

What is the role of the scid parameter in scrcpy?

The scid (socket connection identifier) is a hexadecimal value generated by the desktop client that ensures unique socket names when multiple scrcpy instances run simultaneously. According to the source code in Server.java, the socket name becomes scrcpy_<scid> (or just scrcpy for legacy compatibility), preventing collisions between separate sessions on the same device.

How does scrcpy establish communication without root access?

scrcpy leverages the ADB daemon running on the device, which operates with sufficient privileges to open abstract Unix sockets and access the InputManager service. The desktop client uses standard adb forward commands to tunnel TCP ports to these sockets, while the server runs inside a sandboxed app_process shell with the CLASSPATH set to the temporary JAR. This design requires only standard developer permissions, not root.

What protocols does scrcpy use for video and audio streaming?

scrcpy uses H.264 for video encoding via Android's MediaCodec API, with the desktop client decoding via FFmpeg. For audio, it supports Opus encoding (when available) or raw PCM capture, depending on the Android version and options. Both streams travel over raw TCP sockets tunneled through ADB, using a simple packet-based protocol where Streamer.java writes length-prefixed frames to the file descriptors.

How does the desktop client handle user input events?

User input is captured by SDL on the desktop side and serialized into binary DeviceMessage objects by ControlSender. These messages travel through the control socket (the third ADB tunnel) to the device's ControlChannel, where ControlMessageReader deserializes them. The Controller class then injects the events using Android's InputManager system service, supporting touch, keyboard, clipboard, and hardware button events.

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 →