# Genymobile/scrcpy — codebase unpack

> Canonical page: https://codevetter.com/unpack/scrcpy

This is a deterministic Repo Unpack scan of Genymobile/scrcpy — a structural map, not a review, security audit, or maintainer-endorsed description.

- Scanned commit: 19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac (2026-09-05)
- Last upstream commit: Mention VP8 and VP9 in doc/video.md
- Generated by codevetter 1.12.0 on 2026-09-22
- 367 files scanned · 1.7 MB

## Languages

- C: 180 files
- Java: 98 files
- Markdown: 27 files
- Shell: 20 files
- YAML: 2 files

## Repository structure

- app/ — 201 files
- server/ — 107 files
- doc/ — 22 files
- release/ — 12 files
- .github/ — 5 files
- config/ — 2 files
- gradle/ — 2 files

Probable source entrypoint: `app/` (inferred from the tree, not upstream docs)

## Source layout — app/

- app/data/ — 7 files
- app/deps/ — 10 files
- app/src/ — 168 files
- app/tests/ — 12 files
- app/meson.build — 1 file
- app/scrcpy-windows.manifest — 1 file
- app/scrcpy-windows.rc — 1 file
- app/scrcpy.1 — 1 file

## Structural health signals

Average score 8/10 across 96 files; 22 hotspots. These are deterministic review leads, not proof of a bug.

## Recent history

- 19c1261d2e2c (2026-09-03) Mention VP8 and VP9 in doc/video.md
- 2926c06c5dc3 (2026-07-12) Update links to 4.1
- fa57d7c6b471 (2026-07-10) Bump version to 4.1
- ae94ad858cf3 (2026-07-09) Upgrade libusb (1.0.30)
- 457bcc048abb (2026-07-09) Upgrade FFmpeg (8.1.2)

## Overview

scrcpy is a two-process Android screen-mirroring and remote-control tool: a native C/SDL3+FFmpeg client (app/) runs on Linux/Windows/macOS and pushes a Java 'server' (server/) onto the device via adb, where it is executed with app_process to capture and encode screen/camera video and device audio. Streams travel over up to three sockets (video, audio, control) tunnelled through adb, while a bidirectional control channel injects keyboard/mouse/gamepad events and syncs the clipboard. It is designed for low latency (~35-70ms), no root, and nothing permanently installed on the device.

## System map

The repo contains two deployables: the 'scrcpy' client binary in C (app/src) built by Meson, and 'scrcpy-server', an Android Java program (server/) built by Gradle into a dexed jar pushed to /data/local/tmp and run via app_process. The client entrypoint is app/src/main.c -> scrcpy() in app/src/scrcpy.c, which owns a single static struct aggregating server connection, screen, audio player, demuxers, decoders, recorder, controller and optional USB/V4L2 subsystems. The server entrypoint is Server.main -> internalMain -> scrcpy() in Server.java, which wires AsyncProcessors (Controller, audio encoder/recorder, SurfaceEncoder) onto sockets opened through DesktopConnection. Communication is over adb-tunnelled TCP/local sockets (adb reverse by default, forward with --force-adb-forward). CI/release is driven by .github/workflows/release.yml calling scripts in release/.
- The system is split into a host client (C, app/src) and an on-device server (Java, server/src); the client pushes the server and starts it, then they communicate over 1-3 sockets for video, audio and control. — [doc/develop.md](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/doc/develop.md) [app/src/scrcpy.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c) [server/src/main/java/com/genymobile/scrcpy/Server.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java)
- Client entrypoint: main() in app/src/main.c converts Windows UTF-16 argv to UTF-8, calls main_scrcpy() which runs scrcpy_parse_args() then either scrcpy() or scrcpy_otg() (when built with HAVE_USB and --otg is passed). — [app/src/main.c#L27-175](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/main.c#L27-L175) [app/src/usb/scrcpy_otg.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/usb/scrcpy_otg.c)
- scrcpy() in app/src/scrcpy.c is the client orchestrator: it fills a static 'struct scrcpy' containing sc_server, sc_screen, sc_audio_player, two sc_demuxer, two sc_decoder, sc_recorder, sc_video_regulator, sc_controller and sc_file_pusher, plus unions of SDK/UHID/AOA input backends. — [app/src/scrcpy.c#L51-95](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L51-L95) [app/src/scrcpy.c#L320-425](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L320-L425)
- The client pushes the server jar to /data/local/tmp/scrcpy-server.jar and executes 'adb -s <serial> shell CLASSPATH=<jar> app_process / com.genymobile.scrcpy.Server <version> key=value...' in execute_server(). — [app/src/server.c#L18-21](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L18-L21) [app/src/server.c#L208-262](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L208-L262)
- Server-side entry: Server.main -> internalMain drops root privileges to uid 2000 (shell), installs a main Looper via reflection on Looper.sMainLooper (quitAllowed), then Options.parse() validates the client version arg before scrcpy() opens DesktopConnection and starts AsyncProcessors. — [server/src/main/java/com/genymobile/scrcpy/Server.java#L197-291](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L197-L291) [server/src/main/java/com/genymobile/scrcpy/Options.java#L322-335](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Options.java#L322-L335)
- The control protocol version is tied to the app version: the client passes SCRCPY_VERSION as argv[1] and Options.parse() throws IllegalArgumentException if it differs from BuildConfig.VERSION_NAME. — [app/src/server.c#L253](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L253) [server/src/main/java/com/genymobile/scrcpy/Options.java#L328-332](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Options.java#L328-L332)
- DesktopConnection.open() creates/connects LocalSocket(s) named 'scrcpy_%08x' (scid) — listening with LocalServerSocket in tunnel-forward mode, connecting otherwise — for video, audio and control channels, optionally writing a dummy byte so the client detects a dead tunnel. — [server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java#L47-116](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java#L47-L116) [app/src/server.c#L489-539](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L489-L539)
- Client dependencies are FFmpeg (libavformat, libavcodec, libavutil, libswresample), SDL3 >= 3.2.0, optionally libavdevice (V4L2) and libusb-1.0 (USB/HID/OTG); on Windows it links mingw32 and ws2_32. — [app/meson.build#L125-144](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/meson.build#L125-L144)
- Build system: root meson.build declares project 'scrcpy' v4.1 (C11) with options compile_app/compile_server/prebuilt_server/portable/static/server_debugger/v4l2/usb; the server is produced by a Meson custom_target invoking server/scripts/build-wrapper.sh (Gradle) unless prebuilt_server is set. — [meson.build](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/meson.build) [meson_options.txt](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/meson_options.txt) [server/meson.build#L1-25](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/meson.build#L1-L25)
- scrcpy-server is an Android Gradle module (com.android.application plugin, compileSdk 36, minSdk 21, targetSdk 36) with JUnit testImplementation and a checkstyle config applied from config/android-checkstyle.gradle. — [server/build.gradle](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/build.gradle) [config/android-checkstyle.gradle](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/config/android-checkstyle.gradle)
- Threading model: one dedicated thread per socket direction — run_server (adb process + tunnel), run_demuxer per stream, run_controller + receiver for control, run_file_pusher — coordinated through SDL events posted to the main thread event_loop(). — [app/src/scrcpy.c#L116-153](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L116-L153) [app/src/server.c#L964-1189](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L964-L1189) [app/src/demuxer.c#L176-355](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/demuxer.c#L176-L355) [app/src/controller.c#L162-217](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/controller.c#L162-L217)
- Release pipeline: .github/workflows/release.yml (workflow_dispatch) tests server+client, builds server jar and client dists for linux-x86_64/win32/win64/macos aarch64+x86_64, then packages and checksums them via release/*.sh scripts. — [.github/workflows/release.yml](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/.github/workflows/release.yml) [release/test_client.sh](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/release/test_client.sh) [release/test_server.sh](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/release/test_server.sh)

## Feature catalog

Feature surface is almost entirely CLI-driven: cli.c defines a table of ~100 long options (many with short forms) parsed via getopt_long into scrcpy_options. Major features: video mirroring (display or camera, five codecs), audio forwarding (four codecs, many sources), input control via three injection modes (SDK/UHID/AOA), screen recording to several container formats, virtual/flex displays, app launching, file push and APK install by drag-and-drop, clipboard autosync, TCP/IP wireless setup, OTG mode without adb debugging, V4L2 webcam sink, and --list-* introspection commands executed server-side.
- The full CLI surface is a static 'struct sc_option options[]' table in cli.c (~100 entries: --video-codec, --audio-source, --record, --new-display, --otg, --v4l2-sink, --tcpip, --keyboard, --mouse, --gamepad, --start-app, --list-*, etc.) fed to getopt_long by parse_args_with_getopt(). — [app/src/cli.c#L149-220](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/cli.c#L149-L220) [app/src/cli.c#L2492-2498](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/cli.c#L2492-L2498) [app/src/cli.c#L3561-3568](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/cli.c#L3561-L3568)
- Video codecs h264/h265/av1/vp8/vp9 and audio codecs opus/aac/flac/raw are selectable; sc_server_get_codec_name() maps them to server param strings and the demuxer maps wire codec ids to AVCodecID. — [app/src/server.c#L105-130](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L105-L130) [app/src/demuxer.c#L19-58](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/demuxer.c#L19-L58)
- Audio sources include output, playback, mic and mic variants (mic-unprocessed, mic-camcorder, mic-voice-recognition, mic-voice-communication, voice-call[-uplink|-downlink], voice-performance); server-side they map to AudioDirectCapture or AudioPlaybackCapture. — [app/src/server.c#L147-176](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L147-L176) [server/src/main/java/com/genymobile/scrcpy/Server.java#L119-137](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L119-L137)
- Recording (--record) supports mp4, m4a, aac, mkv, mka, opus, flac and wav containers via FFmpeg muxers, with per-stream PTS rescaling and monotonic-PTS fixing. — [app/src/recorder.c#L64-120](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/recorder.c#L64-L120)
- Input injection has three backends per device class: SDK (injectInputEvent via controller), UHID (virtual HID devices created on-device) and AOA over USB (HAVE_USB builds); the union members keyboard_sdk/uhid/aoa, mouse_sdk/uhid/aoa and gamepad_uhid/aoa are selected by --keyboard/--mouse/--gamepad modes. — [app/src/scrcpy.c#L609-738](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L609-L738) [app/src/uhid/keyboard_uhid.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/uhid/keyboard_uhid.c) [app/src/usb/keyboard_aoa.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/usb/keyboard_aoa.c)
- Virtual display features: --new-display creates a NewDisplayCapture, --start-app sends SC_CONTROL_MSG_TYPE_START_APP, --display-ime-policy/vd_system_decorations/vd_destroy_content/flex_display tune it; requires Android 10+. — [server/src/main/java/com/genymobile/scrcpy/Server.java#L139-160](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L139-L160) [server/src/main/java/com/genymobile/scrcpy/Server.java#L77-86](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L77-L86) [app/src/control_msg.h#L29-53](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/control_msg.h#L29-L53)
- OTG mode (--otg) runs scrcpy_otg() which drives keyboard/mouse/gamepad over Android Open Accessory HID without adb debugging. — [app/src/main.c#L101-105](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/main.c#L101-L105) [app/src/usb/scrcpy_otg.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/usb/scrcpy_otg.c)
- V4L2 sink (--v4l2-sink, Linux-only HAVE_V4L2) registers avdevice and pipes decoded frames to a /dev/videoN device through sc_v4l2_sink with an optional v4l2_regulator buffer. — [app/src/scrcpy.c#L812-829](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L812-L829) [app/src/main.c#L83-87](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/main.c#L83-L87) [meson_options.txt](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/meson_options.txt)
- Clipboard autosync is bidirectional: the server registers a primaryClipChanged listener sending DeviceMessage clipboard updates, and the client receiver applies them via SDL_SetClipboardText. — [server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L144-163](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L144-L163) [app/src/receiver.c#L40-60](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/receiver.c#L40-L60) [app/src/device_msg.h#L14-35](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/device_msg.h#L14-L35)
- File drop/APK install: sc_file_pusher runs a thread draining a request queue — 'adb push' files to push_target (default /sdcard/Download/) then sends SC_CONTROL_MSG_TYPE_SCAN_FILE so the device media-scans them, or 'adb install' for APKs. — [app/src/file_pusher.c#L12-180](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/file_pusher.c#L12-L180)
- --tcpip automates wireless setup: get device IP via getprop, 'adb tcpip 5555', poll service.adb.tcp.port until enabled, then 'adb connect ip:port'; '--tcpip=+addr' disconnects first. — [app/src/server.c#L783-953](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L783-L953)
- --list-encoders/--list-displays/--list-cameras/--list-camera-sizes/--list-apps run a short-lived server invocation that prints LogUtils-built lists instead of mirroring. — [server/src/main/java/com/genymobile/scrcpy/Server.java#L247-269](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L247-L269) [app/src/server.c#L443-457](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L443-L457) [app/src/server.c#L1060-1072](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L1060-L1072)
- Camera mirroring (--video-source=camera, Android 12+) uses CameraCapture with facing/size/ar/fps/high-speed/torch/zoom options, and control messages CAMERA_SET_TORCH/ZOOM_IN/ZOOM_OUT are dispatched to the capture. — [server/src/main/java/com/genymobile/scrcpy/Server.java#L72-75](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L72-L75) [server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L422-438](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L422-L438) [app/src/server.c#L345-373](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L345-L373)
- Device-control extras: --turn-screen-off sends SET_DISPLAY_POWER after setup, --stay-awake, --power-off-on-close, --no-power-on, --keep-active (4s simulated user activity thread), and --kill-adb-on-close kills the whole adb server. — [app/src/scrcpy.c#L849-859](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L849-L859) [server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L229-271](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L229-L271) [app/src/server.c#L955-962](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L955-L962)

## Data flow

Outbound (device->host): capture (screen/camera/mic) -> MediaCodec encoder -> Streamer writes a 4-byte codec id then 12-byte frame/session headers + raw packets onto the video/audio sockets -> client demuxer threads parse headers into AVPackets -> packet_source fans out to sinks (decoder -> screen/v4l2, recorder). Inbound (host->device): SDL input events -> processors build sc_control_msg -> controller queue -> serialized onto the control socket -> ControlMessageReader -> Controller.handleEvent -> Device/InputManager injection or UHID writes. The control socket is also used device->client for DeviceMessages (clipboard, ack, UHID output).
- Wire format: each stream starts with a 4-byte codec id (ASCII like 'h264'); then video gets a 12-byte session header (MSB set, width/height) and each media packet is a 12-byte header (64-bit PTS+flags: bit62 config, bit61 keyframe; 32-bit size) followed by raw packet data. — [app/src/demuxer.c#L12-138](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/demuxer.c#L12-L138) [server/src/main/java/com/genymobile/scrcpy/device/Streamer.java#L17-124](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java#L17-L124)
- Server video pipeline: SurfaceCapture (ScreenCapture / NewDisplayCapture / CameraCapture) feeds a Surface into SurfaceEncoder wrapping MediaCodec; encoded buffers go through Streamer.writePacket() to the video FileDescriptor. — [server/src/main/java/com/genymobile/scrcpy/Server.java#L139-160](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L139-L160) [server/src/main/java/com/genymobile/scrcpy/device/Streamer.java#L68-89](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java#L68-L89)
- Server audio pipeline: AudioCapture (AudioDirectCapture for mic-family sources, AudioPlaybackCapture otherwise) is encoded by AudioEncoder or sent raw by AudioRawRecorder through a Streamer onto the audio fd. — [server/src/main/java/com/genymobile/scrcpy/Server.java#L119-137](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L119-L137) [server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java)
- Client demuxer run_demuxer() reads the codec id (0 = stream disabled by device, 1 = config error), opens an AVCodecContext (LOW_DELAY, YUV420P for video, stereo 48kHz for audio), then loops pushing session updates and merged packets to all packet_source sinks. — [app/src/demuxer.c#L176-355](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/demuxer.c#L176-L355)
- H.264/H.265 config packets are merged into the following media packet by sc_packet_merger before dispatch; other codecs pass through. — [app/src/demuxer.c#L284-344](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/demuxer.c#L284-L344) [app/src/packet_merger.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/packet_merger.c)
- Sink fan-out is wired in scrcpy(): video_demuxer.packet_source -> video_decoder.packet_sink -> (optional video_regulator) -> screen.frame_sink and/or v4l2_sink; both demuxers also feed recorder.packet_sinks when --record is set. — [app/src/scrcpy.c#L532-589](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L532-L589) [app/src/scrcpy.c#L792-829](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L792-L829)
- Control uplink: sc_controller_push_msg() enqueues sc_control_msg (limit SC_CONTROL_MSG_QUEUE_LIMIT=60; only UHID_CREATE/UHID_DESTROY are non-droppable), run_controller serializes with sc_control_msg_serialize() into a static buffer and net_send_all()s to the control socket; RESIZE_DISPLAY is coalesced via a separate field. — [app/src/controller.c#L7-217](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/controller.c#L7-L217) [app/src/control_msg.c#L358-366](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/control_msg.c#L358-L366)
- Control downlink: ControlMessageReader.read() parses the same type byte + big-endian fields into ControlMessage objects, then Controller.handleEvent() dispatches to injectKeycode/injectTouch/injectScroll/clipboard/UHID/camera actions. — [server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java#L25-70](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java#L25-L70) [server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L321-441](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L321-L441)
- Device->client messages (clipboard text, clipboard ack, UHID output) are serialized by DeviceMessageWriter/DeviceMessageSender onto the control socket and deserialized in the client receiver thread, which schedules SDL clipboard writes on the main thread. — [app/src/device_msg.h#L14-45](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/device_msg.h#L14-L45) [app/src/receiver.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/receiver.c) [server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java)
- Handshake metadata: the server sends a fixed 64-byte device name on the first connected socket (sendDeviceMeta), read by client device_read_info() into sc_server_info for use as the window title. — [server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java#L155-165](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java#L155-L165) [app/src/server.c#L596-610](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L596-L610)
- Options flow: CLI -> scrcpy_options -> sc_server_params -> execute_server() renders 'key=value' args (with VALIDATE_STRING against shell metacharacters) -> Server Options.parse() switch reconstructs typed Options. — [app/src/server.c#L256-458](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L256-L458) [server/src/main/java/com/genymobile/scrcpy/Options.java#L336-574](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Options.java#L336-L574)
- Recorder muxing: packet sinks reference AVPackets into a queue; the recorder thread writes them with av_interleaved_write_frame() after rescaling from SCRCPY_TIME_BASE (1/1e6 us) to each stream's time_base, fixing non-monotonic PTS. — [app/src/recorder.c#L21-120](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/recorder.c#L21-L120)
- Session identity: client generates a 31-bit random scid per run; the device socket name 'scrcpy_%08x' lets multiple scrcpy instances coexist on one device. — [app/src/scrcpy.c#L276-283](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L276-L283) [server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java#L47-54](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java#L47-L54)

## Behavior traces

Three end-to-end flows were traced. (1) Startup: CLI parse -> SDL init -> server thread does adb start-server, device selection, push, tunnel open, app_process exec, socket handshake, device meta read -> demuxer/decoder/screen init -> SDL event loop. (2) Input: SDL key/mouse event -> input_manager/keyboard_sdk/mouse_sdk -> sc_control_msg -> controller queue -> serialized write -> server ControlMessageReader -> Controller.handleEvent -> Device.injectEvent or UhidManager. (3) Shutdown/disconnect: SDL_QUIT or demuxer EOS -> sc_server_stop interrupts sockets -> adb process watchdog (1s grace then SIGKILL) -> threads joined in careful order (demuxers before screen destroy).
- Startup order in main_scrcpy(): parse args -> set log level -> optional terminal title -> net_init() -> sc_main_thread_init() -> scrcpy(&opts); scrcpy() then SDL_Init(EVENTS) -> sc_server_init + sc_sdl_set_hints -> sc_server_start. — [app/src/main.c#L27-126](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/main.c#L27-L126) [app/src/scrcpy.c#L320-449](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L320-L449)
- Server-connection trace (run_server thread): sc_adb_start_server -> sc_adb_select_device (serial/-e/-d/$ANDROID_SERIAL) -> optional TCP/IP switch -> push_server -> sc_adb_tunnel_open -> execute_server -> sc_process_observer_init -> sc_server_connect_to -> cbs->on_connected. — [app/src/server.c#L964-1122](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L964-L1122)
- Tunnel connect: in reverse mode the client accept()s video then audio then control sockets on the adb-forwarded local socket; in forward mode it connect()s with retry (100 attempts x 100ms) and a dummy-byte read to detect dead tunnels; control socket gets TCP_NODELAY. — [app/src/server.c#L489-714](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L489-L714) [server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java#L56-116](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java#L56-L116)
- Post-connect init order in scrcpy(): file_pusher -> video/audio demuxer init -> decoders attached as packet sinks -> recorder init+start -> controller configure/start -> screen init -> demuxer start -> optional screen-off msg, timeout, start-app msg -> event_loop(). — [app/src/scrcpy.c#L521-906](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L521-L906)
- Server-side startup: Server.scrcpy() applies Workarounds, opens DesktopConnection, sends device meta, creates Controller (control), audio capture+encoder, SurfaceCapture+SurfaceEncoder, then starts each AsyncProcessor and blocks in Looper.loop() until Completion counts them down or a fatal error quits the looper. — [server/src/main/java/com/genymobile/scrcpy/Server.java#L71-195](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L71-L195)
- Keyboard trace: SDL key event -> sc_input_manager (shortcut_mods decide local shortcut vs device key) -> keyboard_sdk builds INJECT_KEYCODE/INJECT_TEXT msgs -> sc_controller_push_msg -> run_controller -> sc_control_msg_serialize -> socket -> ControlMessageReader.parseInjectKeycode -> Controller.injectKeycode -> Device.injectEvent(ASYNC). — [app/src/input_manager.c#L61-106](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/input_manager.c#L61-L106) [app/src/keyboard_sdk.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/keyboard_sdk.c) [app/src/controller.c#L95-160](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/controller.c#L95-L160) [server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java#L72-78](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java#L72-L78) [server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L443-468](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L443-L468)
- Clipboard device->host trace: ClipboardManager.addPrimaryClipChangedListener -> Device.getClipboardText -> DeviceMessageSender.send -> control socket -> client receiver thread -> sc_device_msg_deserialize -> task_set_clipboard on main thread -> SDL_SetClipboardText. — [server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L146-159](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L146-L159) [app/src/receiver.c#L40-60](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/receiver.c#L40-L60) [app/src/device_msg.h#L14-35](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/device_msg.h#L14-L35)
- Event loop dispatch: SC_EVENT_DEVICE_DISCONNECTED/CONTROLLER_ERROR/DEMUXER_ERROR/RECORDER_ERROR/AOA_OPEN_ERROR/TIME_LIMIT_REACHED map to exit codes; all other events (including SC_EVENT_NEW_FRAME, SC_EVENT_OPEN_WINDOW pushed by screen) go to sc_screen_handle_event(). — [app/src/scrcpy.c#L116-153](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L116-L153) [app/src/screen.c#L1130-1262](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/screen.c#L1130-L1262)
- Demuxer end-of-stream posts SC_EVENT_DEVICE_DISCONNECTED (EOS) or SC_EVENT_DEMUXER_ERROR; audio demuxer tolerates DISABLED unless --require-audio; video DISABLED is asserted impossible. — [app/src/scrcpy.c#L193-225](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L193-L225)
- Shutdown: event_loop returns -> sc_main_thread_stop drains pending runnables -> sc_server_stop sets stopped + interrupts -> run_server net_interrupt()s the three sockets, waits 1s WATCHDOG_DELAY then sc_process_terminate -> demuxers/screen/controller joined in dependency order (screen destroyed only after demuxer join). — [app/src/scrcpy.c#L906-1035](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L906-L1035) [app/src/server.c#L1131-1171](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L1131-L1171)
- Recording trace: --record -> sc_recorder_init + start before demuxer start; demuxer packet_source pushes AVPacket refs into recorder sinks; recorder thread dequeues, rescales PTS, writes via av_interleaved_write_frame; on_ended failure posts SC_EVENT_RECORDER_ERROR. — [app/src/scrcpy.c#L564-589](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L564-L589) [app/src/recorder.c#L40-120](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/recorder.c#L40-L120) [app/src/scrcpy.c#L182-191](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L182-L191)
- UHID input trace (e.g. --keyboard=uhid): sc_keyboard_uhid_init registers a virtual device; key events -> HID report -> UHID_INPUT control msg -> server UhidManager.writeInput -> /dev/uhid; device output reports come back as DEVICE_MSG_TYPE_UHID_OUTPUT handled by receiver -> uhid_output task. — [app/src/uhid/keyboard_uhid.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/uhid/keyboard_uhid.c) [app/src/uhid/uhid_output.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/uhid/uhid_output.c) [server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L398-406](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L398-L406) [app/src/receiver.c#L14-18](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/receiver.c#L14-L18)

## Testing signals

Client tests are 12 assert-based C programs in app/tests, each compiled as a Meson test() executable — but only when buildtype=debug so asserts execute. They cover adb parsing, binary helpers, CLI parsing, control-message serialization, device-message deserialization, orientation parsing, string/strbuf/vecdeque/vector containers and Windows command building. Server tests are JUnit4 in server/src/test covering ControlMessageReader, DeviceMessageWriter, CodecOptions, BinarySearch, Binary, CommandParser and StringUtils. CI runs release/test_server.sh (gradle test) and release/test_client.sh (meson test) plus a build-without-gradle check; there is no end-to-end socket-protocol or UI test coverage.
- app/tests contains 12 test files (test_adb_parser, test_audiobuf, test_binary, test_cli, test_command_windows, test_control_msg_serialize, test_device_msg_deserialize, test_orientation, test_str, test_strbuf, test_vecdeque, test_vector) registered as meson test() targets. — [app/meson.build#L222-298](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/meson.build#L222-L298)
- Tests are only built when get_option('buildtype') == 'debug', because the tests are plain assert()-based programs compiled with -DSC_TEST. — [app/meson.build#L224-297](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/meson.build#L224-L297) [app/tests/test_control_msg_serialize.c#L9-32](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/tests/test_control_msg_serialize.c#L9-L32)
- Serialization is tested at the byte level: test_control_msg_serialize asserts exact serialized layouts (e.g. INJECT_KEYCODE == 14 bytes with specific keycode/metastate encoding). — [app/tests/test_control_msg_serialize.c#L9-52](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/tests/test_control_msg_serialize.c#L9-L52)
- Server unit tests (JUnit 4.13.2) live in server/src/test/java under control/, model/, util/: ControlMessageReaderTest, DeviceMessageWriterTest, CodecOptionsTest, BinarySearchTest, BinaryTest, CommandParserTest, StringUtilsTest. — [server/build.gradle](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/build.gradle) [server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java)
- CI job test-scrcpy-server runs release/test_server.sh on ubuntu-latest with JDK 17 (zulu) using native gradle. — [.github/workflows/release.yml#L14-29](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/.github/workflows/release.yml#L14-L29)
- CI job test-client installs meson/ffmpeg/SDL deps (SDL3 built by app/deps/sdl.sh since it is not packaged in Ubuntu) and runs release/test_client.sh. — [.github/workflows/release.yml#L69-98](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/.github/workflows/release.yml#L69-L98) [release/test_client.sh](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/release/test_client.sh)
- A dedicated CI job builds the server without Gradle via server/build_without_gradle.sh, keeping the manual javac/d8 path tested. — [.github/workflows/release.yml#L54-67](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/.github/workflows/release.yml#L54-L67) [server/build_without_gradle.sh](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/build_without_gradle.sh)
- Both sides test the same protocol independently (client serializer tests vs server reader tests) but no test exercises a real socket round-trip between client and server; protocol drift would only be caught manually. (inferred) — [app/tests/test_control_msg_serialize.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/tests/test_control_msg_serialize.c) [server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java)
- No tests exist for demuxer, decoder, recorder, screen, server.c, adb process management or any SDL/audio playback path — the largest files (cli.c, screen.c, recorder.c, server.c) are exercised only manually or via release scripts. (inferred) — [app/meson.build#L225-297](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/meson.build#L225-L297)
- test_cli compiles the real cli.c+options.c, so option-table regressions (missing longopt, bad argdesc) are caught in CI. — [app/meson.build#L242-251](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/meson.build#L242-L251) [app/tests/test_cli.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/tests/test_cli.c)

## Risk map

The riskiest areas are: (1) shell-injection-adjacent code — server params are interpolated into an 'adb shell ... app_process' command line and only mitigated by a denylist VALIDATE_STRING, with an explicit comment that args are not properly escaped on Windows; (2) heavy reflection into private Android APIs (Workarounds, FakeContext, wrappers) which can break per Android release; (3) fragile lifecycle ordering — a large goto-cleanup chain in scrcpy() and static buffers/structs; (4) privileged-environment handling (root uid drop, killing the adb server, writing to /data/local/tmp and /sdcard); and (5) untested protocol duality where the same wire format is hand-maintained in C and Java.
- Server parameter values are appended verbatim to an adb shell command line; validate_string() only rejects a denylist of shell metacharacters (' ;'"*$?&`#\|<>[]{}()!~\r\n') and the comment admits proper escaping does not exist on Windows (sys/win/process.c). — [app/src/server.c#L193-206](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L193-L206)
- The command array in execute_server() is a fixed 'const char *cmd[128]' filled by ADD_PARAM macros — adding server options without bound checks could overflow it (only ~60 slots used today). — [app/src/server.c#L216-262](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L216-L262)
- SCRCPY_SERVER_PATH env var overrides the pushed server jar; a malicious local env could push an arbitrary jar executed as shell (uid 2000) on the device. — [app/src/server.c#L26-54](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L26-L54)
- If run as root, the server silently drops to uid 2000 via Os.setuid — a comment notes copy-paste breaks as root; failure is only a warning (best-effort privilege drop). — [server/src/main/java/com/genymobile/scrcpy/Server.java#L279-290](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L279-L290)
- Workarounds uses reflection over private/blocked Android APIs (@SuppressLint PrivateApi/BlockedPrivateApi/SoonBlockedPrivateApi) e.g. ActivityThread internals and fake Context — each new Android version can silently break capture/audio. — [server/src/main/java/com/genymobile/scrcpy/Workarounds.java#L1-30](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Workarounds.java#L1-L30) [server/src/main/java/com/genymobile/scrcpy/FakeContext.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/FakeContext.java)
- prepareMainLooper() uses reflection on the private static field Looper.sMainLooper — an AOSP-internal implementation detail that could change. (inferred) — [server/src/main/java/com/genymobile/scrcpy/Server.java#L197-210](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L197-L210)
- --kill-adb-on-close terminates the entire adb server (sc_adb_kill_server), which has a wide blast radius: it disrupts every other adb session/tool on the machine. — [app/src/server.c#L955-962](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L955-L962)
- The control queue silently drops input events beyond 60 pending messages except UHID_CREATE/UHID_DESTROY; under socket backpressure users lose keystrokes with only a verbose-level trace. — [app/src/controller.c#L7-8](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/controller.c#L7-L8) [app/src/controller.c#L95-126](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/controller.c#L95-L126) [app/src/control_msg.c#L358-366](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/control_msg.c#L358-L366)
- scrcpy() uses a single static 'struct scrcpy' (memset to 42 in debug to catch uninit) and a long goto-based cleanup ladder — adding init steps without updating the teardown ordering risks use-after-free, e.g. screen must be destroyed only after video demuxer join. — [app/src/scrcpy.c#L322-360](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L322-L360) [app/src/scrcpy.c#L1002-1008](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L1002-L1008)
- The wire protocol is implemented twice by hand (control_msg.c/h + demuxer.c vs ControlMessage*.java + Streamer.java); nothing enforces parity except matching constants like SC_CONTROL_MSG_MAX_SIZE == MESSAGE_MAX_SIZE == 256k. — [app/src/control_msg.h#L15-21](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/control_msg.h#L15-L21) [server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java#L14-17](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java#L14-L17)
- Device reads trust the peer: device_read_info() force-NUL-terminates the 64-byte name ('in case the client sends garbage') but packet sizes drive av_new_packet allocations of up to 4GB-1 declared bytes before any cap check. — [app/src/server.c#L596-610](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L596-L610) [app/src/demuxer.c#L141-160](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/demuxer.c#L141-L160)
- File push writes to device paths: server jar to /data/local/tmp (world-writable staging area) and user files to /sdcard/Download/ by default; SCAN_FILE messages point the device media scanner at pushed paths. — [app/src/server.c#L21](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L21) [app/src/file_pusher.c#L12](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/file_pusher.c#L12) [app/src/file_pusher.c#L115-131](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/file_pusher.c#L115-L131)
- Version skew is hard-failed: server refuses to run if client version != BuildConfig.VERSION_NAME, so mismatched server jars fail loudly rather than silently misbehaving. — [server/src/main/java/com/genymobile/scrcpy/Options.java#L328-332](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Options.java#L328-L332)

## Extension points

Extension is deliberately table-driven. Client CLI: add an OPT_* enum + options[] row + case in the getopt switch + field in scrcpy_options. Server options: mirror the param as an ADD_PARAM in execute_server() and a case in Options.parse(). New control messages require a coordinated change in enum sc_control_msg_type, sc_control_msg_serialize/is_droppable/destroy/log, ControlMessage type constants, ControlMessageReader parsing and Controller.handleEvent dispatch. New media sinks plug into the packet_source/packet_sink and frame_source/frame_sink trait interfaces; new input modalities implement key/mouse/gamepad processor interfaces (SDK/UHID/AOA are the three existing backends). Server-side work units implement AsyncProcessor; captures implement SurfaceCapture; audio sources implement AudioCapture.
- Adding a CLI option = new OPT_* enum entry + a row in the static options[] table (shortopt/longopt/argdesc/optional_arg/text) + handling in parse_args_with_getopt's switch; undocumented options are supported by leaving .text NULL. — [app/src/cli.c#L22-149](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/cli.c#L22-L149) [app/src/cli.c#L1431](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/cli.c#L1431)
- Client->server option plumbing is a three-step mirror: sc_server_params field -> ADD_PARAM("key=value") in execute_server() -> case "key" in Options.parse() switch. — [app/src/scrcpy.c#L365-425](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L365-L425) [app/src/server.c#L269-458](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L269-L458) [server/src/main/java/com/genymobile/scrcpy/Options.java#L336-574](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Options.java#L336-L574)
- New control message types must be added symmetrically: enum sc_control_msg_type + serialize/log/droppable/destroy cases client-side, and ControlMessage.TYPE_* + ControlMessageReader.read() case + Controller.handleEvent dispatch server-side. — [app/src/control_msg.h#L29-53](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/control_msg.h#L29-L53) [app/src/control_msg.c#L103-211](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/control_msg.c#L103-L211) [server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java#L25-70](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java#L25-L70) [server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L333-441](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/Controller.java#L333-L441)
- The trait/ directory defines generic producer/consumer interfaces (packet_source/packet_sink for AVPackets, frame_source/frame_sink for AVFrames); recorder, decoders, screen, v4l2_sink and regulators all plug in via *_add_sink without knowing each other. — [app/src/trait/packet_source.h](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/trait/packet_source.h) [app/src/trait/frame_source.h](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/trait/frame_source.h) [app/src/scrcpy.c#L555-589](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L555-L589)
- Input backends implement narrow processor interfaces (sc_key_processor, sc_mouse_processor, sc_gamepad_processor in trait/); scrcpy.c picks sdk/uhid/aoa implementations into union members and passes them to the screen as kp/mp/gp. — [app/src/trait/key_processor.h](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/trait/key_processor.h) [app/src/trait/mouse_processor.h](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/trait/mouse_processor.h) [app/src/trait/gamepad_processor.h](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/trait/gamepad_processor.h) [app/src/scrcpy.c#L706-766](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L706-L766)
- Server capture sources implement SurfaceCapture — ScreenCapture, NewDisplayCapture, CameraCapture — selected by videoSource/newDisplay in Server.scrcpy() and attached to SurfaceEncoder. — [server/src/main/java/com/genymobile/scrcpy/Server.java#L139-155](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L139-L155) [server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java)
- Audio capture is pluggable via AudioCapture implementations AudioDirectCapture and AudioPlaybackCapture chosen by AudioSource.isDirect(). — [server/src/main/java/com/genymobile/scrcpy/Server.java#L119-127](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L119-L127) [server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java)
- Server-side background work implements the AsyncProcessor interface (start/stop/join + TerminationListener); Server keeps a List<AsyncProcessor> it starts and joins uniformly. — [server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java) [server/src/main/java/com/genymobile/scrcpy/Server.java#L103-195](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L103-L195)
- Adding a codec means extending enum sc_codec, sc_server_get_codec_name, the demuxer SC_CODEC_ID_* mapping to AVCodecID, and the server-side VideoCodec/AudioCodec enums with matching wire ids. — [app/src/server.c#L105-130](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L105-L130) [app/src/demuxer.c#L19-58](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/demuxer.c#L19-L58) [server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java)
- Custom SDL event types (SC_EVENT_*) in events.h + sc_push_event provide the extension mechanism for cross-thread signaling into the main event loop. — [app/src/events.h](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/events.h) [app/src/scrcpy.c#L116-153](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L116-L153)
- Recording formats extend enum sc_record_format -> sc_recorder_get_format_name() mapping to FFmpeg muxer names (mp4/matroska/opus/flac/wav). — [app/src/recorder.c#L64-83](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/recorder.c#L64-L83)
- Feature flags compile out whole subsystems: HAVE_USB gates OTG/AOA code, HAVE_V4L2 gates the webcam sink, SERVER_DEBUGGER adds jdwp flags to the app_process command. — [app/meson.build#L104-119](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/meson.build#L104-L119) [app/src/server.c#L225-249](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L225-L249) [meson_options.txt](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/meson_options.txt)

## Contributor handoff

Build with meson+ninja (tests only exist in debug buildtype); the server needs the Android SDK via Gradle or can be skipped with -Dprebuilt_server=path. The code is dense C11 with an sc_ prefix convention, callback-struct vtables, vecdeque containers, and a strict init/start/stop/join/destroy lifecycle; every thread posts SDL events rather than touching shared UI state. The single most important invariant is that the C wire protocol and the Java wire protocol are manually kept in sync — always change both sides. Read doc/develop.md, app/src/scrcpy.c, app/src/server.c and Server.java before editing anything cross-cutting.
- Read first: doc/develop.md (architecture overview), app/src/scrcpy.c (wiring of all components), app/src/server.c (adb/tunnel/exec logic), server/.../Server.java + Options.java (server lifecycle and params), app/src/control_msg.h + ControlMessageReader.java (protocol contract). — [doc/develop.md](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/doc/develop.md) [app/src/scrcpy.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c) [app/src/server.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c) [server/src/main/java/com/genymobile/scrcpy/Server.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java) [app/src/control_msg.h](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/control_msg.h)
- Build/test commands: 'meson setup builddir && ninja -C builddir'; tests need -Dbuildtype=debug then 'meson test -C builddir'; run helper './run builddir <args>'; server alone via server/build_without_gradle.sh or gradle; CI equivalents are release/test_client.sh and release/test_server.sh. — [app/meson.build#L224-297](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/meson.build#L224-L297) [run](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/run) [server/build_without_gradle.sh](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/build_without_gradle.sh) [release/test_client.sh](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/release/test_client.sh) [release/test_server.sh](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/release/test_server.sh)
- Convention: every subsystem exposes sc_<name>_init/start/stop/join/destroy and a cbs struct of function pointers (on_ended, on_connected, ...); follow this exact lifecycle when adding components — teardown order in scrcpy() is load-bearing. — [app/src/scrcpy.c#L337-360](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L337-L360) [app/src/scrcpy.c#L914-1035](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L914-L1035)
- Any protocol change must be applied twice: enum/serialize in app/src/control_msg.[ch] AND ControlMessage/ControlMessageReader server-side; same for stream headers (demuxer.c vs Streamer.java) and device messages (device_msg.c vs DeviceMessageWriter). — [app/src/control_msg.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/control_msg.c) [server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java) [app/src/demuxer.c#L72-138](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/demuxer.c#L72-L138) [server/src/main/java/com/genymobile/scrcpy/device/Streamer.java#L17-124](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java#L17-L124)
- When adding a server parameter you MUST also update VALIDATE_STRING usage if the value is user-controlled — values are interpolated into an adb shell command and are deliberately restricted from containing shell metacharacters. — [app/src/server.c#L193-206](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L193-L206) [app/src/server.c#L305-337](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L305-L337)
- SCRCPY_SERVER_PATH (env) and devenv SCRCPY_SERVER_PATH let devs point the client at a locally-built server jar; PORTABLE builds instead look for scrcpy-server next to the binary. — [app/src/server.c#L26-54](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L26-L54) [server/meson.build#L27-31](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/meson.build#L27-L31)
- assert() is used as a real correctness mechanism client-side and is compiled out in release (b_ndebug=if-release) — never put side effects in asserts; debug builds memset the scrcpy struct to 42 to catch missed initializations. — [meson.build#L7](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/meson.build#L7) [app/src/scrcpy.c#L322-327](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L322-L327)
- Threads never call SDL functions off the main thread; they sc_push_event(SC_EVENT_*) and the event_loop dispatches — keep this rule or races in SDL will follow (comment at sc_sdl_set_hints). — [app/src/scrcpy.c#L440-442](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/scrcpy.c#L440-L442) [app/src/events.h](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/events.h) [app/src/receiver.c#L40-60](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/receiver.c#L40-L60)
- Windows-specific traps: argv arrives UTF-16 and is converted to UTF-8 in main(); process spawning lives in sys/win/process.c and does not shell-escape args; stdout/stderr are unbuffered for logging. — [app/src/main.c#L29-34](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/main.c#L29-L34) [app/src/main.c#L128-175](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/main.c#L128-L175) [app/src/server.c#L195-200](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L195-L200)
- cli.c is ~3600 lines but highly regular — never hand-edit getopt strings; the optstring and longopts are generated from the options[] table by sc_getopt_adapter_create_optstring(). — [app/src/cli.c#L1268-1364](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/cli.c#L1268-L1364) [app/src/cli.c#L3561-3568](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/cli.c#L3561-L3568)
- Safe edit boundaries: util/ helpers and trait/ interfaces are self-contained; adding a feature typically means options.c default + cli.c row + server.c ADD_PARAM + Options.java case + a Server.java wiring line — touch all five or the flag silently does nothing. (inferred) — [app/src/options.c](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/options.c) [app/src/cli.c#L149-220](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/cli.c#L149-L220) [app/src/server.c#L269-458](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/app/src/server.c#L269-L458) [server/src/main/java/com/genymobile/scrcpy/Options.java#L336-574](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Options.java#L336-L574) [server/src/main/java/com/genymobile/scrcpy/Server.java#L71-195](https://github.com/Genymobile/scrcpy/blob/19c1261d2e2cbf2b5e6a71a8b64cc1dd3ede06ac/server/src/main/java/com/genymobile/scrcpy/Server.java#L71-L195)

Source repository: https://github.com/Genymobile/scrcpy
Full evidence corpus: https://github.com/Codevetter/codevetter/tree/main/benchmarks/repo-unpacks/scrcpy.json

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
