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:
Server.java: Entry point that parses parameters and initializes the systemDesktopConnection.java: Manages the three abstract Unix socketsStreamer.java: Writes encoded packets to file descriptorsSurfaceEncoder.javaandAudioEncoder.java: Capture and encode mediaController.java: Listens for input events and injects them viaInputManager
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:
SurfaceEncodercaptures the display surface, encodes viaMediaCodec, and writes packets to aStreamerobject - 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:
AudioCapturerecords system audio,AudioEncoderencodes it, and aStreamerforwards packets to the audio fd - Client side:
AudioPlayerreads the socket, decodes, and plays through the OS audio API
Control Stream
The control channel handles bidirectional command and input:
- Desktop to Device:
ControlSenderserializes user actions (touch, key presses, clipboard) intoDeviceMessageobjects and writes them through the control fd - Device to Desktop:
ControllerreadsControlMessageobjects viaControlChanneland injects events using Android'sInputManager
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:
-
The client closes its TCP sockets
-
DesktopConnection.shutdown()on the server closes the threeLocalSocketinstances: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 -
Socket closure causes the
Looperto quit (Looper.getMainLooper().quitSafely()), stopping encoders and the controller -
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
SurfaceEncoderandAudioEncoderon the device, with FFmpeg/SDL decoding on the desktop. - Input injection travels from desktop to device via a binary protocol implemented in
ControlChannelandController. - No installation required: The server runs via
app_processand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →