scrcpy for developers
This application is composed of two parts:
- the server (
scrcpy-server.jar), to be executed on the device,
- the client (the
scrcpybinary), executed on the host computer.
The client is responsible to push the server to the device and start its execution.
Once the client and the server are connected to each other, the server initially sends device information (name and initial screen dimensions), then starts to send a raw H.264 video stream of the device screen. The client decodes the video frames, and display them as soon as possible, without buffering, to minimize latency. The client is not aware of the device rotation (which is handled by the server), it just knows the dimensions of the video frames.
The client captures relevant keyboard and mouse events, that it transmits to the server, which injects them to the device.
Capturing the screen requires some privileges, which are granted to
The server is a Java application (with a
public static void main(String... args) method), compiled against the Android framework, and executed as
shell on the Android device.
To run such a Java application, the classes must be dexed (typically,
my.package.MainClass is the main class, compiled to
classes.dex, pushed to the device in
/data/local/tmp, then it can be run
adb shell CLASSPATH=/data/local/tmp/classes.dex \ app_process / my.package.MainClass
/data/local/tmp is a good candidate to push the server, since it's
readable and writable by
shell, but not world-writable, so a malicious
application may not replace the server just before the client executes it.
Instead of a raw dex file,
app_process accepts a jar containing
classes.dex (e.g. an APK). For simplicity, and to benefit from the gradle
build system, the server is built to an (unsigned) APK (renamed to
Although compiled against the Android framework, hidden methods and classes are not directly accessible (and they may differ from one Android version to another).
The server uses 2 threads:
- the main thread, encoding and streaming the video to the client;
- the controller thread, listening for control events (typically, keyboard and mouse events) from the client.
Since the video encoding is typically hardware, there would be no benefit in encoding and streaming in two different threads.
Screen video encoding
The encoding is managed by
The video is encoded using the
MediaCodec API. The codec takes its input
from a surface associated to the display, and writes the resulting H.264
stream to the provided output stream (the socket connected to the client).
On device rotation, the codec, surface and display are reinitialized, and a new video stream is produced.
New frames are produced only when changes occur on the surface. This is good because it avoids to send unnecessary frames, but there are drawbacks:
- it does not send any frame on start if the device screen does not change,
- after fast motion changes, the last frame may have poor quality.
Input events injection
Control events are received from the client by the
EventController (run in
a separate thread). There are 5 types of input events:
- keycode (cf
- text (special characters may not be handled by keycodes directly),
- mouse motion/click,
- mouse scroll,
- custom command (e.g. to switch the screen on).
The client relies on SDL, which provides cross-platform API for UI, input events, threading, etc.
The video stream is decoded by libav (FFmpeg).
On startup, in addition to libav and SDL initialization, the client must push and start the server on the device, and open a socket so that they may communicate.
Note that the client-server roles are expressed at the application level:
- the server serves video stream and handle requests from the client,
- the client controls the device through the server.
However, the roles are inverted at the network level:
- the client opens a server socket and listen on a port before starting the server,
- the server connects to the client.
This role inversion guarantees that the connection will not fail due to race conditions, and avoids polling.
Once the server is connected, it sends the device information (name and initial screen dimensions). Thus, the client may init the window and renderer, before the first frame is available.
To minimize startup time, SDL initialization is performed while listening for the connection from the server (see commit 90a46b4).
The client uses 3 threads:
- the main thread, executing the SDL event loop,
- the decoder thread, decoding video frames,
- the controller thread, sending control events to the server.
The decoder runs in a separate thread. It uses libav to decode the H.264 stream from the socket, and notifies the main thread when a new frame is available.
There are two frames simultaneously in memory:
- the decoding frame, written by the decoder from the decoder thread,
- the rendering frame, rendered in a texture from the main thread.
When a new decoded frame is available, the decoder swaps the decoding and rendering frame (with proper synchronization). Thus, it immediatly starts to decode a new frame while the main thread renders the last one.
The controller is responsible to send control events to the device. It runs in a separate thread, to avoid I/O on the main thread.
On SDL event, received on the main thread, the input manager creates appropriate control events. It is responsible to convert SDL events to Android events (using convert). It pushes the control events to a blocking queue hold by the controller. On its own thread, the controller takes events from the queue, that it serializes and sends to the client.
UI and event loop
Initialization, input events and rendering are all managed in the main thread.
For more details, go read the code!
If you find a bug, or have an awesome idea to implement, please discuss and contribute ;-)