|
| 1 | +# scrcpy for developers |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This application is composed of two parts: |
| 6 | + - the server (`scrcpy-server.jar`), to be executed on the device, |
| 7 | + - the client (the `scrcpy` binary), executed on the host computer. |
| 8 | + |
| 9 | +The client is responsible to push the server to the device and start its |
| 10 | +execution. |
| 11 | + |
| 12 | +Once the client and the server are connected to each other, the server initially |
| 13 | +sends device information (name and initial screen dimensions), then starts to |
| 14 | +send a raw H.264 video stream of the device screen. The client decodes the video |
| 15 | +frames, and display them as soon as possible, without buffering, to minimize |
| 16 | +latency. The client is not aware of the device rotation (which is handled by the |
| 17 | +server), it just knows the dimensions of the video frames. |
| 18 | + |
| 19 | +The client captures relevant keyboard and mouse events, that it transmits to the |
| 20 | +server, which injects them to the device. |
| 21 | + |
| 22 | + |
| 23 | + |
| 24 | +## Server |
| 25 | + |
| 26 | + |
| 27 | +### Privileges |
| 28 | + |
| 29 | +Capturing the screen requires some privileges, which are granted to `shell`. |
| 30 | + |
| 31 | +The server is a Java application (with a [`public static void main(String... |
| 32 | +args)`][main] method), compiled against the Android framework, and executed as |
| 33 | +`shell` on the Android device. |
| 34 | + |
| 35 | +[main]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/Server.java#L61 |
| 36 | + |
| 37 | +To run such a Java application, the classes must be [_dexed_][dex] (typically, |
| 38 | +to `classes.dex`). If `my.package.MainClass` is the main class, compiled to |
| 39 | +`classes.dex`, pushed to the device in `/data/local/tmp`, then it can be run |
| 40 | +with: |
| 41 | + |
| 42 | + adb shell CLASSPATH=/data/local/tmp/classes.dex \ |
| 43 | + app_process / my.package.MainClass |
| 44 | + |
| 45 | +_The path `/data/local/tmp` is a good candidate to push the server, since it's |
| 46 | +readable and writable by `shell`, but not world-writable, so a malicious |
| 47 | +application may not replace the server just before the client executes it._ |
| 48 | + |
| 49 | +Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing |
| 50 | +`classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle |
| 51 | +build system, the server is built to an (unsigned) APK (renamed to |
| 52 | +`scrcpy-server.jar`). |
| 53 | + |
| 54 | +[dex]: https://en.wikipedia.org/wiki/Dalvik_(software) |
| 55 | +[apk]: https://en.wikipedia.org/wiki/Android_application_package |
| 56 | + |
| 57 | + |
| 58 | +### Hidden methods |
| 59 | + |
| 60 | +Although compiled against the Android framework, [hidden] methods and classes are |
| 61 | +not directly accessible (and they may differ from one Android version to |
| 62 | +another). |
| 63 | + |
| 64 | +They can be called using reflection though. The communication with hidden |
| 65 | +components is provided by [_wrappers_ classes][wrappers] and [aidl]. |
| 66 | + |
| 67 | +[hidden]: https://stackoverflow.com/a/31908373/1987178 |
| 68 | +[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers |
| 69 | +[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/aidl/android/view |
| 70 | + |
| 71 | + |
| 72 | +### Threading |
| 73 | + |
| 74 | +The server uses 2 threads: |
| 75 | + |
| 76 | + - the **main** thread, encoding and streaming the video to the client; |
| 77 | + - the **controller** thread, listening for _control events_ (typically, |
| 78 | + keyboard and mouse events) from the client. |
| 79 | + |
| 80 | +Since the video encoding is typically hardware, there would be no benefit in |
| 81 | +encoding and streaming in two different threads. |
| 82 | + |
| 83 | + |
| 84 | +### Screen video encoding |
| 85 | + |
| 86 | +The encoding is managed by [`ScreenEncoder`]. |
| 87 | + |
| 88 | +The video is encoded using the [`MediaCodec`] API. The codec takes its input |
| 89 | +from a [surface] associated to the display, and writes the resulting H.264 |
| 90 | +stream to the provided output stream (the socket connected to the client). |
| 91 | + |
| 92 | +[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java |
| 93 | +[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html |
| 94 | +[surface]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L63-L64 |
| 95 | + |
| 96 | +On device [rotation], the codec, surface and display are reinitialized, and a |
| 97 | +new video stream is produced. |
| 98 | + |
| 99 | +New frames are produced only when changes occur on the surface. This is good |
| 100 | +because it avoids to send unnecessary frames, but there are drawbacks: |
| 101 | + |
| 102 | + - it does not send any frame on start if the device screen does not change, |
| 103 | + - after fast motion changes, the last frame may have poor quality. |
| 104 | + |
| 105 | +Both problems are [solved][repeat] by the flag |
| 106 | +[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. |
| 107 | + |
| 108 | +[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L89-L92 |
| 109 | +[repeat]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126 |
| 110 | +[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER |
| 111 | + |
| 112 | + |
| 113 | +### Input events injection |
| 114 | + |
| 115 | +_Control events_ are received from the client by the [`EventController`] (run in |
| 116 | +a separate thread). There are 5 types of input events: |
| 117 | + - keycode (cf [`KeyEvent`]), |
| 118 | + - text (special characters may not be handled by keycodes directly), |
| 119 | + - mouse motion/click, |
| 120 | + - mouse scroll, |
| 121 | + - custom command (e.g. to switch the screen on). |
| 122 | + |
| 123 | +All of them may need to inject input events to the system. To do so, they use |
| 124 | +the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our |
| 125 | +[`InputManager` wrapper][inject-wrapper]). |
| 126 | + |
| 127 | +[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/EventController.java#L70 |
| 128 | +[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html |
| 129 | +[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html |
| 130 | +[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857 |
| 131 | +[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 |
| 132 | + |
| 133 | + |
| 134 | + |
| 135 | +## Client |
| 136 | + |
| 137 | +The client relies on [SDL], which provides cross-platform API for UI, input |
| 138 | +events, threading, etc. |
| 139 | + |
| 140 | +The video stream is decoded by [libav] (FFmpeg). |
| 141 | + |
| 142 | +[SDL]: https://www.libsdl.org |
| 143 | +[libav]: https://www.libav.org/ |
| 144 | + |
| 145 | +### Initialization |
| 146 | + |
| 147 | +On startup, in addition to _libav_ and _SDL_ initialization, the client must |
| 148 | +push and start the server on the device, and open a socket so that they may |
| 149 | +communicate. |
| 150 | + |
| 151 | +Note that the client-server roles are expressed at the application level: |
| 152 | + |
| 153 | + - the server _serves_ video stream and handle requests from the client, |
| 154 | + - the client _controls_ the device through the server. |
| 155 | + |
| 156 | +However, the roles are inverted at the network level: |
| 157 | + |
| 158 | + - the client opens a server socket and listen on a port before starting the |
| 159 | + server, |
| 160 | + - the server connects to the client. |
| 161 | + |
| 162 | +This role inversion guarantees that the connection will not fail due to race |
| 163 | +conditions, and avoids polling. |
| 164 | + |
| 165 | +Once the server is connected, it sends the device information (name and initial |
| 166 | +screen dimensions). Thus, the client may init the window and renderer, before |
| 167 | +the first frame is available. |
| 168 | + |
| 169 | +To minimize startup time, SDL initialization is performed while listening for |
| 170 | +the connection from the server (see commit [90a46b4]). |
| 171 | + |
| 172 | +[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e |
| 173 | + |
| 174 | + |
| 175 | +### Threading |
| 176 | + |
| 177 | +The client uses 3 threads: |
| 178 | + |
| 179 | + - the **main** thread, executing the SDL event loop, |
| 180 | + - the **decoder** thread, decoding video frames, |
| 181 | + - the **controller** thread, sending _control events_ to the server. |
| 182 | + |
| 183 | + |
| 184 | +### Decoder |
| 185 | + |
| 186 | +The [decoder] runs in a separate thread. It uses _libav_ to decode the H.264 |
| 187 | +stream from the socket, and notifies the main thread when a new frame is |
| 188 | +available. |
| 189 | + |
| 190 | +There are two [frames] simultaneously in memory: |
| 191 | + - the **decoding** frame, written by the decoder from the decoder thread, |
| 192 | + - the **rendering** frame, rendered in a texture from the main thread. |
| 193 | + |
| 194 | +When a new decoded frame is available, the decoder _swaps_ the decoding and |
| 195 | +rendering frame (with proper synchronization). Thus, it immediatly starts |
| 196 | +to decode a new frame while the main thread renders the last one. |
| 197 | + |
| 198 | +[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/decoder.c |
| 199 | +[frames]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/frames.h |
| 200 | + |
| 201 | + |
| 202 | +### Controller |
| 203 | + |
| 204 | +The [controller] is responsible to send _control events_ to the device. It runs |
| 205 | +in a separate thread, to avoid I/O on the main thread. |
| 206 | + |
| 207 | +On SDL event, received on the main thread, the [input manager][inputmanager] |
| 208 | +creates appropriate [_control events_][controlevent]. It is responsible to |
| 209 | +convert SDL events to Android events (using [convert]). It pushes the _control |
| 210 | +events_ to a blocking queue hold by the controller. On its own thread, the |
| 211 | +controller takes events from the queue, that it serializes and sends to the |
| 212 | +client. |
| 213 | + |
| 214 | +[controller]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controller.h |
| 215 | +[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controlevent.h |
| 216 | +[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/inputmanager.h |
| 217 | +[convert]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/convert.h |
| 218 | + |
| 219 | + |
| 220 | +### UI and event loop |
| 221 | + |
| 222 | +Initialization, input events and rendering are all [managed][scrcpy] in the main |
| 223 | +thread. |
| 224 | + |
| 225 | +Events are handled in the [event loop], which either updates the [screen] or |
| 226 | +delegates to the [input manager][inputmanager]. |
| 227 | + |
| 228 | +[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c |
| 229 | +[event loop]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c#L38 |
| 230 | +[screen]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/screen.h |
| 231 | + |
| 232 | + |
| 233 | +## Hack |
| 234 | + |
| 235 | +For more details, go read the code! |
| 236 | + |
| 237 | +If you find a bug, or have an awesome idea to implement, please discuss and |
| 238 | +contribute ;-) |
0 commit comments