├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── BUILD.md ├── DEVELOP.md ├── FAQ.it.md ├── FAQ.ko.md ├── FAQ.md ├── FAQ.zh-Hans.md ├── LICENSE ├── README.id.md ├── README.it.md ├── README.jp.md ├── README.ko.md ├── README.md ├── README.pt-br.md ├── README.sp.md ├── README.tr.md ├── README.zh-Hans.md ├── README.zh-Hant.md ├── app ├── meson.build ├── scrcpy.1 ├── src │ ├── adb.c │ ├── adb.h │ ├── android │ │ ├── input.h │ │ └── keycodes.h │ ├── cli.c │ ├── cli.h │ ├── clock.c │ ├── clock.h │ ├── common.h │ ├── compat.c │ ├── compat.h │ ├── control_msg.c │ ├── control_msg.h │ ├── controller.c │ ├── controller.h │ ├── coords.h │ ├── decoder.c │ ├── decoder.h │ ├── device_msg.c │ ├── device_msg.h │ ├── event_converter.c │ ├── event_converter.h │ ├── events.h │ ├── file_handler.c │ ├── file_handler.h │ ├── fps_counter.c │ ├── fps_counter.h │ ├── frame_buffer.c │ ├── frame_buffer.h │ ├── icon.xpm │ ├── input_manager.c │ ├── input_manager.h │ ├── main.c │ ├── opengl.c │ ├── opengl.h │ ├── receiver.c │ ├── receiver.h │ ├── recorder.c │ ├── recorder.h │ ├── scrcpy.c │ ├── scrcpy.h │ ├── screen.c │ ├── screen.h │ ├── server.c │ ├── server.h │ ├── stream.c │ ├── stream.h │ ├── sys │ │ ├── unix │ │ │ └── process.c │ │ └── win │ │ │ └── process.c │ ├── tiny_xpm.c │ ├── tiny_xpm.h │ ├── trait │ │ ├── frame_sink.h │ │ └── packet_sink.h │ ├── util │ │ ├── buffer_util.h │ │ ├── cbuf.h │ │ ├── log.c │ │ ├── log.h │ │ ├── net.c │ │ ├── net.h │ │ ├── process.c │ │ ├── process.h │ │ ├── queue.h │ │ ├── str_util.c │ │ ├── str_util.h │ │ ├── thread.c │ │ ├── thread.h │ │ ├── tick.c │ │ └── tick.h │ ├── v4l2_sink.c │ ├── v4l2_sink.h │ ├── video_buffer.c │ └── video_buffer.h └── tests │ ├── test_buffer_util.c │ ├── test_cbuf.c │ ├── test_cli.c │ ├── test_clock.c │ ├── test_control_msg_serialize.c │ ├── test_device_msg_deserialize.c │ ├── test_queue.c │ └── test_strutil.c ├── assets └── screenshot-debian-600.jpg ├── build.gradle ├── config ├── android-checkstyle.gradle └── checkstyle │ └── checkstyle.xml ├── cross_win32.txt ├── cross_win64.txt ├── data ├── scrcpy-console.bat └── scrcpy-noconsole.vbs ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── install_release.sh ├── meson.build ├── meson_options.txt ├── prebuilt-deps ├── .gitignore ├── Makefile └── prepare-dep ├── release.mk ├── release.sh ├── run ├── scripts └── run-scrcpy.sh ├── server ├── .gitignore ├── build.gradle ├── build_without_gradle.sh ├── meson.build ├── proguard-rules.pro ├── scripts │ └── build-wrapper.sh └── src │ ├── main │ ├── AndroidManifest.xml │ ├── aidl │ │ └── android │ │ │ ├── content │ │ │ └── IOnPrimaryClipChangedListener.aidl │ │ │ └── view │ │ │ └── IRotationWatcher.aidl │ └── java │ │ └── com │ │ └── genymobile │ │ └── scrcpy │ │ ├── CleanUp.java │ │ ├── CodecOption.java │ │ ├── Connection.java │ │ ├── ControlMessage.java │ │ ├── ControlMessageReader.java │ │ ├── Controller.java │ │ ├── DesktopConnection.java │ │ ├── Device.java │ │ ├── DeviceMessage.java │ │ ├── DeviceMessageSender.java │ │ ├── DeviceMessageWriter.java │ │ ├── DisplayInfo.java │ │ ├── FilePushHandler.java │ │ ├── IO.java │ │ ├── InvalidDisplayIdException.java │ │ ├── InvalidEncoderException.java │ │ ├── KeyComposition.java │ │ ├── Ln.java │ │ ├── Options.java │ │ ├── Point.java │ │ ├── Pointer.java │ │ ├── PointersState.java │ │ ├── Position.java │ │ ├── ScreenEncoder.java │ │ ├── ScreenInfo.java │ │ ├── Server.java │ │ ├── Size.java │ │ ├── StringUtils.java │ │ ├── VideoSettings.java │ │ ├── WSServer.java │ │ ├── WebSocketConnection.java │ │ ├── Workarounds.java │ │ └── wrappers │ │ ├── ActivityManager.java │ │ ├── ClipboardManager.java │ │ ├── ContentProvider.java │ │ ├── DisplayManager.java │ │ ├── InputManager.java │ │ ├── PowerManager.java │ │ ├── ServiceManager.java │ │ ├── StatusBarManager.java │ │ ├── SurfaceControl.java │ │ └── WindowManager.java │ └── test │ └── java │ └── com │ └── genymobile │ └── scrcpy │ ├── CodecOptionsTest.java │ ├── ControlMessageReaderTest.java │ ├── DeviceMessageWriterTest.java │ └── StringUtilsTest.java └── settings.gradle /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] I have read the [FAQ](https://github.com/Genymobile/scrcpy/blob/master/FAQ.md). 11 | - [ ] I have searched in existing [issues](https://github.com/Genymobile/scrcpy/issues). 12 | 13 | **Environment** 14 | - OS: [e.g. Debian, Windows, macOS...] 15 | - scrcpy version: [e.g. 1.12.1] 16 | - installation method: [e.g. manual build, apt, snap, brew, Windows release...] 17 | - device model: 18 | - Android version: [e.g. 10] 19 | 20 | **Describe the bug** 21 | A clear and concise description of what the bug is. 22 | 23 | On errors, please provide the output of the console (and `adb logcat` if relevant). 24 | 25 | ``` 26 | Please paste terminal output in a code block. 27 | ``` 28 | 29 | Please do not post screenshots of your terminal, just post the content as text instead. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] I have checked that a similar [feature request](https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3A%22feature+request%22) does not already exist. 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | /dist/ 3 | /build-*/ 4 | /build_*/ 5 | /release-*/ 6 | .idea/ 7 | .gradle/ 8 | /x/ 9 | local.properties 10 | -------------------------------------------------------------------------------- /FAQ.ko.md: -------------------------------------------------------------------------------- 1 | # 자주하는 질문 (FAQ) 2 | 3 | 다음은 자주 제보되는 문제들과 그들의 현황입니다. 4 | 5 | 6 | ### Windows 운영체제에서, 디바이스가 발견되지 않습니다. 7 | 8 | 가장 흔한 제보는 `adb`에 발견되지 않는 디바이스 혹은 권한 관련 문제입니다. 9 | 다음 명령어를 호출하여 모든 것들에 이상이 없는지 확인하세요: 10 | 11 | adb devices 12 | 13 | Windows는 당신의 디바이스를 감지하기 위해 [드라이버]가 필요할 수도 있습니다. 14 | 15 | [드라이버]: https://developer.android.com/studio/run/oem-usb.html 16 | 17 | 18 | ### 내 디바이스의 미러링만 가능하고, 디바이스와 상호작용을 할 수 없습니다. 19 | 20 | 일부 디바이스에서는, [simulating input]을 허용하기 위해서 한가지 옵션을 활성화해야 할 수도 있습니다. 21 | 개발자 옵션에서 (developer options) 다음을 활성화 하세요: 22 | 23 | > **USB debugging (Security settings)** 24 | > _권한 부여와 USB 디버깅을 통한 simulating input을 허용한다_ 25 | 26 | [simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 27 | 28 | 29 | ### 마우스 클릭이 다른 곳에 적용됩니다. 30 | 31 | Mac 운영체제에서, HiDPI support 와 여러 스크린 창이 있는 경우, 입력 위치가 잘못 파악될 수 있습니다. 32 | [issue 15]를 참고하세요. 33 | 34 | [issue 15]: https://github.com/Genymobile/scrcpy/issues/15 35 | 36 | 차선책은 HiDPI support을 비활성화 하고 build하는 방법입니다: 37 | 38 | ```bash 39 | meson x --buildtype release -Dhidpi_support=false 40 | ``` 41 | 42 | 하지만, 동영상은 낮은 해상도로 재생될 것 입니다. 43 | 44 | 45 | ### HiDPI display의 화질이 낮습니다. 46 | 47 | Windows에서는, [scaling behavior] 환경을 설정해야 할 수도 있습니다. 48 | 49 | > `scrcpy.exe` > Properties > Compatibility > Change high DPI settings > 50 | > Override high DPI scaling behavior > Scaling performed by: _Application_. 51 | 52 | [scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 53 | 54 | 55 | ### KWin compositor가 실행되지 않습니다 56 | 57 | Plasma Desktop에서는,_scrcpy_ 가 실행중에는 compositor가 비활성화 됩니다. 58 | 59 | 차석책으로는, ["Block compositing"를 비활성화하세요][kwin]. 60 | 61 | [kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 62 | 63 | 64 | ###비디오 스트림을 열 수 없는 에러가 발생합니다.(Could not open video stream). 65 | 66 | 여러가지 원인이 있을 수 있습니다. 가장 흔한 원인은 디바이스의 하드웨어 인코더(hardware encoder)가 67 | 주어진 해상도를 인코딩할 수 없는 경우입니다. 68 | 69 | ``` 70 | ERROR: Exception on thread Thread[main,5,main] 71 | android.media.MediaCodec$CodecException: Error 0xfffffc0e 72 | ... 73 | Exit due to uncaughtException in main thread: 74 | ERROR: Could not open video stream 75 | INFO: Initial texture: 1080x2336 76 | ``` 77 | 78 | 더 낮은 해상도로 시도 해보세요: 79 | 80 | ``` 81 | scrcpy -m 1920 82 | scrcpy -m 1024 83 | scrcpy -m 800 84 | ``` 85 | -------------------------------------------------------------------------------- /app/src/adb.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_ADB_H 2 | #define SC_ADB_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | 9 | #include "util/process.h" 10 | 11 | process_t 12 | adb_execute(const char *serial, const char *const adb_cmd[], size_t len); 13 | 14 | process_t 15 | adb_forward(const char *serial, uint16_t local_port, 16 | const char *device_socket_name); 17 | 18 | process_t 19 | adb_forward_remove(const char *serial, uint16_t local_port); 20 | 21 | process_t 22 | adb_reverse(const char *serial, const char *device_socket_name, 23 | uint16_t local_port); 24 | 25 | process_t 26 | adb_reverse_remove(const char *serial, const char *device_socket_name); 27 | 28 | process_t 29 | adb_push(const char *serial, const char *local, const char *remote); 30 | 31 | process_t 32 | adb_install(const char *serial, const char *local); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /app/src/cli.h: -------------------------------------------------------------------------------- 1 | #ifndef SCRCPY_CLI_H 2 | #define SCRCPY_CLI_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | 8 | #include "scrcpy.h" 9 | 10 | struct scrcpy_cli_args { 11 | struct scrcpy_options opts; 12 | bool help; 13 | bool version; 14 | }; 15 | 16 | void 17 | scrcpy_print_usage(const char *arg0); 18 | 19 | bool 20 | scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]); 21 | 22 | #ifdef SC_TEST 23 | bool 24 | sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods); 25 | #endif 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /app/src/clock.c: -------------------------------------------------------------------------------- 1 | #include "clock.h" 2 | 3 | #include "util/log.h" 4 | 5 | #define SC_CLOCK_NDEBUG // comment to debug 6 | 7 | void 8 | sc_clock_init(struct sc_clock *clock) { 9 | clock->count = 0; 10 | clock->head = 0; 11 | clock->left_sum.system = 0; 12 | clock->left_sum.stream = 0; 13 | clock->right_sum.system = 0; 14 | clock->right_sum.stream = 0; 15 | } 16 | 17 | // Estimate the affine function f(stream) = slope * stream + offset 18 | static void 19 | sc_clock_estimate(struct sc_clock *clock, 20 | double *out_slope, sc_tick *out_offset) { 21 | assert(clock->count > 1); // two points are necessary 22 | 23 | struct sc_clock_point left_avg = { 24 | .system = clock->left_sum.system / (clock->count / 2), 25 | .stream = clock->left_sum.stream / (clock->count / 2), 26 | }; 27 | struct sc_clock_point right_avg = { 28 | .system = clock->right_sum.system / ((clock->count + 1) / 2), 29 | .stream = clock->right_sum.stream / ((clock->count + 1) / 2), 30 | }; 31 | 32 | double slope = (double) (right_avg.system - left_avg.system) 33 | / (right_avg.stream - left_avg.stream); 34 | 35 | if (clock->count < SC_CLOCK_RANGE) { 36 | /* The first frames are typically received and decoded with more delay 37 | * than the others, causing a wrong slope estimation on start. To 38 | * compensate, assume an initial slope of 1, then progressively use the 39 | * estimated slope. */ 40 | slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count)) 41 | / SC_CLOCK_RANGE; 42 | } 43 | 44 | struct sc_clock_point global_avg = { 45 | .system = (clock->left_sum.system + clock->right_sum.system) 46 | / clock->count, 47 | .stream = (clock->left_sum.stream + clock->right_sum.stream) 48 | / clock->count, 49 | }; 50 | 51 | sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope); 52 | 53 | *out_slope = slope; 54 | *out_offset = offset; 55 | } 56 | 57 | void 58 | sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { 59 | struct sc_clock_point *point = &clock->points[clock->head]; 60 | 61 | if (clock->count == SC_CLOCK_RANGE || clock->count & 1) { 62 | // One point passes from the right sum to the left sum 63 | 64 | unsigned mid; 65 | if (clock->count == SC_CLOCK_RANGE) { 66 | mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE; 67 | } else { 68 | // Only for the first frames 69 | mid = clock->count / 2; 70 | } 71 | 72 | struct sc_clock_point *mid_point = &clock->points[mid]; 73 | clock->left_sum.system += mid_point->system; 74 | clock->left_sum.stream += mid_point->stream; 75 | clock->right_sum.system -= mid_point->system; 76 | clock->right_sum.stream -= mid_point->stream; 77 | } 78 | 79 | if (clock->count == SC_CLOCK_RANGE) { 80 | // The current point overwrites the previous value in the circular 81 | // array, update the left sum accordingly 82 | clock->left_sum.system -= point->system; 83 | clock->left_sum.stream -= point->stream; 84 | } else { 85 | ++clock->count; 86 | } 87 | 88 | point->system = system; 89 | point->stream = stream; 90 | 91 | clock->right_sum.system += system; 92 | clock->right_sum.stream += stream; 93 | 94 | clock->head = (clock->head + 1) % SC_CLOCK_RANGE; 95 | 96 | if (clock->count > 1) { 97 | // Update estimation 98 | sc_clock_estimate(clock, &clock->slope, &clock->offset); 99 | 100 | #ifndef SC_CLOCK_NDEBUG 101 | LOGD("Clock estimation: %g * pts + %" PRItick, 102 | clock->slope, clock->offset); 103 | #endif 104 | } 105 | } 106 | 107 | sc_tick 108 | sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) { 109 | assert(clock->count > 1); // sc_clock_update() must have been called 110 | return (sc_tick) (stream * clock->slope) + clock->offset; 111 | } 112 | -------------------------------------------------------------------------------- /app/src/clock.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_CLOCK_H 2 | #define SC_CLOCK_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | 8 | #include "util/tick.h" 9 | 10 | #define SC_CLOCK_RANGE 32 11 | static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even"); 12 | 13 | struct sc_clock_point { 14 | sc_tick system; 15 | sc_tick stream; 16 | }; 17 | 18 | /** 19 | * The clock aims to estimate the affine relation between the stream (device) 20 | * time and the system time: 21 | * 22 | * f(stream) = slope * stream + offset 23 | * 24 | * To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps 25 | * of a frame expressed both in stream time and system time) in a circular 26 | * array. 27 | * 28 | * To estimate the slope, it splits the last SC_CLOCK_RANGE points into two 29 | * sets of SC_CLOCK_RANGE/2 points, and compute their centroid ("average 30 | * point"). The slope of the estimated affine function is that of the line 31 | * passing through these two points. 32 | * 33 | * To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE 34 | * points. The resulting affine function passes by this centroid. 35 | * 36 | * With a circular array, the rolling sums (and average) are quick to compute. 37 | * In practice, the estimation is stable and the evolution is smooth. 38 | */ 39 | struct sc_clock { 40 | // Circular array 41 | struct sc_clock_point points[SC_CLOCK_RANGE]; 42 | 43 | // Number of points in the array (count <= SC_CLOCK_RANGE) 44 | unsigned count; 45 | 46 | // Index of the next point to write 47 | unsigned head; 48 | 49 | // Sum of the first count/2 points 50 | struct sc_clock_point left_sum; 51 | 52 | // Sum of the last (count+1)/2 points 53 | struct sc_clock_point right_sum; 54 | 55 | // Estimated slope and offset 56 | // (computed on sc_clock_update(), used by sc_clock_to_system_time()) 57 | double slope; 58 | sc_tick offset; 59 | }; 60 | 61 | void 62 | sc_clock_init(struct sc_clock *clock); 63 | 64 | void 65 | sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream); 66 | 67 | sc_tick 68 | sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream); 69 | 70 | #endif 71 | -------------------------------------------------------------------------------- /app/src/common.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMON_H 2 | #define COMMON_H 3 | 4 | #include "config.h" 5 | #include "compat.h" 6 | 7 | #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) 8 | #define MIN(X,Y) (X) < (Y) ? (X) : (Y) 9 | #define MAX(X,Y) (X) > (Y) ? (X) : (Y) 10 | 11 | #define container_of(ptr, type, member) \ 12 | ((type *) (((char *) (ptr)) - offsetof(type, member))) 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /app/src/compat.c: -------------------------------------------------------------------------------- 1 | #include "compat.h" 2 | 3 | #include "config.h" 4 | 5 | #ifndef HAVE_STRDUP 6 | char *strdup(const char *s) { 7 | size_t size = strlen(s) + 1; 8 | char *dup = malloc(size); 9 | if (dup) { 10 | memcpy(dup, s, size); 11 | } 12 | return dup; 13 | } 14 | #endif 15 | -------------------------------------------------------------------------------- /app/src/compat.h: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_H 2 | #define COMPAT_H 3 | 4 | #define _POSIX_C_SOURCE 200809L 5 | #define _XOPEN_SOURCE 700 6 | #define _GNU_SOURCE 7 | #ifdef __APPLE__ 8 | # define _DARWIN_C_SOURCE 9 | #endif 10 | 11 | #include 12 | #include 13 | 14 | // In ffmpeg/doc/APIchanges: 15 | // 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h 16 | // Deprecate use of av_register_input_format(), av_register_output_format(), 17 | // av_register_all(), av_iformat_next(), av_oformat_next(). 18 | // Add av_demuxer_iterate(), and av_muxer_iterate(). 19 | #if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100) 20 | # define SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API 21 | #else 22 | # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL 23 | #endif 24 | 25 | 26 | // In ffmpeg/doc/APIchanges: 27 | // 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h 28 | // Deprecate AVFormatContext filename field which had limited length, use the 29 | // new dynamically allocated url field instead. 30 | // 31 | // 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h 32 | // Add url field to AVFormatContext and add ff_format_set_url helper function. 33 | #if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100) 34 | # define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL 35 | #endif 36 | 37 | #if SDL_VERSION_ATLEAST(2, 0, 5) 38 | // 39 | # define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH 40 | // 41 | # define SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS 42 | // 43 | # define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP 44 | #endif 45 | 46 | #if SDL_VERSION_ATLEAST(2, 0, 8) 47 | // 48 | # define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR 49 | #endif 50 | 51 | #ifndef HAVE_STRDUP 52 | char *strdup(const char *s); 53 | #endif 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /app/src/control_msg.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTROLMSG_H 2 | #define CONTROLMSG_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "android/input.h" 11 | #include "android/keycodes.h" 12 | #include "coords.h" 13 | 14 | #define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k 15 | 16 | #define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 17 | // type: 1 byte; paste flag: 1 byte; length: 4 bytes 18 | #define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6) 19 | 20 | #define POINTER_ID_MOUSE UINT64_C(-1) 21 | #define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2) 22 | 23 | enum control_msg_type { 24 | CONTROL_MSG_TYPE_INJECT_KEYCODE, 25 | CONTROL_MSG_TYPE_INJECT_TEXT, 26 | CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, 27 | CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 28 | CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, 29 | CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, 30 | CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, 31 | CONTROL_MSG_TYPE_COLLAPSE_PANELS, 32 | CONTROL_MSG_TYPE_GET_CLIPBOARD, 33 | CONTROL_MSG_TYPE_SET_CLIPBOARD, 34 | CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, 35 | CONTROL_MSG_TYPE_ROTATE_DEVICE, 36 | }; 37 | 38 | enum screen_power_mode { 39 | // see 40 | SCREEN_POWER_MODE_OFF = 0, 41 | SCREEN_POWER_MODE_NORMAL = 2, 42 | }; 43 | 44 | struct control_msg { 45 | enum control_msg_type type; 46 | union { 47 | struct { 48 | enum android_keyevent_action action; 49 | enum android_keycode keycode; 50 | uint32_t repeat; 51 | enum android_metastate metastate; 52 | } inject_keycode; 53 | struct { 54 | char *text; // owned, to be freed by free() 55 | } inject_text; 56 | struct { 57 | enum android_motionevent_action action; 58 | enum android_motionevent_buttons buttons; 59 | uint64_t pointer_id; 60 | struct position position; 61 | float pressure; 62 | } inject_touch_event; 63 | struct { 64 | struct position position; 65 | int32_t hscroll; 66 | int32_t vscroll; 67 | } inject_scroll_event; 68 | struct { 69 | enum android_keyevent_action action; // action for the BACK key 70 | // screen may only be turned on on ACTION_DOWN 71 | } back_or_screen_on; 72 | struct { 73 | char *text; // owned, to be freed by free() 74 | bool paste; 75 | } set_clipboard; 76 | struct { 77 | enum screen_power_mode mode; 78 | } set_screen_power_mode; 79 | }; 80 | }; 81 | 82 | // buf size must be at least CONTROL_MSG_MAX_SIZE 83 | // return the number of bytes written 84 | size_t 85 | control_msg_serialize(const struct control_msg *msg, unsigned char *buf); 86 | 87 | void 88 | control_msg_log(const struct control_msg *msg); 89 | 90 | void 91 | control_msg_destroy(struct control_msg *msg); 92 | 93 | #endif 94 | -------------------------------------------------------------------------------- /app/src/controller.c: -------------------------------------------------------------------------------- 1 | #include "controller.h" 2 | 3 | #include 4 | 5 | #include "util/log.h" 6 | 7 | bool 8 | controller_init(struct controller *controller, socket_t control_socket) { 9 | cbuf_init(&controller->queue); 10 | 11 | bool ok = receiver_init(&controller->receiver, control_socket); 12 | if (!ok) { 13 | return false; 14 | } 15 | 16 | ok = sc_mutex_init(&controller->mutex); 17 | if (!ok) { 18 | receiver_destroy(&controller->receiver); 19 | return false; 20 | } 21 | 22 | ok = sc_cond_init(&controller->msg_cond); 23 | if (!ok) { 24 | receiver_destroy(&controller->receiver); 25 | sc_mutex_destroy(&controller->mutex); 26 | return false; 27 | } 28 | 29 | controller->control_socket = control_socket; 30 | controller->stopped = false; 31 | 32 | return true; 33 | } 34 | 35 | void 36 | controller_destroy(struct controller *controller) { 37 | sc_cond_destroy(&controller->msg_cond); 38 | sc_mutex_destroy(&controller->mutex); 39 | 40 | struct control_msg msg; 41 | while (cbuf_take(&controller->queue, &msg)) { 42 | control_msg_destroy(&msg); 43 | } 44 | 45 | receiver_destroy(&controller->receiver); 46 | } 47 | 48 | bool 49 | controller_push_msg(struct controller *controller, 50 | const struct control_msg *msg) { 51 | if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { 52 | control_msg_log(msg); 53 | } 54 | 55 | sc_mutex_lock(&controller->mutex); 56 | bool was_empty = cbuf_is_empty(&controller->queue); 57 | bool res = cbuf_push(&controller->queue, *msg); 58 | if (was_empty) { 59 | sc_cond_signal(&controller->msg_cond); 60 | } 61 | sc_mutex_unlock(&controller->mutex); 62 | return res; 63 | } 64 | 65 | static bool 66 | process_msg(struct controller *controller, 67 | const struct control_msg *msg) { 68 | static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; 69 | size_t length = control_msg_serialize(msg, serialized_msg); 70 | if (!length) { 71 | return false; 72 | } 73 | ssize_t w = net_send_all(controller->control_socket, serialized_msg, length); 74 | return (size_t) w == length; 75 | } 76 | 77 | static int 78 | run_controller(void *data) { 79 | struct controller *controller = data; 80 | 81 | for (;;) { 82 | sc_mutex_lock(&controller->mutex); 83 | while (!controller->stopped && cbuf_is_empty(&controller->queue)) { 84 | sc_cond_wait(&controller->msg_cond, &controller->mutex); 85 | } 86 | if (controller->stopped) { 87 | // stop immediately, do not process further msgs 88 | sc_mutex_unlock(&controller->mutex); 89 | break; 90 | } 91 | struct control_msg msg; 92 | bool non_empty = cbuf_take(&controller->queue, &msg); 93 | assert(non_empty); 94 | (void) non_empty; 95 | sc_mutex_unlock(&controller->mutex); 96 | 97 | bool ok = process_msg(controller, &msg); 98 | control_msg_destroy(&msg); 99 | if (!ok) { 100 | LOGD("Could not write msg to socket"); 101 | break; 102 | } 103 | } 104 | return 0; 105 | } 106 | 107 | bool 108 | controller_start(struct controller *controller) { 109 | LOGD("Starting controller thread"); 110 | 111 | bool ok = sc_thread_create(&controller->thread, run_controller, 112 | "controller", controller); 113 | if (!ok) { 114 | LOGC("Could not start controller thread"); 115 | return false; 116 | } 117 | 118 | if (!receiver_start(&controller->receiver)) { 119 | controller_stop(controller); 120 | sc_thread_join(&controller->thread, NULL); 121 | return false; 122 | } 123 | 124 | return true; 125 | } 126 | 127 | void 128 | controller_stop(struct controller *controller) { 129 | sc_mutex_lock(&controller->mutex); 130 | controller->stopped = true; 131 | sc_cond_signal(&controller->msg_cond); 132 | sc_mutex_unlock(&controller->mutex); 133 | } 134 | 135 | void 136 | controller_join(struct controller *controller) { 137 | sc_thread_join(&controller->thread, NULL); 138 | receiver_join(&controller->receiver); 139 | } 140 | -------------------------------------------------------------------------------- /app/src/controller.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTROLLER_H 2 | #define CONTROLLER_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | 8 | #include "control_msg.h" 9 | #include "receiver.h" 10 | #include "util/cbuf.h" 11 | #include "util/net.h" 12 | #include "util/thread.h" 13 | 14 | struct control_msg_queue CBUF(struct control_msg, 64); 15 | 16 | struct controller { 17 | socket_t control_socket; 18 | sc_thread thread; 19 | sc_mutex mutex; 20 | sc_cond msg_cond; 21 | bool stopped; 22 | struct control_msg_queue queue; 23 | struct receiver receiver; 24 | }; 25 | 26 | bool 27 | controller_init(struct controller *controller, socket_t control_socket); 28 | 29 | void 30 | controller_destroy(struct controller *controller); 31 | 32 | bool 33 | controller_start(struct controller *controller); 34 | 35 | void 36 | controller_stop(struct controller *controller); 37 | 38 | void 39 | controller_join(struct controller *controller); 40 | 41 | bool 42 | controller_push_msg(struct controller *controller, 43 | const struct control_msg *msg); 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /app/src/coords.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_COORDS 2 | #define SC_COORDS 3 | 4 | #include 5 | 6 | struct size { 7 | uint16_t width; 8 | uint16_t height; 9 | }; 10 | 11 | struct point { 12 | int32_t x; 13 | int32_t y; 14 | }; 15 | 16 | struct position { 17 | // The video screen size may be different from the real device screen size, 18 | // so store to which size the absolute position apply, to scale it 19 | // accordingly. 20 | struct size screen_size; 21 | struct point point; 22 | }; 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /app/src/decoder.c: -------------------------------------------------------------------------------- 1 | #include "decoder.h" 2 | 3 | #include 4 | 5 | #include "events.h" 6 | #include "video_buffer.h" 7 | #include "trait/frame_sink.h" 8 | #include "util/log.h" 9 | 10 | /** Downcast packet_sink to decoder */ 11 | #define DOWNCAST(SINK) container_of(SINK, struct decoder, packet_sink) 12 | 13 | static void 14 | decoder_close_first_sinks(struct decoder *decoder, unsigned count) { 15 | while (count) { 16 | struct sc_frame_sink *sink = decoder->sinks[--count]; 17 | sink->ops->close(sink); 18 | } 19 | } 20 | 21 | static inline void 22 | decoder_close_sinks(struct decoder *decoder) { 23 | decoder_close_first_sinks(decoder, decoder->sink_count); 24 | } 25 | 26 | static bool 27 | decoder_open_sinks(struct decoder *decoder) { 28 | for (unsigned i = 0; i < decoder->sink_count; ++i) { 29 | struct sc_frame_sink *sink = decoder->sinks[i]; 30 | if (!sink->ops->open(sink)) { 31 | LOGE("Could not open frame sink %d", i); 32 | decoder_close_first_sinks(decoder, i); 33 | return false; 34 | } 35 | } 36 | 37 | return true; 38 | } 39 | 40 | static bool 41 | decoder_open(struct decoder *decoder, const AVCodec *codec) { 42 | decoder->codec_ctx = avcodec_alloc_context3(codec); 43 | if (!decoder->codec_ctx) { 44 | LOGC("Could not allocate decoder context"); 45 | return false; 46 | } 47 | 48 | if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) { 49 | LOGE("Could not open codec"); 50 | avcodec_free_context(&decoder->codec_ctx); 51 | return false; 52 | } 53 | 54 | decoder->frame = av_frame_alloc(); 55 | if (!decoder->frame) { 56 | LOGE("Could not create decoder frame"); 57 | avcodec_close(decoder->codec_ctx); 58 | avcodec_free_context(&decoder->codec_ctx); 59 | return false; 60 | } 61 | 62 | if (!decoder_open_sinks(decoder)) { 63 | LOGE("Could not open decoder sinks"); 64 | av_frame_free(&decoder->frame); 65 | avcodec_close(decoder->codec_ctx); 66 | avcodec_free_context(&decoder->codec_ctx); 67 | return false; 68 | } 69 | 70 | return true; 71 | } 72 | 73 | static void 74 | decoder_close(struct decoder *decoder) { 75 | decoder_close_sinks(decoder); 76 | av_frame_free(&decoder->frame); 77 | avcodec_close(decoder->codec_ctx); 78 | avcodec_free_context(&decoder->codec_ctx); 79 | } 80 | 81 | static bool 82 | push_frame_to_sinks(struct decoder *decoder, const AVFrame *frame) { 83 | for (unsigned i = 0; i < decoder->sink_count; ++i) { 84 | struct sc_frame_sink *sink = decoder->sinks[i]; 85 | if (!sink->ops->push(sink, frame)) { 86 | LOGE("Could not send frame to sink %d", i); 87 | return false; 88 | } 89 | } 90 | 91 | return true; 92 | } 93 | 94 | static bool 95 | decoder_push(struct decoder *decoder, const AVPacket *packet) { 96 | bool is_config = packet->pts == AV_NOPTS_VALUE; 97 | if (is_config) { 98 | // nothing to do 99 | return true; 100 | } 101 | 102 | int ret = avcodec_send_packet(decoder->codec_ctx, packet); 103 | if (ret < 0 && ret != AVERROR(EAGAIN)) { 104 | LOGE("Could not send video packet: %d", ret); 105 | return false; 106 | } 107 | ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame); 108 | if (!ret) { 109 | // a frame was received 110 | bool ok = push_frame_to_sinks(decoder, decoder->frame); 111 | // A frame lost should not make the whole pipeline fail. The error, if 112 | // any, is already logged. 113 | (void) ok; 114 | 115 | av_frame_unref(decoder->frame); 116 | } else if (ret != AVERROR(EAGAIN)) { 117 | LOGE("Could not receive video frame: %d", ret); 118 | return false; 119 | } 120 | return true; 121 | } 122 | 123 | static bool 124 | decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { 125 | struct decoder *decoder = DOWNCAST(sink); 126 | return decoder_open(decoder, codec); 127 | } 128 | 129 | static void 130 | decoder_packet_sink_close(struct sc_packet_sink *sink) { 131 | struct decoder *decoder = DOWNCAST(sink); 132 | decoder_close(decoder); 133 | } 134 | 135 | static bool 136 | decoder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { 137 | struct decoder *decoder = DOWNCAST(sink); 138 | return decoder_push(decoder, packet); 139 | } 140 | 141 | void 142 | decoder_init(struct decoder *decoder) { 143 | decoder->sink_count = 0; 144 | 145 | static const struct sc_packet_sink_ops ops = { 146 | .open = decoder_packet_sink_open, 147 | .close = decoder_packet_sink_close, 148 | .push = decoder_packet_sink_push, 149 | }; 150 | 151 | decoder->packet_sink.ops = &ops; 152 | } 153 | 154 | void 155 | decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink) { 156 | assert(decoder->sink_count < DECODER_MAX_SINKS); 157 | assert(sink); 158 | assert(sink->ops); 159 | decoder->sinks[decoder->sink_count++] = sink; 160 | } 161 | -------------------------------------------------------------------------------- /app/src/decoder.h: -------------------------------------------------------------------------------- 1 | #ifndef DECODER_H 2 | #define DECODER_H 3 | 4 | #include "common.h" 5 | 6 | #include "trait/packet_sink.h" 7 | 8 | #include 9 | #include 10 | 11 | #define DECODER_MAX_SINKS 2 12 | 13 | struct decoder { 14 | struct sc_packet_sink packet_sink; // packet sink trait 15 | 16 | struct sc_frame_sink *sinks[DECODER_MAX_SINKS]; 17 | unsigned sink_count; 18 | 19 | AVCodecContext *codec_ctx; 20 | AVFrame *frame; 21 | }; 22 | 23 | void 24 | decoder_init(struct decoder *decoder); 25 | 26 | void 27 | decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink); 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /app/src/device_msg.c: -------------------------------------------------------------------------------- 1 | #include "device_msg.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "util/buffer_util.h" 7 | #include "util/log.h" 8 | 9 | ssize_t 10 | device_msg_deserialize(const unsigned char *buf, size_t len, 11 | struct device_msg *msg) { 12 | if (len < 5) { 13 | // at least type + empty string length 14 | return 0; // not available 15 | } 16 | 17 | msg->type = buf[0]; 18 | switch (msg->type) { 19 | case DEVICE_MSG_TYPE_CLIPBOARD: { 20 | size_t clipboard_len = buffer_read32be(&buf[1]); 21 | if (clipboard_len > len - 5) { 22 | return 0; // not available 23 | } 24 | char *text = malloc(clipboard_len + 1); 25 | if (!text) { 26 | LOGW("Could not allocate text for clipboard"); 27 | return -1; 28 | } 29 | if (clipboard_len) { 30 | memcpy(text, &buf[5], clipboard_len); 31 | } 32 | text[clipboard_len] = '\0'; 33 | 34 | msg->clipboard.text = text; 35 | return 5 + clipboard_len; 36 | } 37 | default: 38 | LOGW("Unknown device message type: %d", (int) msg->type); 39 | return -1; // error, we cannot recover 40 | } 41 | } 42 | 43 | void 44 | device_msg_destroy(struct device_msg *msg) { 45 | if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { 46 | free(msg->clipboard.text); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/device_msg.h: -------------------------------------------------------------------------------- 1 | #ifndef DEVICEMSG_H 2 | #define DEVICEMSG_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k 11 | // type: 1 byte; length: 4 bytes 12 | #define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) 13 | 14 | enum device_msg_type { 15 | DEVICE_MSG_TYPE_CLIPBOARD, 16 | }; 17 | 18 | struct device_msg { 19 | enum device_msg_type type; 20 | union { 21 | struct { 22 | char *text; // owned, to be freed by free() 23 | } clipboard; 24 | }; 25 | }; 26 | 27 | // return the number of bytes consumed (0 for no msg available, -1 on error) 28 | ssize_t 29 | device_msg_deserialize(const unsigned char *buf, size_t len, 30 | struct device_msg *msg); 31 | 32 | void 33 | device_msg_destroy(struct device_msg *msg); 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /app/src/event_converter.h: -------------------------------------------------------------------------------- 1 | #ifndef CONVERT_H 2 | #define CONVERT_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | 9 | #include "control_msg.h" 10 | 11 | bool 12 | convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to); 13 | 14 | enum android_metastate 15 | convert_meta_state(SDL_Keymod mod); 16 | 17 | bool 18 | convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, 19 | bool prefer_text); 20 | 21 | enum android_motionevent_buttons 22 | convert_mouse_buttons(uint32_t state); 23 | 24 | bool 25 | convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to); 26 | 27 | bool 28 | convert_touch_action(SDL_EventType from, enum android_motionevent_action *to); 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /app/src/events.h: -------------------------------------------------------------------------------- 1 | #define EVENT_NEW_FRAME SDL_USEREVENT 2 | #define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1) 3 | -------------------------------------------------------------------------------- /app/src/file_handler.h: -------------------------------------------------------------------------------- 1 | #ifndef FILE_HANDLER_H 2 | #define FILE_HANDLER_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | 8 | #include "adb.h" 9 | #include "util/cbuf.h" 10 | #include "util/thread.h" 11 | 12 | typedef enum { 13 | ACTION_INSTALL_APK, 14 | ACTION_PUSH_FILE, 15 | } file_handler_action_t; 16 | 17 | struct file_handler_request { 18 | file_handler_action_t action; 19 | char *file; 20 | }; 21 | 22 | struct file_handler_request_queue CBUF(struct file_handler_request, 16); 23 | 24 | struct file_handler { 25 | char *serial; 26 | const char *push_target; 27 | sc_thread thread; 28 | sc_mutex mutex; 29 | sc_cond event_cond; 30 | bool stopped; 31 | bool initialized; 32 | process_t current_process; 33 | struct file_handler_request_queue queue; 34 | }; 35 | 36 | bool 37 | file_handler_init(struct file_handler *file_handler, const char *serial, 38 | const char *push_target); 39 | 40 | void 41 | file_handler_destroy(struct file_handler *file_handler); 42 | 43 | bool 44 | file_handler_start(struct file_handler *file_handler); 45 | 46 | void 47 | file_handler_stop(struct file_handler *file_handler); 48 | 49 | void 50 | file_handler_join(struct file_handler *file_handler); 51 | 52 | // take ownership of file, and will free() it 53 | bool 54 | file_handler_request(struct file_handler *file_handler, 55 | file_handler_action_t action, 56 | char *file); 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /app/src/fps_counter.h: -------------------------------------------------------------------------------- 1 | #ifndef FPSCOUNTER_H 2 | #define FPSCOUNTER_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "util/thread.h" 11 | 12 | struct fps_counter { 13 | sc_thread thread; 14 | sc_mutex mutex; 15 | sc_cond state_cond; 16 | 17 | bool thread_started; 18 | 19 | // atomic so that we can check without locking the mutex 20 | // if the FPS counter is disabled, we don't want to lock unnecessarily 21 | atomic_bool started; 22 | 23 | // the following fields are protected by the mutex 24 | bool interrupted; 25 | unsigned nr_rendered; 26 | unsigned nr_skipped; 27 | sc_tick next_timestamp; 28 | }; 29 | 30 | bool 31 | fps_counter_init(struct fps_counter *counter); 32 | 33 | void 34 | fps_counter_destroy(struct fps_counter *counter); 35 | 36 | bool 37 | fps_counter_start(struct fps_counter *counter); 38 | 39 | void 40 | fps_counter_stop(struct fps_counter *counter); 41 | 42 | bool 43 | fps_counter_is_started(struct fps_counter *counter); 44 | 45 | // request to stop the thread (on quit) 46 | // must be called before fps_counter_join() 47 | void 48 | fps_counter_interrupt(struct fps_counter *counter); 49 | 50 | void 51 | fps_counter_join(struct fps_counter *counter); 52 | 53 | void 54 | fps_counter_add_rendered_frame(struct fps_counter *counter); 55 | 56 | void 57 | fps_counter_add_skipped_frame(struct fps_counter *counter); 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /app/src/frame_buffer.c: -------------------------------------------------------------------------------- 1 | #include "frame_buffer.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "util/log.h" 8 | 9 | bool 10 | sc_frame_buffer_init(struct sc_frame_buffer *fb) { 11 | fb->pending_frame = av_frame_alloc(); 12 | if (!fb->pending_frame) { 13 | return false; 14 | } 15 | 16 | fb->tmp_frame = av_frame_alloc(); 17 | if (!fb->tmp_frame) { 18 | av_frame_free(&fb->pending_frame); 19 | return false; 20 | } 21 | 22 | bool ok = sc_mutex_init(&fb->mutex); 23 | if (!ok) { 24 | av_frame_free(&fb->pending_frame); 25 | av_frame_free(&fb->tmp_frame); 26 | return false; 27 | } 28 | 29 | // there is initially no frame, so consider it has already been consumed 30 | fb->pending_frame_consumed = true; 31 | 32 | return true; 33 | } 34 | 35 | void 36 | sc_frame_buffer_destroy(struct sc_frame_buffer *fb) { 37 | sc_mutex_destroy(&fb->mutex); 38 | av_frame_free(&fb->pending_frame); 39 | av_frame_free(&fb->tmp_frame); 40 | } 41 | 42 | static inline void 43 | swap_frames(AVFrame **lhs, AVFrame **rhs) { 44 | AVFrame *tmp = *lhs; 45 | *lhs = *rhs; 46 | *rhs = tmp; 47 | } 48 | 49 | bool 50 | sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, 51 | bool *previous_frame_skipped) { 52 | sc_mutex_lock(&fb->mutex); 53 | 54 | // Use a temporary frame to preserve pending_frame in case of error. 55 | // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand. 56 | int r = av_frame_ref(fb->tmp_frame, frame); 57 | if (r) { 58 | LOGE("Could not ref frame: %d", r); 59 | return false; 60 | } 61 | 62 | // Now that av_frame_ref() succeeded, we can replace the previous 63 | // pending_frame 64 | swap_frames(&fb->pending_frame, &fb->tmp_frame); 65 | av_frame_unref(fb->tmp_frame); 66 | 67 | if (previous_frame_skipped) { 68 | *previous_frame_skipped = !fb->pending_frame_consumed; 69 | } 70 | fb->pending_frame_consumed = false; 71 | 72 | sc_mutex_unlock(&fb->mutex); 73 | 74 | return true; 75 | } 76 | 77 | void 78 | sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst) { 79 | sc_mutex_lock(&fb->mutex); 80 | assert(!fb->pending_frame_consumed); 81 | fb->pending_frame_consumed = true; 82 | 83 | av_frame_move_ref(dst, fb->pending_frame); 84 | // av_frame_move_ref() resets its source frame, so no need to call 85 | // av_frame_unref() 86 | 87 | sc_mutex_unlock(&fb->mutex); 88 | } 89 | -------------------------------------------------------------------------------- /app/src/frame_buffer.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_FRAME_BUFFER_H 2 | #define SC_FRAME_BUFFER_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | 8 | #include "util/thread.h" 9 | 10 | // forward declarations 11 | typedef struct AVFrame AVFrame; 12 | 13 | /** 14 | * A frame buffer holds 1 pending frame, which is the last frame received from 15 | * the producer (typically, the decoder). 16 | * 17 | * If a pending frame has not been consumed when the producer pushes a new 18 | * frame, then it is lost. The intent is to always provide access to the very 19 | * last frame to minimize latency. 20 | */ 21 | 22 | struct sc_frame_buffer { 23 | AVFrame *pending_frame; 24 | AVFrame *tmp_frame; // To preserve the pending frame on error 25 | 26 | sc_mutex mutex; 27 | 28 | bool pending_frame_consumed; 29 | }; 30 | 31 | bool 32 | sc_frame_buffer_init(struct sc_frame_buffer *fb); 33 | 34 | void 35 | sc_frame_buffer_destroy(struct sc_frame_buffer *fb); 36 | 37 | bool 38 | sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, 39 | bool *skipped); 40 | 41 | void 42 | sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst); 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /app/src/icon.xpm: -------------------------------------------------------------------------------- 1 | /* XPM */ 2 | static char * icon_xpm[] = { 3 | "48 48 2 1", 4 | " c None", 5 | ". c #96C13E", 6 | " .. .. ", 7 | " ... ... ", 8 | " ... ...... ... ", 9 | " ................ ", 10 | " .............. ", 11 | " ................ ", 12 | " .................. ", 13 | " .................... ", 14 | " ..... ........ ..... ", 15 | " ..... ........ ..... ", 16 | " ...................... ", 17 | " ........................ ", 18 | " ........................ ", 19 | " ........................ ", 20 | " ", 21 | " ", 22 | " .... ........................ .... ", 23 | " ...... ........................ ...... ", 24 | " ...... ........................ ...... ", 25 | " ...... ........................ ...... ", 26 | " ...... ........................ ...... ", 27 | " ...... ........................ ...... ", 28 | " ...... ........................ ...... ", 29 | " ...... ........................ ...... ", 30 | " ...... ........................ ...... ", 31 | " ...... ........................ ...... ", 32 | " ...... ........................ ...... ", 33 | " ...... ........................ ...... ", 34 | " ...... ........................ ...... ", 35 | " ...... ........................ ...... ", 36 | " ...... ........................ ...... ", 37 | " ...... ........................ ...... ", 38 | " ...... ........................ ...... ", 39 | " ...... ........................ ...... ", 40 | " ...... ........................ ...... ", 41 | " .... ........................ .... ", 42 | " ........................ ", 43 | " ...................... ", 44 | " ...... ...... ", 45 | " ...... ...... ", 46 | " ...... ...... ", 47 | " ...... ...... ", 48 | " ...... ...... ", 49 | " ...... ...... ", 50 | " ...... ...... ", 51 | " ...... ...... ", 52 | " ...... ...... ", 53 | " .... .... "}; 54 | -------------------------------------------------------------------------------- /app/src/input_manager.h: -------------------------------------------------------------------------------- 1 | #ifndef INPUTMANAGER_H 2 | #define INPUTMANAGER_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include "controller.h" 11 | #include "fps_counter.h" 12 | #include "scrcpy.h" 13 | #include "screen.h" 14 | 15 | struct input_manager { 16 | struct controller *controller; 17 | struct screen *screen; 18 | 19 | // SDL reports repeated events as a boolean, but Android expects the actual 20 | // number of repetitions. This variable keeps track of the count. 21 | unsigned repeat; 22 | 23 | bool control; 24 | bool forward_key_repeat; 25 | bool prefer_text; 26 | bool forward_all_clicks; 27 | bool legacy_paste; 28 | 29 | struct { 30 | unsigned data[SC_MAX_SHORTCUT_MODS]; 31 | unsigned count; 32 | } sdl_shortcut_mods; 33 | 34 | bool vfinger_down; 35 | 36 | // Tracks the number of identical consecutive shortcut key down events. 37 | // Not to be confused with event->repeat, which counts the number of 38 | // system-generated repeated key presses. 39 | unsigned key_repeat; 40 | SDL_Keycode last_keycode; 41 | uint16_t last_mod; 42 | }; 43 | 44 | void 45 | input_manager_init(struct input_manager *im, struct controller *controller, 46 | struct screen *screen, const struct scrcpy_options *options); 47 | 48 | bool 49 | input_manager_handle_event(struct input_manager *im, SDL_Event *event); 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /app/src/main.c: -------------------------------------------------------------------------------- 1 | #include "scrcpy.h" 2 | 3 | #include "common.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #ifdef HAVE_V4L2 10 | # include 11 | #endif 12 | #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem 13 | #include 14 | 15 | #include "cli.h" 16 | #include "util/log.h" 17 | 18 | static void 19 | print_version(void) { 20 | fprintf(stderr, "scrcpy %s\n\n", SCRCPY_VERSION); 21 | 22 | fprintf(stderr, "dependencies:\n"); 23 | fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, 24 | SDL_PATCHLEVEL); 25 | fprintf(stderr, " - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, 26 | LIBAVCODEC_VERSION_MINOR, 27 | LIBAVCODEC_VERSION_MICRO); 28 | fprintf(stderr, " - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR, 29 | LIBAVFORMAT_VERSION_MINOR, 30 | LIBAVFORMAT_VERSION_MICRO); 31 | fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, 32 | LIBAVUTIL_VERSION_MINOR, 33 | LIBAVUTIL_VERSION_MICRO); 34 | #ifdef HAVE_V4L2 35 | fprintf(stderr, " - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR, 36 | LIBAVDEVICE_VERSION_MINOR, 37 | LIBAVDEVICE_VERSION_MICRO); 38 | #endif 39 | } 40 | 41 | int 42 | main(int argc, char *argv[]) { 43 | #ifdef __WINDOWS__ 44 | // disable buffering, we want logs immediately 45 | // even line buffering (setvbuf() with mode _IOLBF) is not sufficient 46 | setbuf(stdout, NULL); 47 | setbuf(stderr, NULL); 48 | #endif 49 | 50 | struct scrcpy_cli_args args = { 51 | .opts = SCRCPY_OPTIONS_DEFAULT, 52 | .help = false, 53 | .version = false, 54 | }; 55 | 56 | #ifndef NDEBUG 57 | args.opts.log_level = SC_LOG_LEVEL_DEBUG; 58 | #endif 59 | 60 | if (!scrcpy_parse_args(&args, argc, argv)) { 61 | return 1; 62 | } 63 | 64 | sc_set_log_level(args.opts.log_level); 65 | 66 | if (args.help) { 67 | scrcpy_print_usage(argv[0]); 68 | return 0; 69 | } 70 | 71 | if (args.version) { 72 | print_version(); 73 | return 0; 74 | } 75 | 76 | LOGI("scrcpy " SCRCPY_VERSION " "); 77 | 78 | #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL 79 | av_register_all(); 80 | #endif 81 | 82 | #ifdef HAVE_V4L2 83 | if (args.opts.v4l2_device) { 84 | avdevice_register_all(); 85 | } 86 | #endif 87 | 88 | if (avformat_network_init()) { 89 | return 1; 90 | } 91 | 92 | int res = scrcpy(&args.opts) ? 0 : 1; 93 | 94 | avformat_network_deinit(); // ignore failure 95 | 96 | return res; 97 | } 98 | -------------------------------------------------------------------------------- /app/src/opengl.c: -------------------------------------------------------------------------------- 1 | #include "opengl.h" 2 | 3 | #include 4 | #include 5 | #include "SDL2/SDL.h" 6 | 7 | void 8 | sc_opengl_init(struct sc_opengl *gl) { 9 | gl->GetString = SDL_GL_GetProcAddress("glGetString"); 10 | assert(gl->GetString); 11 | 12 | gl->TexParameterf = SDL_GL_GetProcAddress("glTexParameterf"); 13 | assert(gl->TexParameterf); 14 | 15 | gl->TexParameteri = SDL_GL_GetProcAddress("glTexParameteri"); 16 | assert(gl->TexParameteri); 17 | 18 | // optional 19 | gl->GenerateMipmap = SDL_GL_GetProcAddress("glGenerateMipmap"); 20 | 21 | const char *version = (const char *) gl->GetString(GL_VERSION); 22 | assert(version); 23 | gl->version = version; 24 | 25 | #define OPENGL_ES_PREFIX "OpenGL ES " 26 | /* starts with "OpenGL ES " */ 27 | gl->is_opengles = !strncmp(gl->version, OPENGL_ES_PREFIX, 28 | sizeof(OPENGL_ES_PREFIX) - 1); 29 | if (gl->is_opengles) { 30 | /* skip the prefix */ 31 | version += sizeof(PREFIX) - 1; 32 | } 33 | 34 | int r = sscanf(version, "%d.%d", &gl->version_major, &gl->version_minor); 35 | if (r != 2) { 36 | // failed to parse the version 37 | gl->version_major = 0; 38 | gl->version_minor = 0; 39 | } 40 | } 41 | 42 | bool 43 | sc_opengl_version_at_least(struct sc_opengl *gl, 44 | int minver_major, int minver_minor, 45 | int minver_es_major, int minver_es_minor) 46 | { 47 | if (gl->is_opengles) { 48 | return gl->version_major > minver_es_major 49 | || (gl->version_major == minver_es_major 50 | && gl->version_minor >= minver_es_minor); 51 | } 52 | 53 | return gl->version_major > minver_major 54 | || (gl->version_major == minver_major 55 | && gl->version_minor >= minver_minor); 56 | } 57 | -------------------------------------------------------------------------------- /app/src/opengl.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_OPENGL_H 2 | #define SC_OPENGL_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | 9 | struct sc_opengl { 10 | const char *version; 11 | bool is_opengles; 12 | int version_major; 13 | int version_minor; 14 | 15 | const GLubyte * 16 | (*GetString)(GLenum name); 17 | 18 | void 19 | (*TexParameterf)(GLenum target, GLenum pname, GLfloat param); 20 | 21 | void 22 | (*TexParameteri)(GLenum target, GLenum pname, GLint param); 23 | 24 | void 25 | (*GenerateMipmap)(GLenum target); 26 | }; 27 | 28 | void 29 | sc_opengl_init(struct sc_opengl *gl); 30 | 31 | bool 32 | sc_opengl_version_at_least(struct sc_opengl *gl, 33 | int minver_major, int minver_minor, 34 | int minver_es_major, int minver_es_minor); 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /app/src/receiver.c: -------------------------------------------------------------------------------- 1 | #include "receiver.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "device_msg.h" 7 | #include "util/log.h" 8 | 9 | bool 10 | receiver_init(struct receiver *receiver, socket_t control_socket) { 11 | bool ok = sc_mutex_init(&receiver->mutex); 12 | if (!ok) { 13 | return false; 14 | } 15 | receiver->control_socket = control_socket; 16 | return true; 17 | } 18 | 19 | void 20 | receiver_destroy(struct receiver *receiver) { 21 | sc_mutex_destroy(&receiver->mutex); 22 | } 23 | 24 | static void 25 | process_msg(struct device_msg *msg) { 26 | switch (msg->type) { 27 | case DEVICE_MSG_TYPE_CLIPBOARD: { 28 | char *current = SDL_GetClipboardText(); 29 | bool same = current && !strcmp(current, msg->clipboard.text); 30 | SDL_free(current); 31 | if (same) { 32 | LOGD("Computer clipboard unchanged"); 33 | return; 34 | } 35 | 36 | LOGI("Device clipboard copied"); 37 | SDL_SetClipboardText(msg->clipboard.text); 38 | break; 39 | } 40 | } 41 | } 42 | 43 | static ssize_t 44 | process_msgs(const unsigned char *buf, size_t len) { 45 | size_t head = 0; 46 | for (;;) { 47 | struct device_msg msg; 48 | ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg); 49 | if (r == -1) { 50 | return -1; 51 | } 52 | if (r == 0) { 53 | return head; 54 | } 55 | 56 | process_msg(&msg); 57 | device_msg_destroy(&msg); 58 | 59 | head += r; 60 | assert(head <= len); 61 | if (head == len) { 62 | return head; 63 | } 64 | } 65 | } 66 | 67 | static int 68 | run_receiver(void *data) { 69 | struct receiver *receiver = data; 70 | 71 | static unsigned char buf[DEVICE_MSG_MAX_SIZE]; 72 | size_t head = 0; 73 | 74 | for (;;) { 75 | assert(head < DEVICE_MSG_MAX_SIZE); 76 | ssize_t r = net_recv(receiver->control_socket, buf + head, 77 | DEVICE_MSG_MAX_SIZE - head); 78 | if (r <= 0) { 79 | LOGD("Receiver stopped"); 80 | break; 81 | } 82 | 83 | head += r; 84 | ssize_t consumed = process_msgs(buf, head); 85 | if (consumed == -1) { 86 | // an error occurred 87 | break; 88 | } 89 | 90 | if (consumed) { 91 | head -= consumed; 92 | // shift the remaining data in the buffer 93 | memmove(buf, &buf[consumed], head); 94 | } 95 | } 96 | 97 | return 0; 98 | } 99 | 100 | bool 101 | receiver_start(struct receiver *receiver) { 102 | LOGD("Starting receiver thread"); 103 | 104 | bool ok = sc_thread_create(&receiver->thread, run_receiver, "receiver", 105 | receiver); 106 | if (!ok) { 107 | LOGC("Could not start receiver thread"); 108 | return false; 109 | } 110 | 111 | return true; 112 | } 113 | 114 | void 115 | receiver_join(struct receiver *receiver) { 116 | sc_thread_join(&receiver->thread, NULL); 117 | } 118 | -------------------------------------------------------------------------------- /app/src/receiver.h: -------------------------------------------------------------------------------- 1 | #ifndef RECEIVER_H 2 | #define RECEIVER_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | 8 | #include "util/net.h" 9 | #include "util/thread.h" 10 | 11 | // receive events from the device 12 | // managed by the controller 13 | struct receiver { 14 | socket_t control_socket; 15 | sc_thread thread; 16 | sc_mutex mutex; 17 | }; 18 | 19 | bool 20 | receiver_init(struct receiver *receiver, socket_t control_socket); 21 | 22 | void 23 | receiver_destroy(struct receiver *receiver); 24 | 25 | bool 26 | receiver_start(struct receiver *receiver); 27 | 28 | // no receiver_stop(), it will automatically stop on control_socket shutdown 29 | 30 | void 31 | receiver_join(struct receiver *receiver); 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /app/src/recorder.h: -------------------------------------------------------------------------------- 1 | #ifndef RECORDER_H 2 | #define RECORDER_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | 9 | #include "coords.h" 10 | #include "scrcpy.h" 11 | #include "trait/packet_sink.h" 12 | #include "util/queue.h" 13 | #include "util/thread.h" 14 | 15 | struct record_packet { 16 | AVPacket *packet; 17 | struct record_packet *next; 18 | }; 19 | 20 | struct recorder_queue SC_QUEUE(struct record_packet); 21 | 22 | struct recorder { 23 | struct sc_packet_sink packet_sink; // packet sink trait 24 | 25 | char *filename; 26 | enum sc_record_format format; 27 | AVFormatContext *ctx; 28 | struct size declared_frame_size; 29 | bool header_written; 30 | 31 | sc_thread thread; 32 | sc_mutex mutex; 33 | sc_cond queue_cond; 34 | bool stopped; // set on recorder_close() 35 | bool failed; // set on packet write failure 36 | struct recorder_queue queue; 37 | 38 | // we can write a packet only once we received the next one so that we can 39 | // set its duration (next_pts - current_pts) 40 | // "previous" is only accessed from the recorder thread, so it does not 41 | // need to be protected by the mutex 42 | struct record_packet *previous; 43 | }; 44 | 45 | bool 46 | recorder_init(struct recorder *recorder, const char *filename, 47 | enum sc_record_format format, struct size declared_frame_size); 48 | 49 | void 50 | recorder_destroy(struct recorder *recorder); 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /app/src/scrcpy.h: -------------------------------------------------------------------------------- 1 | #ifndef SCRCPY_H 2 | #define SCRCPY_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "util/tick.h" 11 | 12 | enum sc_log_level { 13 | SC_LOG_LEVEL_VERBOSE, 14 | SC_LOG_LEVEL_DEBUG, 15 | SC_LOG_LEVEL_INFO, 16 | SC_LOG_LEVEL_WARN, 17 | SC_LOG_LEVEL_ERROR, 18 | }; 19 | 20 | enum sc_record_format { 21 | SC_RECORD_FORMAT_AUTO, 22 | SC_RECORD_FORMAT_MP4, 23 | SC_RECORD_FORMAT_MKV, 24 | }; 25 | 26 | enum sc_lock_video_orientation { 27 | SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, 28 | // lock the current orientation when scrcpy starts 29 | SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, 30 | SC_LOCK_VIDEO_ORIENTATION_0 = 0, 31 | SC_LOCK_VIDEO_ORIENTATION_1, 32 | SC_LOCK_VIDEO_ORIENTATION_2, 33 | SC_LOCK_VIDEO_ORIENTATION_3, 34 | }; 35 | 36 | #define SC_MAX_SHORTCUT_MODS 8 37 | 38 | enum sc_shortcut_mod { 39 | SC_MOD_LCTRL = 1 << 0, 40 | SC_MOD_RCTRL = 1 << 1, 41 | SC_MOD_LALT = 1 << 2, 42 | SC_MOD_RALT = 1 << 3, 43 | SC_MOD_LSUPER = 1 << 4, 44 | SC_MOD_RSUPER = 1 << 5, 45 | }; 46 | 47 | struct sc_shortcut_mods { 48 | unsigned data[SC_MAX_SHORTCUT_MODS]; 49 | unsigned count; 50 | }; 51 | 52 | struct sc_port_range { 53 | uint16_t first; 54 | uint16_t last; 55 | }; 56 | 57 | #define SC_WINDOW_POSITION_UNDEFINED (-0x8000) 58 | 59 | struct scrcpy_options { 60 | const char *serial; 61 | const char *crop; 62 | const char *record_filename; 63 | const char *window_title; 64 | const char *push_target; 65 | const char *render_driver; 66 | const char *codec_options; 67 | const char *encoder_name; 68 | const char *v4l2_device; 69 | enum sc_log_level log_level; 70 | enum sc_record_format record_format; 71 | struct sc_port_range port_range; 72 | struct sc_shortcut_mods shortcut_mods; 73 | uint16_t max_size; 74 | uint32_t bit_rate; 75 | uint16_t max_fps; 76 | enum sc_lock_video_orientation lock_video_orientation; 77 | uint8_t rotation; 78 | int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" 79 | int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" 80 | uint16_t window_width; 81 | uint16_t window_height; 82 | uint32_t display_id; 83 | sc_tick display_buffer; 84 | sc_tick v4l2_buffer; 85 | bool show_touches; 86 | bool fullscreen; 87 | bool always_on_top; 88 | bool control; 89 | bool display; 90 | bool turn_screen_off; 91 | bool prefer_text; 92 | bool window_borderless; 93 | bool mipmaps; 94 | bool stay_awake; 95 | bool force_adb_forward; 96 | bool disable_screensaver; 97 | bool forward_key_repeat; 98 | bool forward_all_clicks; 99 | bool legacy_paste; 100 | bool power_off_on_close; 101 | }; 102 | 103 | #define SCRCPY_OPTIONS_DEFAULT { \ 104 | .serial = NULL, \ 105 | .crop = NULL, \ 106 | .record_filename = NULL, \ 107 | .window_title = NULL, \ 108 | .push_target = NULL, \ 109 | .render_driver = NULL, \ 110 | .codec_options = NULL, \ 111 | .encoder_name = NULL, \ 112 | .v4l2_device = NULL, \ 113 | .log_level = SC_LOG_LEVEL_INFO, \ 114 | .record_format = SC_RECORD_FORMAT_AUTO, \ 115 | .port_range = { \ 116 | .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ 117 | .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ 118 | }, \ 119 | .shortcut_mods = { \ 120 | .data = {SC_MOD_LALT, SC_MOD_LSUPER}, \ 121 | .count = 2, \ 122 | }, \ 123 | .max_size = 0, \ 124 | .bit_rate = DEFAULT_BIT_RATE, \ 125 | .max_fps = 0, \ 126 | .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \ 127 | .rotation = 0, \ 128 | .window_x = SC_WINDOW_POSITION_UNDEFINED, \ 129 | .window_y = SC_WINDOW_POSITION_UNDEFINED, \ 130 | .window_width = 0, \ 131 | .window_height = 0, \ 132 | .display_id = 0, \ 133 | .display_buffer = 0, \ 134 | .v4l2_buffer = 0, \ 135 | .show_touches = false, \ 136 | .fullscreen = false, \ 137 | .always_on_top = false, \ 138 | .control = true, \ 139 | .display = true, \ 140 | .turn_screen_off = false, \ 141 | .prefer_text = false, \ 142 | .window_borderless = false, \ 143 | .mipmaps = true, \ 144 | .stay_awake = false, \ 145 | .force_adb_forward = false, \ 146 | .disable_screensaver = false, \ 147 | .forward_key_repeat = true, \ 148 | .forward_all_clicks = false, \ 149 | .legacy_paste = false, \ 150 | .power_off_on_close = false, \ 151 | } 152 | 153 | bool 154 | scrcpy(const struct scrcpy_options *options); 155 | 156 | #endif 157 | -------------------------------------------------------------------------------- /app/src/screen.h: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_H 2 | #define SCREEN_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "coords.h" 11 | #include "fps_counter.h" 12 | #include "opengl.h" 13 | #include "trait/frame_sink.h" 14 | #include "video_buffer.h" 15 | 16 | struct screen { 17 | struct sc_frame_sink frame_sink; // frame sink trait 18 | 19 | #ifndef NDEBUG 20 | bool open; // track the open/close state to assert correct behavior 21 | #endif 22 | 23 | struct sc_video_buffer vb; 24 | struct fps_counter fps_counter; 25 | 26 | SDL_Window *window; 27 | SDL_Renderer *renderer; 28 | SDL_Texture *texture; 29 | struct sc_opengl gl; 30 | struct size frame_size; 31 | struct size content_size; // rotated frame_size 32 | 33 | bool resize_pending; // resize requested while fullscreen or maximized 34 | // The content size the last time the window was not maximized or 35 | // fullscreen (meaningful only when resize_pending is true) 36 | struct size windowed_content_size; 37 | 38 | // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) 39 | unsigned rotation; 40 | // rectangle of the content (excluding black borders) 41 | struct SDL_Rect rect; 42 | bool has_frame; 43 | bool fullscreen; 44 | bool maximized; 45 | bool mipmaps; 46 | 47 | AVFrame *frame; 48 | }; 49 | 50 | struct screen_params { 51 | const char *window_title; 52 | struct size frame_size; 53 | bool always_on_top; 54 | 55 | int16_t window_x; 56 | int16_t window_y; 57 | uint16_t window_width; // accepts SC_WINDOW_POSITION_UNDEFINED 58 | uint16_t window_height; // accepts SC_WINDOW_POSITION_UNDEFINED 59 | 60 | bool window_borderless; 61 | 62 | uint8_t rotation; 63 | bool mipmaps; 64 | 65 | bool fullscreen; 66 | 67 | sc_tick buffering_time; 68 | }; 69 | 70 | // initialize screen, create window, renderer and texture (window is hidden) 71 | bool 72 | screen_init(struct screen *screen, const struct screen_params *params); 73 | 74 | // request to interrupt any inner thread 75 | // must be called before screen_join() 76 | void 77 | screen_interrupt(struct screen *screen); 78 | 79 | // join any inner thread 80 | void 81 | screen_join(struct screen *screen); 82 | 83 | // destroy window, renderer and texture (if any) 84 | void 85 | screen_destroy(struct screen *screen); 86 | 87 | // hide the window 88 | // 89 | // It is used to hide the window immediately on closing without waiting for 90 | // screen_destroy() 91 | void 92 | screen_hide_window(struct screen *screen); 93 | 94 | // render the texture to the renderer 95 | // 96 | // Set the update_content_rect flag if the window or content size may have 97 | // changed, so that the content rectangle is recomputed 98 | void 99 | screen_render(struct screen *screen, bool update_content_rect); 100 | 101 | // switch the fullscreen mode 102 | void 103 | screen_switch_fullscreen(struct screen *screen); 104 | 105 | // resize window to optimal size (remove black borders) 106 | void 107 | screen_resize_to_fit(struct screen *screen); 108 | 109 | // resize window to 1:1 (pixel-perfect) 110 | void 111 | screen_resize_to_pixel_perfect(struct screen *screen); 112 | 113 | // set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) 114 | void 115 | screen_set_rotation(struct screen *screen, unsigned rotation); 116 | 117 | // react to SDL events 118 | bool 119 | screen_handle_event(struct screen *screen, SDL_Event *event); 120 | 121 | // convert point from window coordinates to frame coordinates 122 | // x and y are expressed in pixels 123 | struct point 124 | screen_convert_window_to_frame_coords(struct screen *screen, 125 | int32_t x, int32_t y); 126 | 127 | // convert point from drawable coordinates to frame coordinates 128 | // x and y are expressed in pixels 129 | struct point 130 | screen_convert_drawable_to_frame_coords(struct screen *screen, 131 | int32_t x, int32_t y); 132 | 133 | // Convert coordinates from window to drawable. 134 | // Events are expressed in window coordinates, but content is expressed in 135 | // drawable coordinates. They are the same if HiDPI scaling is 1, but differ 136 | // otherwise. 137 | void 138 | screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y); 139 | 140 | #endif 141 | -------------------------------------------------------------------------------- /app/src/server.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVER_H 2 | #define SERVER_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "adb.h" 11 | #include "coords.h" 12 | #include "scrcpy.h" 13 | #include "util/log.h" 14 | #include "util/net.h" 15 | #include "util/thread.h" 16 | 17 | struct server { 18 | char *serial; 19 | process_t process; 20 | sc_thread wait_server_thread; 21 | atomic_flag server_socket_closed; 22 | 23 | sc_mutex mutex; 24 | sc_cond process_terminated_cond; 25 | bool process_terminated; 26 | 27 | socket_t server_socket; // only used if !tunnel_forward 28 | socket_t video_socket; 29 | socket_t control_socket; 30 | uint16_t local_port; // selected from port_range 31 | bool tunnel_enabled; 32 | bool tunnel_forward; // use "adb forward" instead of "adb reverse" 33 | }; 34 | 35 | struct server_params { 36 | const char *serial; 37 | enum sc_log_level log_level; 38 | const char *crop; 39 | const char *codec_options; 40 | const char *encoder_name; 41 | struct sc_port_range port_range; 42 | uint16_t max_size; 43 | uint32_t bit_rate; 44 | uint16_t max_fps; 45 | int8_t lock_video_orientation; 46 | bool control; 47 | uint32_t display_id; 48 | bool show_touches; 49 | bool stay_awake; 50 | bool force_adb_forward; 51 | bool power_off_on_close; 52 | }; 53 | 54 | // init default values 55 | bool 56 | server_init(struct server *server); 57 | 58 | // push, enable tunnel et start the server 59 | bool 60 | server_start(struct server *server, const struct server_params *params); 61 | 62 | #define DEVICE_NAME_FIELD_LENGTH 64 63 | // block until the communication with the server is established 64 | // device_name must point to a buffer of at least DEVICE_NAME_FIELD_LENGTH bytes 65 | bool 66 | server_connect_to(struct server *server, char *device_name, struct size *size); 67 | 68 | // disconnect and kill the server process 69 | void 70 | server_stop(struct server *server); 71 | 72 | // close and release sockets 73 | void 74 | server_destroy(struct server *server); 75 | 76 | #endif 77 | -------------------------------------------------------------------------------- /app/src/stream.h: -------------------------------------------------------------------------------- 1 | #ifndef STREAM_H 2 | #define STREAM_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "trait/packet_sink.h" 11 | #include "util/net.h" 12 | #include "util/thread.h" 13 | 14 | #define STREAM_MAX_SINKS 2 15 | 16 | struct stream { 17 | socket_t socket; 18 | sc_thread thread; 19 | 20 | struct sc_packet_sink *sinks[STREAM_MAX_SINKS]; 21 | unsigned sink_count; 22 | 23 | AVCodecContext *codec_ctx; 24 | AVCodecParserContext *parser; 25 | // successive packets may need to be concatenated, until a non-config 26 | // packet is available 27 | AVPacket *pending; 28 | 29 | const struct stream_callbacks *cbs; 30 | void *cbs_userdata; 31 | }; 32 | 33 | struct stream_callbacks { 34 | void (*on_eos)(struct stream *stream, void *userdata); 35 | }; 36 | 37 | void 38 | stream_init(struct stream *stream, socket_t socket, 39 | const struct stream_callbacks *cbs, void *cbs_userdata); 40 | 41 | void 42 | stream_add_sink(struct stream *stream, struct sc_packet_sink *sink); 43 | 44 | bool 45 | stream_start(struct stream *stream); 46 | 47 | void 48 | stream_join(struct stream *stream); 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /app/src/sys/unix/process.c: -------------------------------------------------------------------------------- 1 | #include "util/process.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "util/log.h" 15 | 16 | bool 17 | search_executable(const char *file) { 18 | char *path = getenv("PATH"); 19 | if (!path) 20 | return false; 21 | path = strdup(path); 22 | if (!path) 23 | return false; 24 | 25 | bool ret = false; 26 | size_t file_len = strlen(file); 27 | char *saveptr; 28 | for (char *dir = strtok_r(path, ":", &saveptr); dir; 29 | dir = strtok_r(NULL, ":", &saveptr)) { 30 | size_t dir_len = strlen(dir); 31 | char *fullpath = malloc(dir_len + file_len + 2); 32 | if (!fullpath) 33 | continue; 34 | memcpy(fullpath, dir, dir_len); 35 | fullpath[dir_len] = '/'; 36 | memcpy(fullpath + dir_len + 1, file, file_len + 1); 37 | 38 | struct stat sb; 39 | bool fullpath_executable = stat(fullpath, &sb) == 0 && 40 | sb.st_mode & S_IXUSR; 41 | free(fullpath); 42 | if (fullpath_executable) { 43 | ret = true; 44 | break; 45 | } 46 | } 47 | 48 | free(path); 49 | return ret; 50 | } 51 | 52 | enum process_result 53 | process_execute(const char *const argv[], pid_t *pid) { 54 | int fd[2]; 55 | 56 | if (pipe(fd) == -1) { 57 | perror("pipe"); 58 | return PROCESS_ERROR_GENERIC; 59 | } 60 | 61 | enum process_result ret = PROCESS_SUCCESS; 62 | 63 | *pid = fork(); 64 | if (*pid == -1) { 65 | perror("fork"); 66 | ret = PROCESS_ERROR_GENERIC; 67 | goto end; 68 | } 69 | 70 | if (*pid > 0) { 71 | // parent close write side 72 | close(fd[1]); 73 | fd[1] = -1; 74 | // wait for EOF or receive errno from child 75 | if (read(fd[0], &ret, sizeof(ret)) == -1) { 76 | perror("read"); 77 | ret = PROCESS_ERROR_GENERIC; 78 | goto end; 79 | } 80 | } else if (*pid == 0) { 81 | // child close read side 82 | close(fd[0]); 83 | if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) { 84 | execvp(argv[0], (char *const *)argv); 85 | if (errno == ENOENT) { 86 | ret = PROCESS_ERROR_MISSING_BINARY; 87 | } else { 88 | ret = PROCESS_ERROR_GENERIC; 89 | } 90 | perror("exec"); 91 | } else { 92 | perror("fcntl"); 93 | ret = PROCESS_ERROR_GENERIC; 94 | } 95 | // send ret to the parent 96 | if (write(fd[1], &ret, sizeof(ret)) == -1) { 97 | perror("write"); 98 | } 99 | // close write side before exiting 100 | close(fd[1]); 101 | _exit(1); 102 | } 103 | 104 | end: 105 | if (fd[0] != -1) { 106 | close(fd[0]); 107 | } 108 | if (fd[1] != -1) { 109 | close(fd[1]); 110 | } 111 | return ret; 112 | } 113 | 114 | bool 115 | process_terminate(pid_t pid) { 116 | if (pid <= 0) { 117 | LOGC("Requested to kill %d, this is an error. Please report the bug.\n", 118 | (int) pid); 119 | abort(); 120 | } 121 | return kill(pid, SIGKILL) != -1; 122 | } 123 | 124 | exit_code_t 125 | process_wait(pid_t pid, bool close) { 126 | int code; 127 | int options = WEXITED; 128 | if (!close) { 129 | options |= WNOWAIT; 130 | } 131 | 132 | siginfo_t info; 133 | int r = waitid(P_PID, pid, &info, options); 134 | if (r == -1 || info.si_code != CLD_EXITED) { 135 | // could not wait, or exited unexpectedly, probably by a signal 136 | code = NO_EXIT_CODE; 137 | } else { 138 | code = info.si_status; 139 | } 140 | return code; 141 | } 142 | 143 | void 144 | process_close(pid_t pid) { 145 | process_wait(pid, true); // ignore exit code 146 | } 147 | 148 | char * 149 | get_executable_path(void) { 150 | // 151 | #ifdef __linux__ 152 | char buf[PATH_MAX + 1]; // +1 for the null byte 153 | ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX); 154 | if (len == -1) { 155 | perror("readlink"); 156 | return NULL; 157 | } 158 | buf[len] = '\0'; 159 | return strdup(buf); 160 | #else 161 | // in practice, we only need this feature for portable builds, only used on 162 | // Windows, so we don't care implementing it for every platform 163 | // (it's useful to have a working version on Linux for debugging though) 164 | return NULL; 165 | #endif 166 | } 167 | 168 | bool 169 | is_regular_file(const char *path) { 170 | struct stat path_stat; 171 | 172 | if (stat(path, &path_stat)) { 173 | perror("stat"); 174 | return false; 175 | } 176 | return S_ISREG(path_stat.st_mode); 177 | } 178 | -------------------------------------------------------------------------------- /app/src/sys/win/process.c: -------------------------------------------------------------------------------- 1 | #include "util/process.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "util/log.h" 7 | #include "util/str_util.h" 8 | 9 | #define CMD_MAX_LEN 8192 10 | 11 | static bool 12 | build_cmd(char *cmd, size_t len, const char *const argv[]) { 13 | // Windows command-line parsing is WTF: 14 | // 15 | // only make it work for this very specific program 16 | // (don't handle escaping nor quotes) 17 | size_t ret = xstrjoin(cmd, argv, ' ', len); 18 | if (ret >= len) { 19 | LOGE("Command too long (%" PRIsizet " chars)", len - 1); 20 | return false; 21 | } 22 | return true; 23 | } 24 | 25 | enum process_result 26 | process_execute(const char *const argv[], HANDLE *handle) { 27 | STARTUPINFOW si; 28 | PROCESS_INFORMATION pi; 29 | memset(&si, 0, sizeof(si)); 30 | si.cb = sizeof(si); 31 | 32 | char *cmd = malloc(CMD_MAX_LEN); 33 | if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) { 34 | *handle = NULL; 35 | return PROCESS_ERROR_GENERIC; 36 | } 37 | 38 | wchar_t *wide = utf8_to_wide_char(cmd); 39 | free(cmd); 40 | if (!wide) { 41 | LOGC("Could not allocate wide char string"); 42 | return PROCESS_ERROR_GENERIC; 43 | } 44 | 45 | if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, 0, NULL, NULL, &si, 46 | &pi)) { 47 | free(wide); 48 | *handle = NULL; 49 | if (GetLastError() == ERROR_FILE_NOT_FOUND) { 50 | return PROCESS_ERROR_MISSING_BINARY; 51 | } 52 | return PROCESS_ERROR_GENERIC; 53 | } 54 | 55 | free(wide); 56 | *handle = pi.hProcess; 57 | return PROCESS_SUCCESS; 58 | } 59 | 60 | bool 61 | process_terminate(HANDLE handle) { 62 | return TerminateProcess(handle, 1); 63 | } 64 | 65 | exit_code_t 66 | process_wait(HANDLE handle, bool close) { 67 | DWORD code; 68 | if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 69 | || !GetExitCodeProcess(handle, &code)) { 70 | // could not wait or retrieve the exit code 71 | code = NO_EXIT_CODE; // max value, it's unsigned 72 | } 73 | if (close) { 74 | CloseHandle(handle); 75 | } 76 | return code; 77 | } 78 | 79 | void 80 | process_close(HANDLE handle) { 81 | bool closed = CloseHandle(handle); 82 | assert(closed); 83 | (void) closed; 84 | } 85 | 86 | char * 87 | get_executable_path(void) { 88 | HMODULE hModule = GetModuleHandleW(NULL); 89 | if (!hModule) { 90 | return NULL; 91 | } 92 | WCHAR buf[MAX_PATH + 1]; // +1 for the null byte 93 | int len = GetModuleFileNameW(hModule, buf, MAX_PATH); 94 | if (!len) { 95 | return NULL; 96 | } 97 | buf[len] = '\0'; 98 | return utf8_from_wide_char(buf); 99 | } 100 | 101 | bool 102 | is_regular_file(const char *path) { 103 | wchar_t *wide_path = utf8_to_wide_char(path); 104 | if (!wide_path) { 105 | LOGC("Could not allocate wide char string"); 106 | return false; 107 | } 108 | 109 | struct _stat path_stat; 110 | int r = _wstat(wide_path, &path_stat); 111 | free(wide_path); 112 | 113 | if (r) { 114 | perror("stat"); 115 | return false; 116 | } 117 | return S_ISREG(path_stat.st_mode); 118 | } 119 | -------------------------------------------------------------------------------- /app/src/tiny_xpm.c: -------------------------------------------------------------------------------- 1 | #include "tiny_xpm.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "util/log.h" 10 | 11 | struct index { 12 | char c; 13 | uint32_t color; 14 | }; 15 | 16 | static bool 17 | find_color(struct index *index, int len, char c, uint32_t *color) { 18 | // there are typically very few color, so it's ok to iterate over the array 19 | for (int i = 0; i < len; ++i) { 20 | if (index[i].c == c) { 21 | *color = index[i].color; 22 | return true; 23 | } 24 | } 25 | *color = 0; 26 | return false; 27 | } 28 | 29 | // We encounter some problems with SDL2_image on MSYS2 (Windows), 30 | // so here is our own XPM parsing not to depend on SDL_image. 31 | // 32 | // We do not hardcode the binary image to keep some flexibility to replace the 33 | // icon easily (just by replacing icon.xpm). 34 | // 35 | // Parameter is not "const char *" because XPM formats are generally stored in a 36 | // (non-const) "char *" 37 | SDL_Surface * 38 | read_xpm(char *xpm[]) { 39 | #ifndef NDEBUG 40 | // patch the XPM to change the icon color in debug mode 41 | xpm[2] = ". c #CC00CC"; 42 | #endif 43 | 44 | char *endptr; 45 | // *** No error handling, assume the XPM source is valid *** 46 | // (it's in our source repo) 47 | // Assertions are only checked in debug 48 | int width = strtol(xpm[0], &endptr, 10); 49 | int height = strtol(endptr + 1, &endptr, 10); 50 | int colors = strtol(endptr + 1, &endptr, 10); 51 | int chars = strtol(endptr + 1, &endptr, 10); 52 | 53 | // sanity checks 54 | assert(0 <= width && width < 256); 55 | assert(0 <= height && height < 256); 56 | assert(0 <= colors && colors < 256); 57 | assert(chars == 1); // this implementation does not support more 58 | 59 | (void) chars; 60 | 61 | // init index 62 | struct index index[colors]; 63 | for (int i = 0; i < colors; ++i) { 64 | const char *line = xpm[1+i]; 65 | index[i].c = line[0]; 66 | assert(line[1] == '\t'); 67 | assert(line[2] == 'c'); 68 | assert(line[3] == ' '); 69 | if (line[4] == '#') { 70 | index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10); 71 | assert(*endptr == '\0'); 72 | } else { 73 | assert(!strcmp("None", &line[4])); 74 | index[i].color = 0; 75 | } 76 | } 77 | 78 | // parse image 79 | uint32_t *pixels = SDL_malloc(4 * width * height); 80 | if (!pixels) { 81 | LOGE("Could not allocate icon memory"); 82 | return NULL; 83 | } 84 | for (int y = 0; y < height; ++y) { 85 | const char *line = xpm[1 + colors + y]; 86 | for (int x = 0; x < width; ++x) { 87 | char c = line[x]; 88 | uint32_t color; 89 | bool color_found = find_color(index, colors, c, &color); 90 | assert(color_found); 91 | (void) color_found; 92 | pixels[y * width + x] = color; 93 | } 94 | } 95 | 96 | #if SDL_BYTEORDER == SDL_BIG_ENDIAN 97 | uint32_t amask = 0x000000ff; 98 | uint32_t rmask = 0x0000ff00; 99 | uint32_t gmask = 0x00ff0000; 100 | uint32_t bmask = 0xff000000; 101 | #else // little endian, like x86 102 | uint32_t amask = 0xff000000; 103 | uint32_t rmask = 0x00ff0000; 104 | uint32_t gmask = 0x0000ff00; 105 | uint32_t bmask = 0x000000ff; 106 | #endif 107 | 108 | SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels, 109 | width, height, 110 | 32, 4 * width, 111 | rmask, gmask, bmask, amask); 112 | if (!surface) { 113 | LOGE("Could not create icon surface"); 114 | return NULL; 115 | } 116 | // make the surface own the raw pixels 117 | surface->flags &= ~SDL_PREALLOC; 118 | return surface; 119 | } 120 | -------------------------------------------------------------------------------- /app/src/tiny_xpm.h: -------------------------------------------------------------------------------- 1 | #ifndef TINYXPM_H 2 | #define TINYXPM_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | 8 | SDL_Surface * 9 | read_xpm(char *xpm[]); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /app/src/trait/frame_sink.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_FRAME_SINK 2 | #define SC_FRAME_SINK 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | 9 | typedef struct AVFrame AVFrame; 10 | 11 | /** 12 | * Frame sink trait. 13 | * 14 | * Component able to receive AVFrames should implement this trait. 15 | */ 16 | struct sc_frame_sink { 17 | const struct sc_frame_sink_ops *ops; 18 | }; 19 | 20 | struct sc_frame_sink_ops { 21 | bool (*open)(struct sc_frame_sink *sink); 22 | void (*close)(struct sc_frame_sink *sink); 23 | bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame); 24 | }; 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /app/src/trait/packet_sink.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_PACKET_SINK 2 | #define SC_PACKET_SINK 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | 9 | typedef struct AVCodec AVCodec; 10 | typedef struct AVPacket AVPacket; 11 | 12 | /** 13 | * Packet sink trait. 14 | * 15 | * Component able to receive AVPackets should implement this trait. 16 | */ 17 | struct sc_packet_sink { 18 | const struct sc_packet_sink_ops *ops; 19 | }; 20 | 21 | struct sc_packet_sink_ops { 22 | bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec); 23 | void (*close)(struct sc_packet_sink *sink); 24 | bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet); 25 | }; 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /app/src/util/buffer_util.h: -------------------------------------------------------------------------------- 1 | #ifndef BUFFER_UTIL_H 2 | #define BUFFER_UTIL_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | 9 | static inline void 10 | buffer_write16be(uint8_t *buf, uint16_t value) { 11 | buf[0] = value >> 8; 12 | buf[1] = value; 13 | } 14 | 15 | static inline void 16 | buffer_write32be(uint8_t *buf, uint32_t value) { 17 | buf[0] = value >> 24; 18 | buf[1] = value >> 16; 19 | buf[2] = value >> 8; 20 | buf[3] = value; 21 | } 22 | 23 | static inline void 24 | buffer_write64be(uint8_t *buf, uint64_t value) { 25 | buffer_write32be(buf, value >> 32); 26 | buffer_write32be(&buf[4], (uint32_t) value); 27 | } 28 | 29 | static inline uint16_t 30 | buffer_read16be(const uint8_t *buf) { 31 | return (buf[0] << 8) | buf[1]; 32 | } 33 | 34 | static inline uint32_t 35 | buffer_read32be(const uint8_t *buf) { 36 | return ((uint32_t) buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; 37 | } 38 | 39 | static inline uint64_t 40 | buffer_read64be(const uint8_t *buf) { 41 | uint32_t msb = buffer_read32be(buf); 42 | uint32_t lsb = buffer_read32be(&buf[4]); 43 | return ((uint64_t) msb << 32) | lsb; 44 | } 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /app/src/util/cbuf.h: -------------------------------------------------------------------------------- 1 | // generic circular buffer (bounded queue) implementation 2 | #ifndef CBUF_H 3 | #define CBUF_H 4 | 5 | #include "common.h" 6 | 7 | #include 8 | #include 9 | 10 | // To define a circular buffer type of 20 ints: 11 | // struct cbuf_int CBUF(int, 20); 12 | // 13 | // data has length CAP + 1 to distinguish empty vs full. 14 | #define CBUF(TYPE, CAP) { \ 15 | TYPE data[(CAP) + 1]; \ 16 | size_t head; \ 17 | size_t tail; \ 18 | } 19 | 20 | #define cbuf_size_(PCBUF) \ 21 | (sizeof((PCBUF)->data) / sizeof(*(PCBUF)->data)) 22 | 23 | #define cbuf_is_empty(PCBUF) \ 24 | ((PCBUF)->head == (PCBUF)->tail) 25 | 26 | #define cbuf_is_full(PCBUF) \ 27 | (((PCBUF)->head + 1) % cbuf_size_(PCBUF) == (PCBUF)->tail) 28 | 29 | #define cbuf_init(PCBUF) \ 30 | (void) ((PCBUF)->head = (PCBUF)->tail = 0) 31 | 32 | #define cbuf_push(PCBUF, ITEM) \ 33 | ({ \ 34 | bool ok = !cbuf_is_full(PCBUF); \ 35 | if (ok) { \ 36 | (PCBUF)->data[(PCBUF)->head] = (ITEM); \ 37 | (PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \ 38 | } \ 39 | ok; \ 40 | }) 41 | 42 | #define cbuf_take(PCBUF, PITEM) \ 43 | ({ \ 44 | bool ok = !cbuf_is_empty(PCBUF); \ 45 | if (ok) { \ 46 | *(PITEM) = (PCBUF)->data[(PCBUF)->tail]; \ 47 | (PCBUF)->tail = ((PCBUF)->tail + 1) % cbuf_size_(PCBUF); \ 48 | } \ 49 | ok; \ 50 | }) 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /app/src/util/log.c: -------------------------------------------------------------------------------- 1 | #include "log.h" 2 | 3 | #include 4 | 5 | static SDL_LogPriority 6 | log_level_sc_to_sdl(enum sc_log_level level) { 7 | switch (level) { 8 | case SC_LOG_LEVEL_VERBOSE: 9 | return SDL_LOG_PRIORITY_VERBOSE; 10 | case SC_LOG_LEVEL_DEBUG: 11 | return SDL_LOG_PRIORITY_DEBUG; 12 | case SC_LOG_LEVEL_INFO: 13 | return SDL_LOG_PRIORITY_INFO; 14 | case SC_LOG_LEVEL_WARN: 15 | return SDL_LOG_PRIORITY_WARN; 16 | case SC_LOG_LEVEL_ERROR: 17 | return SDL_LOG_PRIORITY_ERROR; 18 | default: 19 | assert(!"unexpected log level"); 20 | return SDL_LOG_PRIORITY_INFO; 21 | } 22 | } 23 | 24 | static enum sc_log_level 25 | log_level_sdl_to_sc(SDL_LogPriority priority) { 26 | switch (priority) { 27 | case SDL_LOG_PRIORITY_VERBOSE: 28 | return SC_LOG_LEVEL_VERBOSE; 29 | case SDL_LOG_PRIORITY_DEBUG: 30 | return SC_LOG_LEVEL_DEBUG; 31 | case SDL_LOG_PRIORITY_INFO: 32 | return SC_LOG_LEVEL_INFO; 33 | case SDL_LOG_PRIORITY_WARN: 34 | return SC_LOG_LEVEL_WARN; 35 | case SDL_LOG_PRIORITY_ERROR: 36 | return SC_LOG_LEVEL_ERROR; 37 | default: 38 | assert(!"unexpected log level"); 39 | return SC_LOG_LEVEL_INFO; 40 | } 41 | } 42 | 43 | void 44 | sc_set_log_level(enum sc_log_level level) { 45 | SDL_LogPriority sdl_log = log_level_sc_to_sdl(level); 46 | SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log); 47 | } 48 | 49 | enum sc_log_level 50 | sc_get_log_level(void) { 51 | SDL_LogPriority sdl_log = SDL_LogGetPriority(SDL_LOG_CATEGORY_APPLICATION); 52 | return log_level_sdl_to_sc(sdl_log); 53 | } 54 | -------------------------------------------------------------------------------- /app/src/util/log.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_LOG_H 2 | #define SC_LOG_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | 8 | #include "scrcpy.h" 9 | 10 | #define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 11 | #define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 12 | #define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 13 | #define LOGW(...) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 14 | #define LOGE(...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 15 | #define LOGC(...) SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 16 | 17 | void 18 | sc_set_log_level(enum sc_log_level level); 19 | 20 | enum sc_log_level 21 | sc_get_log_level(void); 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /app/src/util/net.c: -------------------------------------------------------------------------------- 1 | #include "net.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "log.h" 7 | 8 | #ifdef __WINDOWS__ 9 | typedef int socklen_t; 10 | #else 11 | # include 12 | # include 13 | # include 14 | # include 15 | # include 16 | # define SOCKET_ERROR -1 17 | typedef struct sockaddr_in SOCKADDR_IN; 18 | typedef struct sockaddr SOCKADDR; 19 | typedef struct in_addr IN_ADDR; 20 | #endif 21 | 22 | static void 23 | net_perror(const char *s) { 24 | #ifdef _WIN32 25 | int error = WSAGetLastError(); 26 | char *wsa_message; 27 | FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, 28 | NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), 29 | (char *) &wsa_message, 0, NULL); 30 | // no explicit '\n', wsa_message already contains a trailing '\n' 31 | fprintf(stderr, "%s: [%d] %s", s, error, wsa_message); 32 | LocalFree(wsa_message); 33 | #else 34 | perror(s); 35 | #endif 36 | } 37 | 38 | socket_t 39 | net_connect(uint32_t addr, uint16_t port) { 40 | socket_t sock = socket(AF_INET, SOCK_STREAM, 0); 41 | if (sock == INVALID_SOCKET) { 42 | net_perror("socket"); 43 | return INVALID_SOCKET; 44 | } 45 | 46 | SOCKADDR_IN sin; 47 | sin.sin_family = AF_INET; 48 | sin.sin_addr.s_addr = htonl(addr); 49 | sin.sin_port = htons(port); 50 | 51 | if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { 52 | net_perror("connect"); 53 | net_close(sock); 54 | return INVALID_SOCKET; 55 | } 56 | 57 | return sock; 58 | } 59 | 60 | socket_t 61 | net_listen(uint32_t addr, uint16_t port, int backlog) { 62 | socket_t sock = socket(AF_INET, SOCK_STREAM, 0); 63 | if (sock == INVALID_SOCKET) { 64 | net_perror("socket"); 65 | return INVALID_SOCKET; 66 | } 67 | 68 | int reuse = 1; 69 | if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, 70 | sizeof(reuse)) == -1) { 71 | net_perror("setsockopt(SO_REUSEADDR)"); 72 | } 73 | 74 | SOCKADDR_IN sin; 75 | sin.sin_family = AF_INET; 76 | sin.sin_addr.s_addr = htonl(addr); // htonl() harmless on INADDR_ANY 77 | sin.sin_port = htons(port); 78 | 79 | if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { 80 | net_perror("bind"); 81 | net_close(sock); 82 | return INVALID_SOCKET; 83 | } 84 | 85 | if (listen(sock, backlog) == SOCKET_ERROR) { 86 | net_perror("listen"); 87 | net_close(sock); 88 | return INVALID_SOCKET; 89 | } 90 | 91 | return sock; 92 | } 93 | 94 | socket_t 95 | net_accept(socket_t server_socket) { 96 | SOCKADDR_IN csin; 97 | socklen_t sinsize = sizeof(csin); 98 | return accept(server_socket, (SOCKADDR *) &csin, &sinsize); 99 | } 100 | 101 | ssize_t 102 | net_recv(socket_t socket, void *buf, size_t len) { 103 | return recv(socket, buf, len, 0); 104 | } 105 | 106 | ssize_t 107 | net_recv_all(socket_t socket, void *buf, size_t len) { 108 | return recv(socket, buf, len, MSG_WAITALL); 109 | } 110 | 111 | ssize_t 112 | net_send(socket_t socket, const void *buf, size_t len) { 113 | return send(socket, buf, len, 0); 114 | } 115 | 116 | ssize_t 117 | net_send_all(socket_t socket, const void *buf, size_t len) { 118 | size_t copied = 0; 119 | ssize_t w = 0; 120 | while (len > 0) { 121 | w = send(socket, buf, len, 0); 122 | if (w == -1) { 123 | return copied ? (ssize_t) copied : -1; 124 | } 125 | len -= w; 126 | buf = (char *) buf + w; 127 | copied += w; 128 | } 129 | return copied; 130 | } 131 | 132 | bool 133 | net_shutdown(socket_t socket, int how) { 134 | return !shutdown(socket, how); 135 | } 136 | 137 | bool 138 | net_init(void) { 139 | #ifdef __WINDOWS__ 140 | WSADATA wsa; 141 | int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; 142 | if (res < 0) { 143 | LOGC("WSAStartup failed with error %d", res); 144 | return false; 145 | } 146 | #endif 147 | return true; 148 | } 149 | 150 | void 151 | net_cleanup(void) { 152 | #ifdef __WINDOWS__ 153 | WSACleanup(); 154 | #endif 155 | } 156 | 157 | bool 158 | net_close(socket_t socket) { 159 | #ifdef __WINDOWS__ 160 | return !closesocket(socket); 161 | #else 162 | return !close(socket); 163 | #endif 164 | } 165 | -------------------------------------------------------------------------------- /app/src/util/net.h: -------------------------------------------------------------------------------- 1 | #ifndef NET_H 2 | #define NET_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #ifdef __WINDOWS__ 11 | # include 12 | #define SHUT_RD SD_RECEIVE 13 | #define SHUT_WR SD_SEND 14 | #define SHUT_RDWR SD_BOTH 15 | typedef SOCKET socket_t; 16 | #else 17 | # include 18 | # define INVALID_SOCKET -1 19 | typedef int socket_t; 20 | #endif 21 | 22 | bool 23 | net_init(void); 24 | 25 | void 26 | net_cleanup(void); 27 | 28 | socket_t 29 | net_connect(uint32_t addr, uint16_t port); 30 | 31 | socket_t 32 | net_listen(uint32_t addr, uint16_t port, int backlog); 33 | 34 | socket_t 35 | net_accept(socket_t server_socket); 36 | 37 | // the _all versions wait/retry until len bytes have been written/read 38 | ssize_t 39 | net_recv(socket_t socket, void *buf, size_t len); 40 | 41 | ssize_t 42 | net_recv_all(socket_t socket, void *buf, size_t len); 43 | 44 | ssize_t 45 | net_send(socket_t socket, const void *buf, size_t len); 46 | 47 | ssize_t 48 | net_send_all(socket_t socket, const void *buf, size_t len); 49 | 50 | // how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both) 51 | bool 52 | net_shutdown(socket_t socket, int how); 53 | 54 | bool 55 | net_close(socket_t socket); 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /app/src/util/process.c: -------------------------------------------------------------------------------- 1 | #include "process.h" 2 | 3 | #include "log.h" 4 | 5 | bool 6 | process_check_success(process_t proc, const char *name, bool close) { 7 | if (proc == PROCESS_NONE) { 8 | LOGE("Could not execute \"%s\"", name); 9 | return false; 10 | } 11 | exit_code_t exit_code = process_wait(proc, close); 12 | if (exit_code) { 13 | if (exit_code != NO_EXIT_CODE) { 14 | LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code); 15 | } else { 16 | LOGE("\"%s\" exited unexpectedly", name); 17 | } 18 | return false; 19 | } 20 | return true; 21 | } 22 | -------------------------------------------------------------------------------- /app/src/util/process.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_PROCESS_H 2 | #define SC_PROCESS_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | 8 | #ifdef _WIN32 9 | 10 | // not needed here, but winsock2.h must never be included AFTER windows.h 11 | # include 12 | # include 13 | # define PATH_SEPARATOR '\\' 14 | # define PRIexitcode "lu" 15 | // 16 | # define PRIsizet "Iu" 17 | # define PROCESS_NONE NULL 18 | # define NO_EXIT_CODE -1u // max value as unsigned 19 | typedef HANDLE process_t; 20 | typedef DWORD exit_code_t; 21 | 22 | #else 23 | 24 | # include 25 | # define PATH_SEPARATOR '/' 26 | # define PRIsizet "zu" 27 | # define PRIexitcode "d" 28 | # define PROCESS_NONE -1 29 | # define NO_EXIT_CODE -1 30 | typedef pid_t process_t; 31 | typedef int exit_code_t; 32 | 33 | #endif 34 | 35 | enum process_result { 36 | PROCESS_SUCCESS, 37 | PROCESS_ERROR_GENERIC, 38 | PROCESS_ERROR_MISSING_BINARY, 39 | }; 40 | 41 | // execute the command and write the result to the output parameter "process" 42 | enum process_result 43 | process_execute(const char *const argv[], process_t *process); 44 | 45 | // kill the process 46 | bool 47 | process_terminate(process_t pid); 48 | 49 | // wait and close the process (like waitpid()) 50 | // the "close" flag indicates if the process must be "closed" (reaped) 51 | // (passing false is equivalent to enable WNOWAIT in waitid()) 52 | exit_code_t 53 | process_wait(process_t pid, bool close); 54 | 55 | // close the process 56 | // 57 | // Semantically, process_wait(close) = process_wait(noclose) + process_close 58 | void 59 | process_close(process_t pid); 60 | 61 | // convenience function to wait for a successful process execution 62 | // automatically log process errors with the provided process name 63 | bool 64 | process_check_success(process_t proc, const char *name, bool close); 65 | 66 | #ifndef _WIN32 67 | // only used to find package manager, not implemented for Windows 68 | bool 69 | search_executable(const char *file); 70 | #endif 71 | 72 | // return the absolute path of the executable (the scrcpy binary) 73 | // may be NULL on error; to be freed by free() 74 | char * 75 | get_executable_path(void); 76 | 77 | // returns true if the file exists and is not a directory 78 | bool 79 | is_regular_file(const char *path); 80 | 81 | #endif 82 | -------------------------------------------------------------------------------- /app/src/util/queue.h: -------------------------------------------------------------------------------- 1 | // generic intrusive FIFO queue 2 | #ifndef SC_QUEUE_H 3 | #define SC_QUEUE_H 4 | 5 | #include "common.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | // To define a queue type of "struct foo": 12 | // struct queue_foo QUEUE(struct foo); 13 | #define SC_QUEUE(TYPE) { \ 14 | TYPE *first; \ 15 | TYPE *last; \ 16 | } 17 | 18 | #define sc_queue_init(PQ) \ 19 | (void) ((PQ)->first = (PQ)->last = NULL) 20 | 21 | #define sc_queue_is_empty(PQ) \ 22 | !(PQ)->first 23 | 24 | // NEXTFIELD is the field in the ITEM type used for intrusive linked-list 25 | // 26 | // For example: 27 | // struct foo { 28 | // int value; 29 | // struct foo *next; 30 | // }; 31 | // 32 | // // define the type "struct my_queue" 33 | // struct my_queue SC_QUEUE(struct foo); 34 | // 35 | // struct my_queue queue; 36 | // sc_queue_init(&queue); 37 | // 38 | // struct foo v1 = { .value = 42 }; 39 | // struct foo v2 = { .value = 27 }; 40 | // 41 | // sc_queue_push(&queue, next, v1); 42 | // sc_queue_push(&queue, next, v2); 43 | // 44 | // struct foo *foo; 45 | // sc_queue_take(&queue, next, &foo); 46 | // assert(foo->value == 42); 47 | // sc_queue_take(&queue, next, &foo); 48 | // assert(foo->value == 27); 49 | // assert(sc_queue_is_empty(&queue)); 50 | // 51 | 52 | // push a new item into the queue 53 | #define sc_queue_push(PQ, NEXTFIELD, ITEM) \ 54 | (void) ({ \ 55 | (ITEM)->NEXTFIELD = NULL; \ 56 | if (sc_queue_is_empty(PQ)) { \ 57 | (PQ)->first = (PQ)->last = (ITEM); \ 58 | } else { \ 59 | (PQ)->last->NEXTFIELD = (ITEM); \ 60 | (PQ)->last = (ITEM); \ 61 | } \ 62 | }) 63 | 64 | // take the next item and remove it from the queue (the queue must not be empty) 65 | // the result is stored in *(PITEM) 66 | // (without typeof(), we could not store a local variable having the correct 67 | // type so that we can "return" it) 68 | #define sc_queue_take(PQ, NEXTFIELD, PITEM) \ 69 | (void) ({ \ 70 | assert(!sc_queue_is_empty(PQ)); \ 71 | *(PITEM) = (PQ)->first; \ 72 | (PQ)->first = (PQ)->first->NEXTFIELD; \ 73 | }) 74 | // no need to update (PQ)->last if the queue is left empty: 75 | // (PQ)->last is undefined if !(PQ)->first anyway 76 | 77 | #endif 78 | -------------------------------------------------------------------------------- /app/src/util/str_util.h: -------------------------------------------------------------------------------- 1 | #ifndef STRUTIL_H 2 | #define STRUTIL_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | 9 | // like strncpy, except: 10 | // - it copies at most n-1 chars 11 | // - the dest string is nul-terminated 12 | // - it does not write useless bytes if strlen(src) < n 13 | // - it returns the number of chars actually written (max n-1) if src has 14 | // been copied completely, or n if src has been truncated 15 | size_t 16 | xstrncpy(char *dest, const char *src, size_t n); 17 | 18 | // join tokens by sep into dst 19 | // returns the number of chars actually written (max n-1) if no truncation 20 | // occurred, or n if truncated 21 | size_t 22 | xstrjoin(char *dst, const char *const tokens[], char sep, size_t n); 23 | 24 | // quote a string 25 | // returns the new allocated string, to be freed by the caller 26 | char * 27 | strquote(const char *src); 28 | 29 | // parse s as an integer into value 30 | // returns true if the conversion succeeded, false otherwise 31 | bool 32 | parse_integer(const char *s, long *out); 33 | 34 | // parse s as integers separated by sep (for example '1234:2000') 35 | // returns the number of integers on success, 0 on failure 36 | size_t 37 | parse_integers(const char *s, const char sep, size_t max_items, long *out); 38 | 39 | // parse s as an integer into value 40 | // like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as 41 | // suffix 42 | // returns true if the conversion succeeded, false otherwise 43 | bool 44 | parse_integer_with_suffix(const char *s, long *out); 45 | 46 | // search s in the list separated by sep 47 | // for example, strlist_contains("a,bc,def", ',', "bc") returns true 48 | bool 49 | strlist_contains(const char *list, char sep, const char *s); 50 | 51 | // return the index to truncate a UTF-8 string at a valid position 52 | size_t 53 | utf8_truncation_index(const char *utf8, size_t max_len); 54 | 55 | #ifdef _WIN32 56 | // convert a UTF-8 string to a wchar_t string 57 | // returns the new allocated string, to be freed by the caller 58 | wchar_t * 59 | utf8_to_wide_char(const char *utf8); 60 | 61 | char * 62 | utf8_from_wide_char(const wchar_t *s); 63 | #endif 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /app/src/util/thread.c: -------------------------------------------------------------------------------- 1 | #include "thread.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "log.h" 7 | 8 | bool 9 | sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, 10 | void *userdata) { 11 | SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata); 12 | if (!sdl_thread) { 13 | return false; 14 | } 15 | 16 | thread->thread = sdl_thread; 17 | return true; 18 | } 19 | 20 | void 21 | sc_thread_join(sc_thread *thread, int *status) { 22 | SDL_WaitThread(thread->thread, status); 23 | } 24 | 25 | bool 26 | sc_mutex_init(sc_mutex *mutex) { 27 | SDL_mutex *sdl_mutex = SDL_CreateMutex(); 28 | if (!sdl_mutex) { 29 | return false; 30 | } 31 | 32 | mutex->mutex = sdl_mutex; 33 | #ifndef NDEBUG 34 | atomic_init(&mutex->locker, 0); 35 | #endif 36 | return true; 37 | } 38 | 39 | void 40 | sc_mutex_destroy(sc_mutex *mutex) { 41 | SDL_DestroyMutex(mutex->mutex); 42 | } 43 | 44 | void 45 | sc_mutex_lock(sc_mutex *mutex) { 46 | // SDL mutexes are recursive, but we don't want to use recursive mutexes 47 | assert(!sc_mutex_held(mutex)); 48 | int r = SDL_LockMutex(mutex->mutex); 49 | #ifndef NDEBUG 50 | if (r) { 51 | LOGC("Could not lock mutex: %s", SDL_GetError()); 52 | abort(); 53 | } 54 | 55 | atomic_store_explicit(&mutex->locker, sc_thread_get_id(), 56 | memory_order_relaxed); 57 | #else 58 | (void) r; 59 | #endif 60 | } 61 | 62 | void 63 | sc_mutex_unlock(sc_mutex *mutex) { 64 | #ifndef NDEBUG 65 | assert(sc_mutex_held(mutex)); 66 | atomic_store_explicit(&mutex->locker, 0, memory_order_relaxed); 67 | #endif 68 | int r = SDL_UnlockMutex(mutex->mutex); 69 | #ifndef NDEBUG 70 | if (r) { 71 | LOGC("Could not lock mutex: %s", SDL_GetError()); 72 | abort(); 73 | } 74 | #else 75 | (void) r; 76 | #endif 77 | } 78 | 79 | sc_thread_id 80 | sc_thread_get_id(void) { 81 | return SDL_ThreadID(); 82 | } 83 | 84 | #ifndef NDEBUG 85 | bool 86 | sc_mutex_held(struct sc_mutex *mutex) { 87 | sc_thread_id locker_id = 88 | atomic_load_explicit(&mutex->locker, memory_order_relaxed); 89 | return locker_id == sc_thread_get_id(); 90 | } 91 | #endif 92 | 93 | bool 94 | sc_cond_init(sc_cond *cond) { 95 | SDL_cond *sdl_cond = SDL_CreateCond(); 96 | if (!sdl_cond) { 97 | return false; 98 | } 99 | 100 | cond->cond = sdl_cond; 101 | return true; 102 | } 103 | 104 | void 105 | sc_cond_destroy(sc_cond *cond) { 106 | SDL_DestroyCond(cond->cond); 107 | } 108 | 109 | void 110 | sc_cond_wait(sc_cond *cond, sc_mutex *mutex) { 111 | int r = SDL_CondWait(cond->cond, mutex->mutex); 112 | #ifndef NDEBUG 113 | if (r) { 114 | LOGC("Could not wait on condition: %s", SDL_GetError()); 115 | abort(); 116 | } 117 | 118 | atomic_store_explicit(&mutex->locker, sc_thread_get_id(), 119 | memory_order_relaxed); 120 | #else 121 | (void) r; 122 | #endif 123 | } 124 | 125 | bool 126 | sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) { 127 | sc_tick now = sc_tick_now(); 128 | if (deadline <= now) { 129 | return false; // timeout 130 | } 131 | 132 | uint32_t ms = SC_TICK_TO_MS(deadline - now); 133 | int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms); 134 | #ifndef NDEBUG 135 | if (r < 0) { 136 | LOGC("Could not wait on condition with timeout: %s", SDL_GetError()); 137 | abort(); 138 | } 139 | 140 | atomic_store_explicit(&mutex->locker, sc_thread_get_id(), 141 | memory_order_relaxed); 142 | #endif 143 | assert(r == 0 || r == SDL_MUTEX_TIMEDOUT); 144 | return r == 0; 145 | } 146 | 147 | void 148 | sc_cond_signal(sc_cond *cond) { 149 | int r = SDL_CondSignal(cond->cond); 150 | #ifndef NDEBUG 151 | if (r) { 152 | LOGC("Could not signal a condition: %s", SDL_GetError()); 153 | abort(); 154 | } 155 | #else 156 | (void) r; 157 | #endif 158 | } 159 | 160 | void 161 | sc_cond_broadcast(sc_cond *cond) { 162 | int r = SDL_CondBroadcast(cond->cond); 163 | #ifndef NDEBUG 164 | if (r) { 165 | LOGC("Could not broadcast a condition: %s", SDL_GetError()); 166 | abort(); 167 | } 168 | #else 169 | (void) r; 170 | #endif 171 | } 172 | -------------------------------------------------------------------------------- /app/src/util/thread.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_THREAD_H 2 | #define SC_THREAD_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | #include 8 | 9 | #include "tick.h" 10 | 11 | /* Forward declarations */ 12 | typedef struct SDL_Thread SDL_Thread; 13 | typedef struct SDL_mutex SDL_mutex; 14 | typedef struct SDL_cond SDL_cond; 15 | 16 | typedef int sc_thread_fn(void *); 17 | typedef unsigned sc_thread_id; 18 | typedef atomic_uint sc_atomic_thread_id; 19 | 20 | typedef struct sc_thread { 21 | SDL_Thread *thread; 22 | } sc_thread; 23 | 24 | typedef struct sc_mutex { 25 | SDL_mutex *mutex; 26 | #ifndef NDEBUG 27 | sc_atomic_thread_id locker; 28 | #endif 29 | } sc_mutex; 30 | 31 | typedef struct sc_cond { 32 | SDL_cond *cond; 33 | } sc_cond; 34 | 35 | bool 36 | sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, 37 | void *userdata); 38 | 39 | void 40 | sc_thread_join(sc_thread *thread, int *status); 41 | 42 | bool 43 | sc_mutex_init(sc_mutex *mutex); 44 | 45 | void 46 | sc_mutex_destroy(sc_mutex *mutex); 47 | 48 | void 49 | sc_mutex_lock(sc_mutex *mutex); 50 | 51 | void 52 | sc_mutex_unlock(sc_mutex *mutex); 53 | 54 | sc_thread_id 55 | sc_thread_get_id(void); 56 | 57 | #ifndef NDEBUG 58 | bool 59 | sc_mutex_held(struct sc_mutex *mutex); 60 | # define sc_mutex_assert(mutex) assert(sc_mutex_held(mutex)) 61 | #else 62 | # define sc_mutex_assert(mutex) 63 | #endif 64 | 65 | bool 66 | sc_cond_init(sc_cond *cond); 67 | 68 | void 69 | sc_cond_destroy(sc_cond *cond); 70 | 71 | void 72 | sc_cond_wait(sc_cond *cond, sc_mutex *mutex); 73 | 74 | // return true on signaled, false on timeout 75 | bool 76 | sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline); 77 | 78 | void 79 | sc_cond_signal(sc_cond *cond); 80 | 81 | void 82 | sc_cond_broadcast(sc_cond *cond); 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /app/src/util/tick.c: -------------------------------------------------------------------------------- 1 | #include "tick.h" 2 | 3 | #include 4 | 5 | sc_tick 6 | sc_tick_now(void) { 7 | // SDL_GetTicks() resolution is in milliseconds, but sc_tick are expressed 8 | // in microseconds to store PTS without precision loss. 9 | // 10 | // As an alternative, SDL_GetPerformanceCounter() and 11 | // SDL_GetPerformanceFrequency() could be used, but: 12 | // - the conversions (avoiding overflow) are expansive, since the 13 | // frequency is not known at compile time; 14 | // - in practice, we don't need more precision for now. 15 | return (sc_tick) SDL_GetTicks() * 1000; 16 | } 17 | -------------------------------------------------------------------------------- /app/src/util/tick.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_TICK_H 2 | #define SC_TICK_H 3 | 4 | #include 5 | 6 | typedef int64_t sc_tick; 7 | #define PRItick PRIi64 8 | #define SC_TICK_FREQ 1000000 // microsecond 9 | 10 | // To be adapted if SC_TICK_FREQ changes 11 | #define SC_TICK_TO_US(tick) (tick) 12 | #define SC_TICK_TO_MS(tick) ((tick) / 1000) 13 | #define SC_TICK_TO_SEC(tick) ((tick) / 1000000) 14 | #define SC_TICK_FROM_US(us) (us) 15 | #define SC_TICK_FROM_MS(ms) ((ms) * 1000) 16 | #define SC_TICK_FROM_SEC(sec) ((sec) * 1000000) 17 | 18 | sc_tick 19 | sc_tick_now(void); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /app/src/v4l2_sink.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_V4L2_SINK_H 2 | #define SC_V4L2_SINK_H 3 | 4 | #include "common.h" 5 | 6 | #include "coords.h" 7 | #include "trait/frame_sink.h" 8 | #include "video_buffer.h" 9 | #include "util/tick.h" 10 | 11 | #include 12 | 13 | struct sc_v4l2_sink { 14 | struct sc_frame_sink frame_sink; // frame sink trait 15 | 16 | struct sc_video_buffer vb; 17 | AVFormatContext *format_ctx; 18 | AVCodecContext *encoder_ctx; 19 | 20 | char *device_name; 21 | struct size frame_size; 22 | sc_tick buffering_time; 23 | 24 | sc_thread thread; 25 | sc_mutex mutex; 26 | sc_cond cond; 27 | bool has_frame; 28 | bool stopped; 29 | bool header_written; 30 | 31 | AVFrame *frame; 32 | AVPacket *packet; 33 | }; 34 | 35 | bool 36 | sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, 37 | struct size frame_size, sc_tick buffering_time); 38 | 39 | void 40 | sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs); 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /app/src/video_buffer.h: -------------------------------------------------------------------------------- 1 | #ifndef SC_VIDEO_BUFFER_H 2 | #define SC_VIDEO_BUFFER_H 3 | 4 | #include "common.h" 5 | 6 | #include 7 | 8 | #include "clock.h" 9 | #include "frame_buffer.h" 10 | #include "util/queue.h" 11 | #include "util/thread.h" 12 | #include "util/tick.h" 13 | 14 | // forward declarations 15 | typedef struct AVFrame AVFrame; 16 | 17 | struct sc_video_buffer_frame { 18 | AVFrame *frame; 19 | struct sc_video_buffer_frame *next; 20 | #ifndef NDEBUG 21 | sc_tick push_date; 22 | #endif 23 | }; 24 | 25 | struct sc_video_buffer_frame_queue SC_QUEUE(struct sc_video_buffer_frame); 26 | 27 | struct sc_video_buffer { 28 | struct sc_frame_buffer fb; 29 | 30 | sc_tick buffering_time; 31 | 32 | // only if buffering_time > 0 33 | struct { 34 | sc_thread thread; 35 | sc_mutex mutex; 36 | sc_cond queue_cond; 37 | sc_cond wait_cond; 38 | 39 | struct sc_clock clock; 40 | struct sc_video_buffer_frame_queue queue; 41 | bool stopped; 42 | } b; // buffering 43 | 44 | const struct sc_video_buffer_callbacks *cbs; 45 | void *cbs_userdata; 46 | }; 47 | 48 | struct sc_video_buffer_callbacks { 49 | void (*on_new_frame)(struct sc_video_buffer *vb, bool previous_skipped, 50 | void *userdata); 51 | }; 52 | 53 | bool 54 | sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, 55 | const struct sc_video_buffer_callbacks *cbs, 56 | void *cbs_userdata); 57 | 58 | bool 59 | sc_video_buffer_start(struct sc_video_buffer *vb); 60 | 61 | void 62 | sc_video_buffer_stop(struct sc_video_buffer *vb); 63 | 64 | void 65 | sc_video_buffer_join(struct sc_video_buffer *vb); 66 | 67 | void 68 | sc_video_buffer_destroy(struct sc_video_buffer *vb); 69 | 70 | bool 71 | sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame); 72 | 73 | void 74 | sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst); 75 | 76 | #endif 77 | -------------------------------------------------------------------------------- /app/tests/test_buffer_util.c: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | 3 | #include 4 | 5 | #include "util/buffer_util.h" 6 | 7 | static void test_buffer_write16be(void) { 8 | uint16_t val = 0xABCD; 9 | uint8_t buf[2]; 10 | 11 | buffer_write16be(buf, val); 12 | 13 | assert(buf[0] == 0xAB); 14 | assert(buf[1] == 0xCD); 15 | } 16 | 17 | static void test_buffer_write32be(void) { 18 | uint32_t val = 0xABCD1234; 19 | uint8_t buf[4]; 20 | 21 | buffer_write32be(buf, val); 22 | 23 | assert(buf[0] == 0xAB); 24 | assert(buf[1] == 0xCD); 25 | assert(buf[2] == 0x12); 26 | assert(buf[3] == 0x34); 27 | } 28 | 29 | static void test_buffer_write64be(void) { 30 | uint64_t val = 0xABCD1234567890EF; 31 | uint8_t buf[8]; 32 | 33 | buffer_write64be(buf, val); 34 | 35 | assert(buf[0] == 0xAB); 36 | assert(buf[1] == 0xCD); 37 | assert(buf[2] == 0x12); 38 | assert(buf[3] == 0x34); 39 | assert(buf[4] == 0x56); 40 | assert(buf[5] == 0x78); 41 | assert(buf[6] == 0x90); 42 | assert(buf[7] == 0xEF); 43 | } 44 | 45 | static void test_buffer_read16be(void) { 46 | uint8_t buf[2] = {0xAB, 0xCD}; 47 | 48 | uint16_t val = buffer_read16be(buf); 49 | 50 | assert(val == 0xABCD); 51 | } 52 | 53 | static void test_buffer_read32be(void) { 54 | uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34}; 55 | 56 | uint32_t val = buffer_read32be(buf); 57 | 58 | assert(val == 0xABCD1234); 59 | } 60 | 61 | static void test_buffer_read64be(void) { 62 | uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34, 63 | 0x56, 0x78, 0x90, 0xEF}; 64 | 65 | uint64_t val = buffer_read64be(buf); 66 | 67 | assert(val == 0xABCD1234567890EF); 68 | } 69 | 70 | int main(int argc, char *argv[]) { 71 | (void) argc; 72 | (void) argv; 73 | 74 | test_buffer_write16be(); 75 | test_buffer_write32be(); 76 | test_buffer_write64be(); 77 | test_buffer_read16be(); 78 | test_buffer_read32be(); 79 | test_buffer_read64be(); 80 | return 0; 81 | } 82 | -------------------------------------------------------------------------------- /app/tests/test_cbuf.c: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "util/cbuf.h" 7 | 8 | struct int_queue CBUF(int, 32); 9 | 10 | static void test_cbuf_empty(void) { 11 | struct int_queue queue; 12 | cbuf_init(&queue); 13 | 14 | assert(cbuf_is_empty(&queue)); 15 | 16 | bool push_ok = cbuf_push(&queue, 42); 17 | assert(push_ok); 18 | assert(!cbuf_is_empty(&queue)); 19 | 20 | int item; 21 | bool take_ok = cbuf_take(&queue, &item); 22 | assert(take_ok); 23 | assert(cbuf_is_empty(&queue)); 24 | 25 | bool take_empty_ok = cbuf_take(&queue, &item); 26 | assert(!take_empty_ok); // the queue is empty 27 | } 28 | 29 | static void test_cbuf_full(void) { 30 | struct int_queue queue; 31 | cbuf_init(&queue); 32 | 33 | assert(!cbuf_is_full(&queue)); 34 | 35 | // fill the queue 36 | for (int i = 0; i < 32; ++i) { 37 | bool ok = cbuf_push(&queue, i); 38 | assert(ok); 39 | } 40 | bool ok = cbuf_push(&queue, 42); 41 | assert(!ok); // the queue if full 42 | 43 | int item; 44 | bool take_ok = cbuf_take(&queue, &item); 45 | assert(take_ok); 46 | assert(!cbuf_is_full(&queue)); 47 | } 48 | 49 | static void test_cbuf_push_take(void) { 50 | struct int_queue queue; 51 | cbuf_init(&queue); 52 | 53 | bool push1_ok = cbuf_push(&queue, 42); 54 | assert(push1_ok); 55 | 56 | bool push2_ok = cbuf_push(&queue, 35); 57 | assert(push2_ok); 58 | 59 | int item; 60 | 61 | bool take1_ok = cbuf_take(&queue, &item); 62 | assert(take1_ok); 63 | assert(item == 42); 64 | 65 | bool take2_ok = cbuf_take(&queue, &item); 66 | assert(take2_ok); 67 | assert(item == 35); 68 | } 69 | 70 | int main(int argc, char *argv[]) { 71 | (void) argc; 72 | (void) argv; 73 | 74 | test_cbuf_empty(); 75 | test_cbuf_full(); 76 | test_cbuf_push_take(); 77 | return 0; 78 | } 79 | -------------------------------------------------------------------------------- /app/tests/test_clock.c: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | 3 | #include 4 | 5 | #include "clock.h" 6 | 7 | void test_small_rolling_sum(void) { 8 | struct sc_clock clock; 9 | sc_clock_init(&clock); 10 | 11 | assert(clock.count == 0); 12 | assert(clock.left_sum.system == 0); 13 | assert(clock.left_sum.stream == 0); 14 | assert(clock.right_sum.system == 0); 15 | assert(clock.right_sum.stream == 0); 16 | 17 | sc_clock_update(&clock, 2, 3); 18 | assert(clock.count == 1); 19 | assert(clock.left_sum.system == 0); 20 | assert(clock.left_sum.stream == 0); 21 | assert(clock.right_sum.system == 2); 22 | assert(clock.right_sum.stream == 3); 23 | 24 | sc_clock_update(&clock, 10, 20); 25 | assert(clock.count == 2); 26 | assert(clock.left_sum.system == 2); 27 | assert(clock.left_sum.stream == 3); 28 | assert(clock.right_sum.system == 10); 29 | assert(clock.right_sum.stream == 20); 30 | 31 | sc_clock_update(&clock, 40, 80); 32 | assert(clock.count == 3); 33 | assert(clock.left_sum.system == 2); 34 | assert(clock.left_sum.stream == 3); 35 | assert(clock.right_sum.system == 50); 36 | assert(clock.right_sum.stream == 100); 37 | 38 | sc_clock_update(&clock, 400, 800); 39 | assert(clock.count == 4); 40 | assert(clock.left_sum.system == 12); 41 | assert(clock.left_sum.stream == 23); 42 | assert(clock.right_sum.system == 440); 43 | assert(clock.right_sum.stream == 880); 44 | } 45 | 46 | void test_large_rolling_sum(void) { 47 | const unsigned half_range = SC_CLOCK_RANGE / 2; 48 | 49 | struct sc_clock clock1; 50 | sc_clock_init(&clock1); 51 | for (unsigned i = 0; i < 5 * half_range; ++i) { 52 | sc_clock_update(&clock1, i, 2 * i + 1); 53 | } 54 | 55 | struct sc_clock clock2; 56 | sc_clock_init(&clock2); 57 | for (unsigned i = 3 * half_range; i < 5 * half_range; ++i) { 58 | sc_clock_update(&clock2, i, 2 * i + 1); 59 | } 60 | 61 | assert(clock1.count == SC_CLOCK_RANGE); 62 | assert(clock2.count == SC_CLOCK_RANGE); 63 | 64 | // The values before the last SC_CLOCK_RANGE points in clock1 should have 65 | // no impact 66 | assert(clock1.left_sum.system == clock2.left_sum.system); 67 | assert(clock1.left_sum.stream == clock2.left_sum.stream); 68 | assert(clock1.right_sum.system == clock2.right_sum.system); 69 | assert(clock1.right_sum.stream == clock2.right_sum.stream); 70 | } 71 | 72 | int main(int argc, char *argv[]) { 73 | (void) argc; 74 | (void) argv; 75 | 76 | test_small_rolling_sum(); 77 | test_large_rolling_sum(); 78 | return 0; 79 | }; 80 | -------------------------------------------------------------------------------- /app/tests/test_device_msg_deserialize.c: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "device_msg.h" 7 | 8 | #include 9 | 10 | static void test_deserialize_clipboard(void) { 11 | const unsigned char input[] = { 12 | DEVICE_MSG_TYPE_CLIPBOARD, 13 | 0x00, 0x00, 0x00, 0x03, // text length 14 | 0x41, 0x42, 0x43, // "ABC" 15 | }; 16 | 17 | struct device_msg msg; 18 | ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); 19 | assert(r == 8); 20 | 21 | assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); 22 | assert(msg.clipboard.text); 23 | assert(!strcmp("ABC", msg.clipboard.text)); 24 | 25 | device_msg_destroy(&msg); 26 | } 27 | 28 | static void test_deserialize_clipboard_big(void) { 29 | unsigned char input[DEVICE_MSG_MAX_SIZE]; 30 | input[0] = DEVICE_MSG_TYPE_CLIPBOARD; 31 | input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24; 32 | input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16; 33 | input[3] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x0000ff00u) >> 8; 34 | input[4] = DEVICE_MSG_TEXT_MAX_LENGTH & 0x000000ffu; 35 | 36 | memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH); 37 | 38 | struct device_msg msg; 39 | ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); 40 | assert(r == DEVICE_MSG_MAX_SIZE); 41 | 42 | assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); 43 | assert(msg.clipboard.text); 44 | assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH); 45 | assert(msg.clipboard.text[0] == 'a'); 46 | 47 | device_msg_destroy(&msg); 48 | } 49 | 50 | int main(int argc, char *argv[]) { 51 | (void) argc; 52 | (void) argv; 53 | 54 | test_deserialize_clipboard(); 55 | test_deserialize_clipboard_big(); 56 | return 0; 57 | } 58 | -------------------------------------------------------------------------------- /app/tests/test_queue.c: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | 3 | #include 4 | 5 | #include "util/queue.h" 6 | 7 | struct foo { 8 | int value; 9 | struct foo *next; 10 | }; 11 | 12 | static void test_queue(void) { 13 | struct my_queue SC_QUEUE(struct foo) queue; 14 | sc_queue_init(&queue); 15 | 16 | assert(sc_queue_is_empty(&queue)); 17 | 18 | struct foo v1 = { .value = 42 }; 19 | struct foo v2 = { .value = 27 }; 20 | 21 | sc_queue_push(&queue, next, &v1); 22 | sc_queue_push(&queue, next, &v2); 23 | 24 | struct foo *foo; 25 | 26 | assert(!sc_queue_is_empty(&queue)); 27 | sc_queue_take(&queue, next, &foo); 28 | assert(foo->value == 42); 29 | 30 | assert(!sc_queue_is_empty(&queue)); 31 | sc_queue_take(&queue, next, &foo); 32 | assert(foo->value == 27); 33 | 34 | assert(sc_queue_is_empty(&queue)); 35 | } 36 | 37 | int main(int argc, char *argv[]) { 38 | (void) argc; 39 | (void) argv; 40 | 41 | test_queue(); 42 | return 0; 43 | } 44 | -------------------------------------------------------------------------------- /assets/screenshot-debian-600.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetrisTV/scrcpy/3114cfd139381e9f8a8ecad57b3a422c02770348/assets/screenshot-debian-600.jpg -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:4.0.1' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | tasks.withType(JavaCompile) { 23 | options.compilerArgs << "-Xlint:deprecation" 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /config/android-checkstyle.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'checkstyle' 2 | check.dependsOn 'checkstyle' 3 | 4 | checkstyle { 5 | toolVersion = '6.19' 6 | } 7 | 8 | task checkstyle(type: Checkstyle) { 9 | description = "Check Java style with Checkstyle" 10 | configFile = rootProject.file("config/checkstyle/checkstyle.xml") 11 | source = javaSources() 12 | classpath = files() 13 | ignoreFailures = true 14 | } 15 | 16 | def javaSources() { 17 | def files = [] 18 | android.sourceSets.each { sourceSet -> 19 | sourceSet.java.each { javaSource -> 20 | javaSource.getSrcDirs().each { 21 | if (it.exists()) { 22 | files.add(it) 23 | } 24 | } 25 | } 26 | } 27 | return files 28 | } 29 | -------------------------------------------------------------------------------- /cross_win32.txt: -------------------------------------------------------------------------------- 1 | # apt install mingw-w64 mingw-w64-tools 2 | 3 | [binaries] 4 | name = 'mingw' 5 | c = 'i686-w64-mingw32-gcc' 6 | cpp = 'i686-w64-mingw32-g++' 7 | ar = 'i686-w64-mingw32-ar' 8 | strip = 'i686-w64-mingw32-strip' 9 | pkgconfig = 'i686-w64-mingw32-pkg-config' 10 | 11 | [host_machine] 12 | system = 'windows' 13 | cpu_family = 'x86' 14 | cpu = 'i686' 15 | endian = 'little' 16 | 17 | [properties] 18 | prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared' 19 | prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev' 20 | prebuilt_sdl2 = 'SDL2-2.0.16/i686-w64-mingw32' 21 | -------------------------------------------------------------------------------- /cross_win64.txt: -------------------------------------------------------------------------------- 1 | # apt install mingw-w64 mingw-w64-tools 2 | 3 | [binaries] 4 | name = 'mingw' 5 | c = 'x86_64-w64-mingw32-gcc' 6 | cpp = 'x86_64-w64-mingw32-g++' 7 | ar = 'x86_64-w64-mingw32-ar' 8 | strip = 'x86_64-w64-mingw32-strip' 9 | pkgconfig = 'x86_64-w64-mingw32-pkg-config' 10 | 11 | [host_machine] 12 | system = 'windows' 13 | cpu_family = 'x86' 14 | cpu = 'x86_64' 15 | endian = 'little' 16 | 17 | [properties] 18 | prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared' 19 | prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev' 20 | prebuilt_sdl2 = 'SDL2-2.0.16/x86_64-w64-mingw32' 21 | -------------------------------------------------------------------------------- /data/scrcpy-console.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | scrcpy.exe %* 3 | :: if the exit code is >= 1, then pause 4 | if errorlevel 1 pause 5 | -------------------------------------------------------------------------------- /data/scrcpy-noconsole.vbs: -------------------------------------------------------------------------------- 1 | strCommand = "cmd /c scrcpy.exe" 2 | 3 | For Each Arg In WScript.Arguments 4 | strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """" 5 | Next 6 | 7 | CreateObject("Wscript.Shell").Run strCommand, 0, false 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetrisTV/scrcpy/3114cfd139381e9f8a8ecad57b3a422c02770348/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /install_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | BUILDDIR=build-auto 5 | PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-server-v1.18 6 | PREBUILT_SERVER_SHA256=641c5c6beda9399dfae72d116f5ff43b5ed1059d871c9ebc3f47610fd33c51a3 7 | 8 | echo "[scrcpy] Downloading prebuilt server..." 9 | wget "$PREBUILT_SERVER_URL" -O scrcpy-server 10 | echo "[scrcpy] Verifying prebuilt server..." 11 | echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check 12 | 13 | echo "[scrcpy] Building client..." 14 | rm -rf "$BUILDDIR" 15 | meson "$BUILDDIR" --buildtype release --strip -Db_lto=true \ 16 | -Dprebuilt_server=scrcpy-server 17 | cd "$BUILDDIR" 18 | ninja 19 | 20 | echo "[scrcpy] Installing (sudo)..." 21 | sudo ninja install 22 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('scrcpy', 'c', 2 | version: '1.19', 3 | meson_version: '>= 0.48', 4 | default_options: [ 5 | 'c_std=c11', 6 | 'warning_level=2', 7 | 'b_ndebug=if-release', 8 | ]) 9 | 10 | if get_option('compile_app') 11 | subdir('app') 12 | endif 13 | 14 | if get_option('compile_server') 15 | subdir('server') 16 | endif 17 | 18 | run_target('run', command: ['scripts/run-scrcpy.sh']) 19 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('compile_app', type: 'boolean', value: true, description: 'Build the client') 2 | option('compile_server', type: 'boolean', value: true, description: 'Build the server') 3 | option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') 4 | option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') 5 | option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') 6 | option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') 7 | option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")') 8 | -------------------------------------------------------------------------------- /prebuilt-deps/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !/.gitignore 3 | !/Makefile 4 | !/prepare-dep 5 | -------------------------------------------------------------------------------- /prebuilt-deps/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: prepare-win32 prepare-win64 \ 2 | prepare-ffmpeg-shared-win32 \ 3 | prepare-ffmpeg-dev-win32 \ 4 | prepare-ffmpeg-shared-win64 \ 5 | prepare-ffmpeg-dev-win64 \ 6 | prepare-sdl2 \ 7 | prepare-adb 8 | 9 | prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-adb 10 | prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb 11 | 12 | prepare-ffmpeg-shared-win32: 13 | @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-shared.zip \ 14 | 357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 \ 15 | ffmpeg-4.3.1-win32-shared 16 | 17 | prepare-ffmpeg-dev-win32: 18 | @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-dev.zip \ 19 | 230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b \ 20 | ffmpeg-4.3.1-win32-dev 21 | 22 | prepare-ffmpeg-shared-win64: 23 | @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-shared.zip \ 24 | dd29b7f92f48dead4dd940492c7509138c0f99db445076d0a597007298a79940 \ 25 | ffmpeg-4.3.1-win64-shared 26 | 27 | prepare-ffmpeg-dev-win64: 28 | @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-dev.zip \ 29 | 2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \ 30 | ffmpeg-4.3.1-win64-dev 31 | 32 | prepare-sdl2: 33 | @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.16-mingw.tar.gz \ 34 | 2bfe48628aa9635c12eac7d421907e291525de1d0b04b3bca4a5bd6e6c881a6f \ 35 | SDL2-2.0.16 36 | 37 | prepare-adb: 38 | @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip \ 39 | 0f4b8fdd26af2c3733539d6eebb3c2ed499ea1d4bb1f4e0ecc2d6016961a6e24 \ 40 | platform-tools 41 | -------------------------------------------------------------------------------- /prebuilt-deps/prepare-dep: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | url="$1" 4 | sum="$2" 5 | dir="$3" 6 | 7 | checksum() { 8 | local file="$1" 9 | local sum="$2" 10 | echo "$file: verifying checksum..." 11 | echo "$sum $file" | sha256sum -c 12 | } 13 | 14 | get_file() { 15 | local url="$1" 16 | local file="$2" 17 | local sum="$3" 18 | if [[ -f "$file" ]] 19 | then 20 | echo "$file: found" 21 | else 22 | echo "$file: not found, downloading..." 23 | wget "$url" -O "$file" 24 | fi 25 | checksum "$file" "$sum" 26 | } 27 | 28 | extract() { 29 | local file="$1" 30 | echo "Extracting $file..." 31 | if [[ "$file" == *.zip ]] 32 | then 33 | unzip -q "$file" 34 | elif [[ "$file" == *.tar.gz ]] 35 | then 36 | tar xf "$file" 37 | else 38 | echo "Unsupported file: $file" 39 | return 1 40 | fi 41 | } 42 | 43 | get_dep() { 44 | local url="$1" 45 | local sum="$2" 46 | local dir="$3" 47 | local file="${url##*/}" 48 | if [[ -d "$dir" ]] 49 | then 50 | echo "$dir: found" 51 | else 52 | echo "$dir: not found" 53 | get_file "$url" "$file" "$sum" 54 | extract "$file" 55 | fi 56 | } 57 | 58 | get_dep "$url" "$sum" "$dir" 59 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | make -f release.mk 3 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Run scrcpy generated in the specified BUILDDIR. 3 | # 4 | # This provides the same feature as "ninja run", except that it is possible to 5 | # pass arguments to scrcpy. 6 | # 7 | # Syntax: ./run BUILDDIR 8 | if [[ $# = 0 ]] 9 | then 10 | echo "Syntax: $0 BUILDDIR " >&2 11 | exit 1 12 | fi 13 | 14 | BUILDDIR="$1" 15 | shift 16 | 17 | if [[ ! -d "$BUILDDIR" ]] 18 | then 19 | echo "The build dir \"$BUILDDIR\" does not exist." >&2 20 | exit 1 21 | fi 22 | 23 | SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@" 24 | -------------------------------------------------------------------------------- /scripts/run-scrcpy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy" 3 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /server/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 30 5 | defaultConfig { 6 | applicationId "com.genymobile.scrcpy" 7 | minSdkVersion 21 8 | targetSdkVersion 30 9 | versionCode 11900 10 | versionName "1.19-ws6" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'org.java-websocket:Java-WebSocket:1.4.0' 24 | implementation 'org.slf4j:slf4j-api:1.7.25' 25 | implementation 'uk.uuid.slf4j:slf4j-android:1.7.25-1' 26 | testImplementation 'junit:junit:4.13' 27 | } 28 | 29 | apply from: "$project.rootDir/config/android-checkstyle.gradle" 30 | -------------------------------------------------------------------------------- /server/build_without_gradle.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script generates the scrcpy binary "manually" (without gradle). 4 | # 5 | # Adapt Android platform and build tools versions (via ANDROID_PLATFORM and 6 | # ANDROID_BUILD_TOOLS environment variables). 7 | # 8 | # Then execute: 9 | # 10 | # BUILD_DIR=my_build_dir ./build_without_gradle.sh 11 | 12 | set -e 13 | 14 | SCRCPY_DEBUG=false 15 | SCRCPY_VERSION_NAME=1.19 16 | 17 | PLATFORM=${ANDROID_PLATFORM:-30} 18 | BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0} 19 | 20 | BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" 21 | CLASSES_DIR="$BUILD_DIR/classes" 22 | SERVER_DIR=$(dirname "$0") 23 | SERVER_BINARY=scrcpy-server 24 | 25 | echo "Platform: android-$PLATFORM" 26 | echo "Build-tools: $BUILD_TOOLS" 27 | echo "Build dir: $BUILD_DIR" 28 | 29 | rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex 30 | mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" 31 | 32 | << EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java" 33 | package com.genymobile.scrcpy; 34 | 35 | public final class BuildConfig { 36 | public static final boolean DEBUG = $SCRCPY_DEBUG; 37 | public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME"; 38 | } 39 | EOF 40 | 41 | echo "Generating java from aidl..." 42 | cd "$SERVER_DIR/src/main/aidl" 43 | "$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ 44 | android/view/IRotationWatcher.aidl 45 | "$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ 46 | android/content/IOnPrimaryClipChangedListener.aidl 47 | 48 | echo "Compiling java sources..." 49 | cd ../java 50 | javac -bootclasspath "$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" \ 51 | -cp "$CLASSES_DIR" -d "$CLASSES_DIR" -source 1.8 -target 1.8 \ 52 | com/genymobile/scrcpy/*.java \ 53 | com/genymobile/scrcpy/wrappers/*.java 54 | 55 | echo "Dexing..." 56 | cd "$CLASSES_DIR" 57 | "$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ 58 | --output "$BUILD_DIR/classes.dex" \ 59 | android/view/*.class \ 60 | android/content/*.class \ 61 | com/genymobile/scrcpy/*.class \ 62 | com/genymobile/scrcpy/wrappers/*.class 63 | 64 | echo "Archiving..." 65 | cd "$BUILD_DIR" 66 | jar cvf "$SERVER_BINARY" classes.dex 67 | rm -rf classes.dex classes 68 | 69 | echo "Server generated in $BUILD_DIR/$SERVER_BINARY" 70 | -------------------------------------------------------------------------------- /server/meson.build: -------------------------------------------------------------------------------- 1 | # It may be useful to use a prebuilt server, so that no Android SDK is required 2 | # to build. If the 'prebuilt_server' option is set, just copy the file as is. 3 | prebuilt_server = get_option('prebuilt_server') 4 | if prebuilt_server == '' 5 | custom_target('scrcpy-server', 6 | # gradle is responsible for tracking source changes 7 | build_by_default: true, 8 | build_always_stale: true, 9 | output: 'scrcpy-server', 10 | command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], 11 | console: true, 12 | install: true, 13 | install_dir: 'share/scrcpy') 14 | else 15 | if not prebuilt_server.startswith('/') 16 | # relative path needs some trick 17 | prebuilt_server = meson.source_root() + '/' + prebuilt_server 18 | endif 19 | custom_target('scrcpy-server-prebuilt', 20 | input: prebuilt_server, 21 | output: 'scrcpy-server', 22 | command: ['cp', '@INPUT@', '@OUTPUT@'], 23 | install: true, 24 | install_dir: 'share/scrcpy') 25 | endif 26 | -------------------------------------------------------------------------------- /server/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /server/scripts/build-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Wrapper script to invoke gradle from meson 3 | set -e 4 | 5 | # Do not execute gradle when ninja is called as root (it would download the 6 | # whole gradle world in /root/.gradle). 7 | # This is typically useful for calling "sudo ninja install" after a "ninja 8 | # install" 9 | if [[ "$EUID" == 0 ]] 10 | then 11 | echo "(not invoking gradle, since we are root)" >&2 12 | exit 0 13 | fi 14 | 15 | PROJECT_ROOT="$1" 16 | OUTPUT="$2" 17 | BUILDTYPE="$3" 18 | 19 | # gradlew is in the parent of the server directory 20 | GRADLE=${GRADLE:-$PROJECT_ROOT/../gradlew} 21 | 22 | if [[ "$BUILDTYPE" == debug ]] 23 | then 24 | "$GRADLE" -p "$PROJECT_ROOT" assembleDebug 25 | cp "$PROJECT_ROOT/build/outputs/apk/debug/server-debug.apk" "$OUTPUT" 26 | else 27 | "$GRADLE" -p "$PROJECT_ROOT" assembleRelease 28 | cp "$PROJECT_ROOT/build/outputs/apk/release/server-release-unsigned.apk" "$OUTPUT" 29 | fi 30 | -------------------------------------------------------------------------------- /server/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2008, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package android.content; 18 | 19 | /** 20 | * {@hide} 21 | */ 22 | oneway interface IOnPrimaryClipChangedListener { 23 | void dispatchPrimaryClipChanged(); 24 | } 25 | -------------------------------------------------------------------------------- /server/src/main/aidl/android/view/IRotationWatcher.aidl: -------------------------------------------------------------------------------- 1 | /* //device/java/android/android/hardware/ISensorListener.aidl 2 | ** 3 | ** Copyright 2008, The Android Open Source Project 4 | ** 5 | ** Licensed under the Apache License, Version 2.0 (the "License"); 6 | ** you may not use this file except in compliance with the License. 7 | ** You may obtain a copy of the License at 8 | ** 9 | ** http://www.apache.org/licenses/LICENSE-2.0 10 | ** 11 | ** Unless required by applicable law or agreed to in writing, software 12 | ** distributed under the License is distributed on an "AS IS" BASIS, 13 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ** See the License for the specific language governing permissions and 15 | ** limitations under the License. 16 | */ 17 | 18 | package android.view; 19 | 20 | /** 21 | * {@hide} 22 | */ 23 | interface IRotationWatcher { 24 | oneway void onRotationChanged(int rotation); 25 | } 26 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/CodecOption.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class CodecOption { 7 | private String key; 8 | private Object value; 9 | 10 | public CodecOption(String key, Object value) { 11 | this.key = key; 12 | this.value = value; 13 | } 14 | 15 | public String getKey() { 16 | return key; 17 | } 18 | 19 | public Object getValue() { 20 | return value; 21 | } 22 | 23 | public static List parse(String codecOptions) { 24 | if ("-".equals(codecOptions)) { 25 | return null; 26 | } 27 | 28 | List result = new ArrayList<>(); 29 | 30 | boolean escape = false; 31 | StringBuilder buf = new StringBuilder(); 32 | 33 | for (char c : codecOptions.toCharArray()) { 34 | switch (c) { 35 | case '\\': 36 | if (escape) { 37 | buf.append('\\'); 38 | escape = false; 39 | } else { 40 | escape = true; 41 | } 42 | break; 43 | case ',': 44 | if (escape) { 45 | buf.append(','); 46 | escape = false; 47 | } else { 48 | // This comma is a separator between codec options 49 | String codecOption = buf.toString(); 50 | result.add(parseOption(codecOption)); 51 | // Clear buf 52 | buf.setLength(0); 53 | } 54 | break; 55 | default: 56 | buf.append(c); 57 | break; 58 | } 59 | } 60 | 61 | if (buf.length() > 0) { 62 | String codecOption = buf.toString(); 63 | result.add(parseOption(codecOption)); 64 | } 65 | 66 | return result; 67 | } 68 | 69 | private static CodecOption parseOption(String option) { 70 | int equalSignIndex = option.indexOf('='); 71 | if (equalSignIndex == -1) { 72 | throw new IllegalArgumentException("'=' expected"); 73 | } 74 | String keyAndType = option.substring(0, equalSignIndex); 75 | if (keyAndType.length() == 0) { 76 | throw new IllegalArgumentException("Key may not be null"); 77 | } 78 | 79 | String key; 80 | String type; 81 | 82 | int colonIndex = keyAndType.indexOf(':'); 83 | if (colonIndex != -1) { 84 | key = keyAndType.substring(0, colonIndex); 85 | type = keyAndType.substring(colonIndex + 1); 86 | } else { 87 | key = keyAndType; 88 | type = "int"; // assume int by default 89 | } 90 | 91 | Object value; 92 | String valueString = option.substring(equalSignIndex + 1); 93 | switch (type) { 94 | case "int": 95 | value = Integer.parseInt(valueString); 96 | break; 97 | case "long": 98 | value = Long.parseLong(valueString); 99 | break; 100 | case "float": 101 | value = Float.parseFloat(valueString); 102 | break; 103 | case "string": 104 | value = valueString; 105 | break; 106 | default: 107 | throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type); 108 | } 109 | 110 | return new CodecOption(key, value); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Connection.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import com.genymobile.scrcpy.wrappers.ContentProvider; 4 | 5 | import android.os.BatteryManager; 6 | import android.os.Build; 7 | 8 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | 11 | public abstract class Connection implements Device.RotationListener, Device.ClipboardListener { 12 | public interface StreamInvalidateListener { 13 | void onStreamInvalidate(); 14 | } 15 | 16 | protected final ControlMessageReader reader = new ControlMessageReader(); 17 | protected static final int DEVICE_NAME_FIELD_LENGTH = 64; 18 | protected StreamInvalidateListener streamInvalidateListener; 19 | protected Device device; 20 | protected final VideoSettings videoSettings; 21 | protected final Options options; 22 | protected Controller controller; 23 | protected ScreenEncoder screenEncoder; 24 | 25 | abstract void send(ByteBuffer data); 26 | 27 | abstract void sendDeviceMessage(DeviceMessage msg) throws IOException; 28 | 29 | abstract void close() throws Exception; 30 | 31 | abstract boolean hasConnections(); 32 | 33 | public Connection(Options options, VideoSettings videoSettings) { 34 | Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); 35 | this.videoSettings = videoSettings; 36 | this.options = options; 37 | device = new Device(options, videoSettings); 38 | device.setRotationListener(this); 39 | controller = new Controller(device, this); 40 | startDeviceMessageSender(controller.getSender()); 41 | device.setClipboardListener(this); 42 | 43 | boolean mustDisableShowTouchesOnCleanUp = false; 44 | int restoreStayOn = -1; 45 | if (options.getShowTouches() || options.getStayAwake()) { 46 | try (ContentProvider settings = Device.createSettingsProvider()) { 47 | if (options.getShowTouches()) { 48 | String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1"); 49 | // If "show touches" was disabled, it must be disabled back on clean up 50 | mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); 51 | } 52 | 53 | if (options.getStayAwake()) { 54 | int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; 55 | String oldValue = settings.getAndPutValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); 56 | try { 57 | restoreStayOn = Integer.parseInt(oldValue); 58 | if (restoreStayOn == stayOn) { 59 | // No need to restore 60 | restoreStayOn = -1; 61 | } 62 | } catch (NumberFormatException e) { 63 | restoreStayOn = 0; 64 | } 65 | } 66 | } 67 | } 68 | 69 | try { 70 | CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose()); 71 | } catch (IOException e) { 72 | Ln.w("CleanUp.configure() failed:" + e.getMessage()); 73 | } 74 | } 75 | 76 | public boolean setVideoSettings(VideoSettings newSettings) { 77 | if (!videoSettings.equals(newSettings)) { 78 | videoSettings.merge(newSettings); 79 | device.applyNewVideoSetting(videoSettings); 80 | if (this.streamInvalidateListener != null) { 81 | streamInvalidateListener.onStreamInvalidate(); 82 | } 83 | return true; 84 | } 85 | return false; 86 | } 87 | 88 | public void setStreamInvalidateListener(StreamInvalidateListener listener) { 89 | this.streamInvalidateListener = listener; 90 | } 91 | 92 | @Override 93 | public void onRotationChanged(int rotation) { 94 | if (this.streamInvalidateListener != null) { 95 | streamInvalidateListener.onStreamInvalidate(); 96 | } 97 | } 98 | 99 | @Override 100 | public void onClipboardTextChanged(String text) { 101 | controller.getSender().pushClipboardText(text); 102 | } 103 | 104 | 105 | private static void startDeviceMessageSender(final DeviceMessageSender sender) { 106 | new Thread(new Runnable() { 107 | @Override 108 | public void run() { 109 | try { 110 | sender.loop(); 111 | } catch (IOException | InterruptedException e) { 112 | // this is expected on close 113 | Ln.d("Device message sender stopped"); 114 | } 115 | } 116 | }).start(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | public abstract class DeviceMessage { 7 | private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k 8 | public static final int MAX_EVENT_SIZE = 4096; 9 | public static final int TYPE_CLIPBOARD = 0; 10 | public static final int TYPE_PUSH_RESPONSE = 101; 11 | 12 | private int type; 13 | 14 | private DeviceMessage(int type) { 15 | this.type = type; 16 | } 17 | 18 | private static final class ClipboardMessage extends DeviceMessage { 19 | public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes 20 | private byte[] raw; 21 | private int len; 22 | private ClipboardMessage(String text) { 23 | super(TYPE_CLIPBOARD); 24 | this.raw = text.getBytes(StandardCharsets.UTF_8); 25 | this.len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); 26 | } 27 | public void writeToByteArray(byte[] array, int offset) { 28 | ByteBuffer buffer = ByteBuffer.wrap(array, offset, array.length - offset); 29 | buffer.put((byte) this.getType()); 30 | buffer.putInt(len); 31 | buffer.put(raw, 0, len); 32 | } 33 | public int getLen() { 34 | return 5 + len; 35 | } 36 | } 37 | 38 | private static final class FilePushResponseMessage extends DeviceMessage { 39 | private short id; 40 | private int result; 41 | 42 | private FilePushResponseMessage(short id, int result) { 43 | super(TYPE_PUSH_RESPONSE); 44 | this.id = id; 45 | this.result = result; 46 | } 47 | 48 | @Override 49 | public void writeToByteArray(byte[] array, int offset) { 50 | ByteBuffer buffer = ByteBuffer.wrap(array, offset, array.length - offset); 51 | buffer.put((byte) this.getType()); 52 | buffer.putShort(id); 53 | buffer.put((byte) result); 54 | } 55 | 56 | @Override 57 | public int getLen() { 58 | return 4; 59 | } 60 | } 61 | 62 | public static DeviceMessage createClipboard(String text) { 63 | return new ClipboardMessage(text); 64 | } 65 | 66 | public static DeviceMessage createPushResponse(short id, int result) { 67 | return new FilePushResponseMessage(id, result); 68 | } 69 | 70 | public int getType() { 71 | return type; 72 | } 73 | public void writeToByteArray(byte[] array) { 74 | writeToByteArray(array, 0); 75 | } 76 | public byte[] writeToByteArray(int offset) { 77 | byte[] temp = new byte[offset + this.getLen()]; 78 | writeToByteArray(temp, offset); 79 | return temp; 80 | } 81 | public abstract void writeToByteArray(byte[] array, int offset); 82 | public abstract int getLen(); 83 | } 84 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.io.IOException; 4 | 5 | public final class DeviceMessageSender { 6 | 7 | private final Connection connection; 8 | 9 | private String clipboardText; 10 | 11 | public DeviceMessageSender(Connection connection) { 12 | this.connection = connection; 13 | } 14 | 15 | public synchronized void pushClipboardText(String text) { 16 | clipboardText = text; 17 | notify(); 18 | } 19 | 20 | public void loop() throws IOException, InterruptedException { 21 | while (true) { 22 | String text; 23 | synchronized (this) { 24 | while (clipboardText == null) { 25 | wait(); 26 | } 27 | text = clipboardText; 28 | clipboardText = null; 29 | } 30 | DeviceMessage event = DeviceMessage.createClipboard(text); 31 | connection.sendDeviceMessage(event); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | 6 | public class DeviceMessageWriter { 7 | 8 | private final byte[] rawBuffer = new byte[DeviceMessage.MAX_EVENT_SIZE]; 9 | 10 | public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { 11 | msg.writeToByteArray(rawBuffer); 12 | output.write(rawBuffer, 0, msg.getLen()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public final class DisplayInfo { 6 | private final int displayId; 7 | private final Size size; 8 | private final int rotation; 9 | private final int layerStack; 10 | private final int flags; 11 | 12 | public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; 13 | 14 | public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { 15 | this.displayId = displayId; 16 | this.size = size; 17 | this.rotation = rotation; 18 | this.layerStack = layerStack; 19 | this.flags = flags; 20 | } 21 | 22 | public int getDisplayId() { 23 | return displayId; 24 | } 25 | 26 | public Size getSize() { 27 | return size; 28 | } 29 | 30 | public int getRotation() { 31 | return rotation; 32 | } 33 | 34 | public int getLayerStack() { 35 | return layerStack; 36 | } 37 | 38 | public int getFlags() { 39 | return flags; 40 | } 41 | 42 | public byte[] toByteArray() { 43 | ByteBuffer temp = ByteBuffer.allocate(24); 44 | temp.putInt(displayId); 45 | temp.putInt(size.getWidth()); 46 | temp.putInt(size.getHeight()); 47 | temp.putInt(rotation); 48 | temp.putInt(layerStack); 49 | temp.putInt(flags); 50 | temp.rewind(); 51 | return temp.array(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/IO.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.system.ErrnoException; 4 | import android.system.Os; 5 | import android.system.OsConstants; 6 | 7 | import java.io.FileDescriptor; 8 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | 11 | public final class IO { 12 | private IO() { 13 | // not instantiable 14 | } 15 | 16 | public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { 17 | // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so 18 | // count the remaining bytes manually. 19 | // See . 20 | int remaining = from.remaining(); 21 | while (remaining > 0) { 22 | try { 23 | int w = Os.write(fd, from); 24 | if (BuildConfig.DEBUG && w < 0) { 25 | // w should not be negative, since an exception is thrown on error 26 | throw new AssertionError("Os.write() returned a negative value (" + w + ")"); 27 | } 28 | remaining -= w; 29 | } catch (ErrnoException e) { 30 | if (e.errno != OsConstants.EINTR) { 31 | throw new IOException(e); 32 | } 33 | } 34 | } 35 | } 36 | 37 | public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException { 38 | writeFully(fd, ByteBuffer.wrap(buffer, offset, len)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | public class InvalidDisplayIdException extends RuntimeException { 4 | 5 | private final int displayId; 6 | private final int[] availableDisplayIds; 7 | 8 | public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) { 9 | super("There is no display having id " + displayId); 10 | this.displayId = displayId; 11 | this.availableDisplayIds = availableDisplayIds; 12 | } 13 | 14 | public int getDisplayId() { 15 | return displayId; 16 | } 17 | 18 | public int[] getAvailableDisplayIds() { 19 | return availableDisplayIds; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.media.MediaCodecInfo; 4 | 5 | public class InvalidEncoderException extends RuntimeException { 6 | 7 | private final String name; 8 | private final MediaCodecInfo[] availableEncoders; 9 | 10 | public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) { 11 | super("There is no encoder having name '" + name + '"'); 12 | this.name = name; 13 | this.availableEncoders = availableEncoders; 14 | } 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | public MediaCodecInfo[] getAvailableEncoders() { 21 | return availableEncoders; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Ln.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal 7 | * directly). 8 | */ 9 | public final class Ln { 10 | 11 | private static final String TAG = "scrcpy"; 12 | private static final String PREFIX = "[server] "; 13 | 14 | enum Level { 15 | VERBOSE, DEBUG, INFO, WARN, ERROR 16 | } 17 | 18 | private static Level threshold = Level.INFO; 19 | 20 | private Ln() { 21 | // not instantiable 22 | } 23 | 24 | /** 25 | * Initialize the log level. 26 | *

27 | * Must be called before starting any new thread. 28 | * 29 | * @param level the log level 30 | */ 31 | public static void initLogLevel(Level level) { 32 | threshold = level; 33 | } 34 | 35 | public static boolean isEnabled(Level level) { 36 | return level.ordinal() >= threshold.ordinal(); 37 | } 38 | 39 | public static void v(String message) { 40 | if (isEnabled(Level.VERBOSE)) { 41 | Log.v(TAG, message); 42 | System.out.println(PREFIX + "VERBOSE: " + message); 43 | } 44 | } 45 | 46 | public static void d(String message) { 47 | if (isEnabled(Level.DEBUG)) { 48 | Log.d(TAG, message); 49 | System.out.println(PREFIX + "DEBUG: " + message); 50 | } 51 | } 52 | 53 | public static void i(String message) { 54 | if (isEnabled(Level.INFO)) { 55 | Log.i(TAG, message); 56 | System.out.println(PREFIX + "INFO: " + message); 57 | } 58 | } 59 | 60 | public static void w(String message) { 61 | if (isEnabled(Level.WARN)) { 62 | Log.w(TAG, message); 63 | System.out.println(PREFIX + "WARN: " + message); 64 | } 65 | } 66 | 67 | public static void e(String message, Throwable throwable) { 68 | if (isEnabled(Level.ERROR)) { 69 | Log.e(TAG, message, throwable); 70 | System.out.println(PREFIX + "ERROR: " + message); 71 | if (throwable != null) { 72 | throwable.printStackTrace(); 73 | } 74 | } 75 | } 76 | 77 | public static void e(String message) { 78 | e(message, null); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Options.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.graphics.Rect; 4 | 5 | public class Options { 6 | public static final int TYPE_LOCAL_SOCKET = 1; 7 | public static final int TYPE_WEB_SOCKET = 2; 8 | 9 | private Ln.Level logLevel = Ln.Level.ERROR; 10 | private int maxSize; 11 | private int bitRate; 12 | private int maxFps; 13 | private int lockedVideoOrientation; 14 | private boolean tunnelForward = false; 15 | private Rect crop; 16 | private boolean sendFrameMeta; // send PTS so that the client may record properly 17 | private boolean control = true; 18 | private int displayId; 19 | private boolean showTouches = false; 20 | private boolean stayAwake = false; 21 | private String codecOptions; 22 | private String encoderName; 23 | private boolean powerOffScreenOnClose; 24 | private int serverType = TYPE_LOCAL_SOCKET; 25 | private int portNumber = 8886; 26 | private boolean listenOnAllInterfaces = true; 27 | 28 | public Ln.Level getLogLevel() { 29 | return logLevel; 30 | } 31 | 32 | public void setLogLevel(Ln.Level logLevel) { 33 | this.logLevel = logLevel; 34 | } 35 | 36 | public int getMaxSize() { 37 | return maxSize; 38 | } 39 | 40 | public void setMaxSize(int maxSize) { 41 | this.maxSize = (maxSize / 8) * 8; 42 | } 43 | 44 | public int getBitRate() { 45 | return bitRate; 46 | } 47 | 48 | public void setBitRate(int bitRate) { 49 | this.bitRate = bitRate; 50 | } 51 | 52 | public int getMaxFps() { 53 | return maxFps; 54 | } 55 | 56 | public void setMaxFps(int maxFps) { 57 | this.maxFps = maxFps; 58 | } 59 | 60 | public int getLockedVideoOrientation() { 61 | return lockedVideoOrientation; 62 | } 63 | 64 | public void setLockedVideoOrientation(int lockedVideoOrientation) { 65 | this.lockedVideoOrientation = lockedVideoOrientation; 66 | } 67 | 68 | public boolean isTunnelForward() { 69 | return tunnelForward; 70 | } 71 | 72 | public void setTunnelForward(boolean tunnelForward) { 73 | this.tunnelForward = tunnelForward; 74 | } 75 | 76 | public Rect getCrop() { 77 | return crop; 78 | } 79 | 80 | public void setCrop(Rect crop) { 81 | this.crop = crop; 82 | } 83 | 84 | public boolean getSendFrameMeta() { 85 | return sendFrameMeta; 86 | } 87 | 88 | public void setSendFrameMeta(boolean sendFrameMeta) { 89 | this.sendFrameMeta = sendFrameMeta; 90 | } 91 | 92 | public boolean getControl() { 93 | return control; 94 | } 95 | 96 | public void setControl(boolean control) { 97 | this.control = control; 98 | } 99 | 100 | public int getDisplayId() { 101 | return displayId; 102 | } 103 | 104 | public void setDisplayId(int displayId) { 105 | this.displayId = displayId; 106 | } 107 | 108 | public boolean getShowTouches() { 109 | return showTouches; 110 | } 111 | 112 | public void setShowTouches(boolean showTouches) { 113 | this.showTouches = showTouches; 114 | } 115 | 116 | public boolean getStayAwake() { 117 | return stayAwake; 118 | } 119 | 120 | public void setStayAwake(boolean stayAwake) { 121 | this.stayAwake = stayAwake; 122 | } 123 | 124 | public String getCodecOptions() { 125 | return codecOptions; 126 | } 127 | 128 | public void setCodecOptions(String codecOptions) { 129 | this.codecOptions = codecOptions; 130 | } 131 | 132 | public String getEncoderName() { 133 | return encoderName; 134 | } 135 | 136 | public void setEncoderName(String encoderName) { 137 | this.encoderName = encoderName; 138 | } 139 | 140 | public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) { 141 | this.powerOffScreenOnClose = powerOffScreenOnClose; 142 | } 143 | 144 | public boolean getPowerOffScreenOnClose() { 145 | return this.powerOffScreenOnClose; 146 | } 147 | 148 | public int getServerType() { 149 | return serverType; 150 | } 151 | 152 | public void setServerType(int type) { 153 | if (type == TYPE_LOCAL_SOCKET || type == TYPE_WEB_SOCKET) { 154 | this.serverType = type; 155 | } 156 | } 157 | 158 | public void setPortNumber(int portNumber) { 159 | this.portNumber = portNumber; 160 | } 161 | 162 | public int getPortNumber() { 163 | return this.portNumber; 164 | } 165 | 166 | public boolean getListenOnAllInterfaces() { 167 | return this.listenOnAllInterfaces; 168 | } 169 | 170 | public void setListenOnAllInterfaces(boolean value) { 171 | this.listenOnAllInterfaces = value; 172 | } 173 | 174 | @Override 175 | public String toString() { 176 | return "Options{" 177 | + "maxSize=" + maxSize 178 | + ", bitRate=" + bitRate 179 | + ", maxFps=" + maxFps 180 | + ", tunnelForward=" + tunnelForward 181 | + ", crop=" + crop 182 | + ", sendFrameMeta=" + sendFrameMeta 183 | + ", serverType=" + (serverType == TYPE_LOCAL_SOCKET ? "local" : "web") 184 | + ", listenOnAllInterfaces=" + (this.listenOnAllInterfaces ? "true" : "false") 185 | + '}'; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Point.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.util.Objects; 4 | 5 | public class Point { 6 | private final int x; 7 | private final int y; 8 | 9 | public Point(int x, int y) { 10 | this.x = x; 11 | this.y = y; 12 | } 13 | 14 | public int getX() { 15 | return x; 16 | } 17 | 18 | public int getY() { 19 | return y; 20 | } 21 | 22 | @Override 23 | public boolean equals(Object o) { 24 | if (this == o) { 25 | return true; 26 | } 27 | if (o == null || getClass() != o.getClass()) { 28 | return false; 29 | } 30 | Point point = (Point) o; 31 | return x == point.x && y == point.y; 32 | } 33 | 34 | @Override 35 | public int hashCode() { 36 | return Objects.hash(x, y); 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return "Point{" + "x=" + x + ", y=" + y + '}'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Pointer.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | public class Pointer { 4 | 5 | /** 6 | * Pointer id as received from the client. 7 | */ 8 | private final long id; 9 | 10 | /** 11 | * Local pointer id, using the lowest possible values to fill the {@link android.view.MotionEvent.PointerProperties PointerProperties}. 12 | */ 13 | private final int localId; 14 | 15 | private Point point; 16 | private float pressure; 17 | private boolean up; 18 | 19 | public Pointer(long id, int localId) { 20 | this.id = id; 21 | this.localId = localId; 22 | } 23 | 24 | public long getId() { 25 | return id; 26 | } 27 | 28 | public int getLocalId() { 29 | return localId; 30 | } 31 | 32 | public Point getPoint() { 33 | return point; 34 | } 35 | 36 | public void setPoint(Point point) { 37 | this.point = point; 38 | } 39 | 40 | public float getPressure() { 41 | return pressure; 42 | } 43 | 44 | public void setPressure(float pressure) { 45 | this.pressure = pressure; 46 | } 47 | 48 | public boolean isUp() { 49 | return up; 50 | } 51 | 52 | public void setUp(boolean up) { 53 | this.up = up; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/PointersState.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.view.MotionEvent; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class PointersState { 9 | 10 | public static final int MAX_POINTERS = 10; 11 | 12 | private final List pointers = new ArrayList<>(); 13 | 14 | private int indexOf(long id) { 15 | for (int i = 0; i < pointers.size(); ++i) { 16 | Pointer pointer = pointers.get(i); 17 | if (pointer.getId() == id) { 18 | return i; 19 | } 20 | } 21 | return -1; 22 | } 23 | 24 | private boolean isLocalIdAvailable(int localId) { 25 | for (int i = 0; i < pointers.size(); ++i) { 26 | Pointer pointer = pointers.get(i); 27 | if (pointer.getLocalId() == localId) { 28 | return false; 29 | } 30 | } 31 | return true; 32 | } 33 | 34 | private int nextUnusedLocalId() { 35 | for (int localId = 0; localId < MAX_POINTERS; ++localId) { 36 | if (isLocalIdAvailable(localId)) { 37 | return localId; 38 | } 39 | } 40 | return -1; 41 | } 42 | 43 | public Pointer get(int index) { 44 | return pointers.get(index); 45 | } 46 | 47 | public int getPointerIndex(long id) { 48 | int index = indexOf(id); 49 | if (index != -1) { 50 | // already exists, return it 51 | return index; 52 | } 53 | if (pointers.size() >= MAX_POINTERS) { 54 | // it's full 55 | return -1; 56 | } 57 | // id 0 is reserved for mouse events 58 | int localId = nextUnusedLocalId(); 59 | if (localId == -1) { 60 | throw new AssertionError("pointers.size() < maxFingers implies that a local id is available"); 61 | } 62 | Pointer pointer = new Pointer(id, localId); 63 | pointers.add(pointer); 64 | // return the index of the pointer 65 | return pointers.size() - 1; 66 | } 67 | 68 | /** 69 | * Initialize the motion event parameters. 70 | * 71 | * @param props the pointer properties 72 | * @param coords the pointer coordinates 73 | * @return The number of items initialized (the number of pointers). 74 | */ 75 | public int update(MotionEvent.PointerProperties[] props, MotionEvent.PointerCoords[] coords) { 76 | int count = pointers.size(); 77 | for (int i = 0; i < count; ++i) { 78 | Pointer pointer = pointers.get(i); 79 | 80 | // id 0 is reserved for mouse events 81 | props[i].id = pointer.getLocalId(); 82 | 83 | Point point = pointer.getPoint(); 84 | coords[i].x = point.getX(); 85 | coords[i].y = point.getY(); 86 | coords[i].pressure = pointer.getPressure(); 87 | } 88 | cleanUp(); 89 | return count; 90 | } 91 | 92 | /** 93 | * Remove all pointers which are UP. 94 | */ 95 | private void cleanUp() { 96 | for (int i = pointers.size() - 1; i >= 0; --i) { 97 | Pointer pointer = pointers.get(i); 98 | if (pointer.isUp()) { 99 | pointers.remove(i); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Position.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.util.Objects; 4 | 5 | public class Position { 6 | private Point point; 7 | private Size screenSize; 8 | 9 | public Position(Point point, Size screenSize) { 10 | this.point = point; 11 | this.screenSize = screenSize; 12 | } 13 | 14 | public Position(int x, int y, int screenWidth, int screenHeight) { 15 | this(new Point(x, y), new Size(screenWidth, screenHeight)); 16 | } 17 | 18 | public Point getPoint() { 19 | return point; 20 | } 21 | 22 | public Size getScreenSize() { 23 | return screenSize; 24 | } 25 | 26 | public Position rotate(int rotation) { 27 | switch (rotation) { 28 | case 1: 29 | return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate()); 30 | case 2: 31 | return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize); 32 | case 3: 33 | return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate()); 34 | default: 35 | return this; 36 | } 37 | } 38 | 39 | @Override 40 | public boolean equals(Object o) { 41 | if (this == o) { 42 | return true; 43 | } 44 | if (o == null || getClass() != o.getClass()) { 45 | return false; 46 | } 47 | Position position = (Position) o; 48 | return Objects.equals(point, position.point) && Objects.equals(screenSize, position.screenSize); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return Objects.hash(point, screenSize); 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return "Position{" + "point=" + point + ", screenSize=" + screenSize + '}'; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Size.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.graphics.Rect; 4 | 5 | import java.util.Objects; 6 | 7 | public final class Size { 8 | private final int width; 9 | private final int height; 10 | 11 | public Size(int width, int height) { 12 | this.width = width; 13 | this.height = height; 14 | } 15 | 16 | public int getWidth() { 17 | return width; 18 | } 19 | 20 | public int getHeight() { 21 | return height; 22 | } 23 | 24 | public Size rotate() { 25 | return new Size(height, width); 26 | } 27 | 28 | public Rect toRect() { 29 | return new Rect(0, 0, width, height); 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) { 35 | return true; 36 | } 37 | if (o == null || getClass() != o.getClass()) { 38 | return false; 39 | } 40 | Size size = (Size) o; 41 | return width == size.width && height == size.height; 42 | } 43 | 44 | @Override 45 | public int hashCode() { 46 | return Objects.hash(width, height); 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "Size{" + "width=" + width + ", height=" + height + '}'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/StringUtils.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | public final class StringUtils { 4 | private StringUtils() { 5 | // not instantiable 6 | } 7 | 8 | public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { 9 | int len = utf8.length; 10 | if (len <= maxLength) { 11 | return len; 12 | } 13 | len = maxLength; 14 | // see UTF-8 encoding 15 | while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) { 16 | // the next byte is not the start of a new UTF-8 codepoint 17 | // so if we would cut there, the character would be truncated 18 | len--; 19 | } 20 | return len; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Workarounds.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Application; 5 | import android.app.Instrumentation; 6 | import android.content.Context; 7 | import android.content.pm.ApplicationInfo; 8 | import android.os.Looper; 9 | 10 | import java.lang.reflect.Constructor; 11 | import java.lang.reflect.Field; 12 | import java.lang.reflect.Method; 13 | 14 | public final class Workarounds { 15 | private static boolean looperPrepared = false; 16 | private Workarounds() { 17 | // not instantiable 18 | } 19 | 20 | @SuppressWarnings("deprecation") 21 | public static void prepareMainLooper() { 22 | // Some devices internally create a Handler when creating an input Surface, causing an exception: 23 | // "Can't create handler inside thread that has not called Looper.prepare()" 24 | // 25 | // 26 | // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: 27 | // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' 28 | // on a null object reference" 29 | // 30 | if (looperPrepared) { 31 | return; 32 | } 33 | looperPrepared = true; 34 | Looper.prepareMainLooper(); 35 | } 36 | 37 | @SuppressLint("PrivateApi,DiscouragedPrivateApi") 38 | public static void fillAppInfo() { 39 | try { 40 | // ActivityThread activityThread = new ActivityThread(); 41 | Class activityThreadClass = Class.forName("android.app.ActivityThread"); 42 | Constructor activityThreadConstructor = activityThreadClass.getDeclaredConstructor(); 43 | activityThreadConstructor.setAccessible(true); 44 | Object activityThread = activityThreadConstructor.newInstance(); 45 | 46 | // ActivityThread.sCurrentActivityThread = activityThread; 47 | Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); 48 | sCurrentActivityThreadField.setAccessible(true); 49 | sCurrentActivityThreadField.set(null, activityThread); 50 | 51 | // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); 52 | Class appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); 53 | Constructor appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); 54 | appBindDataConstructor.setAccessible(true); 55 | Object appBindData = appBindDataConstructor.newInstance(); 56 | 57 | ApplicationInfo applicationInfo = new ApplicationInfo(); 58 | applicationInfo.packageName = "com.genymobile.scrcpy"; 59 | 60 | // appBindData.appInfo = applicationInfo; 61 | Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); 62 | appInfoField.setAccessible(true); 63 | appInfoField.set(appBindData, applicationInfo); 64 | 65 | // activityThread.mBoundApplication = appBindData; 66 | Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); 67 | mBoundApplicationField.setAccessible(true); 68 | mBoundApplicationField.set(activityThread, appBindData); 69 | 70 | // Context ctx = activityThread.getSystemContext(); 71 | Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext"); 72 | Context ctx = (Context) getSystemContextMethod.invoke(activityThread); 73 | 74 | Application app = Instrumentation.newApplication(Application.class, ctx); 75 | 76 | // activityThread.mInitialApplication = app; 77 | Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); 78 | mInitialApplicationField.setAccessible(true); 79 | mInitialApplicationField.set(activityThread, app); 80 | } catch (Throwable throwable) { 81 | // this is a workaround, so failing is not an error 82 | Ln.d("Could not fill app info: " + throwable.getMessage()); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import com.genymobile.scrcpy.Ln; 4 | 5 | import android.os.Binder; 6 | import android.os.IBinder; 7 | import android.os.IInterface; 8 | 9 | import java.lang.reflect.Field; 10 | import java.lang.reflect.InvocationTargetException; 11 | import java.lang.reflect.Method; 12 | 13 | public class ActivityManager { 14 | 15 | private final IInterface manager; 16 | private Method getContentProviderExternalMethod; 17 | private boolean getContentProviderExternalMethodNewVersion = true; 18 | private Method removeContentProviderExternalMethod; 19 | 20 | public ActivityManager(IInterface manager) { 21 | this.manager = manager; 22 | } 23 | 24 | private Method getGetContentProviderExternalMethod() throws NoSuchMethodException { 25 | if (getContentProviderExternalMethod == null) { 26 | try { 27 | getContentProviderExternalMethod = manager.getClass() 28 | .getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class); 29 | } catch (NoSuchMethodException e) { 30 | // old version 31 | getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class); 32 | getContentProviderExternalMethodNewVersion = false; 33 | } 34 | } 35 | return getContentProviderExternalMethod; 36 | } 37 | 38 | private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException { 39 | if (removeContentProviderExternalMethod == null) { 40 | removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class); 41 | } 42 | return removeContentProviderExternalMethod; 43 | } 44 | 45 | private ContentProvider getContentProviderExternal(String name, IBinder token) { 46 | try { 47 | Method method = getGetContentProviderExternalMethod(); 48 | Object[] args; 49 | if (getContentProviderExternalMethodNewVersion) { 50 | // new version 51 | args = new Object[]{name, ServiceManager.USER_ID, token, null}; 52 | } else { 53 | // old version 54 | args = new Object[]{name, ServiceManager.USER_ID, token}; 55 | } 56 | // ContentProviderHolder providerHolder = getContentProviderExternal(...); 57 | Object providerHolder = method.invoke(manager, args); 58 | if (providerHolder == null) { 59 | return null; 60 | } 61 | // IContentProvider provider = providerHolder.provider; 62 | Field providerField = providerHolder.getClass().getDeclaredField("provider"); 63 | providerField.setAccessible(true); 64 | Object provider = providerField.get(providerHolder); 65 | if (provider == null) { 66 | return null; 67 | } 68 | return new ContentProvider(this, provider, name, token); 69 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) { 70 | Ln.e("Could not invoke method", e); 71 | return null; 72 | } 73 | } 74 | 75 | void removeContentProviderExternal(String name, IBinder token) { 76 | try { 77 | Method method = getRemoveContentProviderExternalMethod(); 78 | method.invoke(manager, name, token); 79 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 80 | Ln.e("Could not invoke method", e); 81 | } 82 | } 83 | 84 | public ContentProvider createSettingsProvider() { 85 | return getContentProviderExternal("settings", new Binder()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import com.genymobile.scrcpy.DisplayInfo; 4 | import com.genymobile.scrcpy.Ln; 5 | import com.genymobile.scrcpy.Size; 6 | 7 | import android.os.IInterface; 8 | import android.hardware.display.VirtualDisplay; 9 | import android.view.Display; 10 | import android.view.Surface; 11 | 12 | import java.lang.reflect.Method; 13 | 14 | public final class DisplayManager { 15 | private final IInterface manager; 16 | private Method createVirtualDisplayMethod; 17 | 18 | public DisplayManager(IInterface manager) { 19 | this.manager = manager; 20 | } 21 | 22 | public DisplayInfo getDisplayInfo(int displayId) { 23 | try { 24 | Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); 25 | if (displayInfo == null) { 26 | return null; 27 | } 28 | Class cls = displayInfo.getClass(); 29 | // width and height already take the rotation into account 30 | int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo); 31 | int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo); 32 | int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); 33 | int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); 34 | int flags = cls.getDeclaredField("flags").getInt(displayInfo); 35 | return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); 36 | } catch (Exception e) { 37 | throw new AssertionError(e); 38 | } 39 | } 40 | 41 | public int[] getDisplayIds() { 42 | Method method; 43 | String methodName = "getDisplayIds"; 44 | try { 45 | method = manager.getClass().getMethod(methodName); 46 | return (int[]) method.invoke(manager); 47 | } catch (NoSuchMethodException e) { 48 | Ln.e("FIXME: Returning only default display."); 49 | Ln.e("See https://github.com/NetrisTV/ws-scrcpy/issues/217"); 50 | return new int[]{Display.DEFAULT_DISPLAY}; 51 | } catch (Exception e) { 52 | throw new AssertionError(e); 53 | } 54 | } 55 | 56 | private Method getCreateVirtualDisplayMethod() throws NoSuchMethodException { 57 | if (createVirtualDisplayMethod == null) { 58 | createVirtualDisplayMethod = android.hardware.display.DisplayManager.class 59 | .getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class); 60 | } 61 | return createVirtualDisplayMethod; 62 | } 63 | 64 | public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface) throws Exception { 65 | Method method = getCreateVirtualDisplayMethod(); 66 | return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import com.genymobile.scrcpy.Ln; 4 | 5 | import android.os.IInterface; 6 | import android.view.InputEvent; 7 | 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.lang.reflect.Method; 10 | 11 | public final class InputManager { 12 | 13 | public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0; 14 | public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; 15 | public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; 16 | 17 | private final IInterface manager; 18 | private Method injectInputEventMethod; 19 | 20 | private static Method setDisplayIdMethod; 21 | 22 | public InputManager(IInterface manager) { 23 | this.manager = manager; 24 | } 25 | 26 | private Method getInjectInputEventMethod() throws NoSuchMethodException { 27 | if (injectInputEventMethod == null) { 28 | injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); 29 | } 30 | return injectInputEventMethod; 31 | } 32 | 33 | public boolean injectInputEvent(InputEvent inputEvent, int mode) { 34 | try { 35 | Method method = getInjectInputEventMethod(); 36 | return (boolean) method.invoke(manager, inputEvent, mode); 37 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 38 | Ln.e("Could not invoke method", e); 39 | return false; 40 | } 41 | } 42 | 43 | private static Method getSetDisplayIdMethod() throws NoSuchMethodException { 44 | if (setDisplayIdMethod == null) { 45 | setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class); 46 | } 47 | return setDisplayIdMethod; 48 | } 49 | 50 | public static boolean setDisplayId(InputEvent inputEvent, int displayId) { 51 | try { 52 | Method method = getSetDisplayIdMethod(); 53 | method.invoke(inputEvent, displayId); 54 | return true; 55 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 56 | Ln.e("Cannot associate a display id to the input event", e); 57 | return false; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import com.genymobile.scrcpy.Ln; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.os.Build; 7 | import android.os.IInterface; 8 | 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.lang.reflect.Method; 11 | 12 | public final class PowerManager { 13 | private final IInterface manager; 14 | private Method isScreenOnMethod; 15 | 16 | public PowerManager(IInterface manager) { 17 | this.manager = manager; 18 | } 19 | 20 | private Method getIsScreenOnMethod() throws NoSuchMethodException { 21 | if (isScreenOnMethod == null) { 22 | @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future 23 | String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; 24 | isScreenOnMethod = manager.getClass().getMethod(methodName); 25 | } 26 | return isScreenOnMethod; 27 | } 28 | 29 | public boolean isScreenOn() { 30 | try { 31 | Method method = getIsScreenOnMethod(); 32 | return (boolean) method.invoke(manager); 33 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 34 | Ln.e("Could not invoke method", e); 35 | return false; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | 7 | import java.lang.reflect.Method; 8 | 9 | @SuppressLint("PrivateApi,DiscouragedPrivateApi") 10 | public final class ServiceManager { 11 | 12 | public static final String PACKAGE_NAME = "com.android.shell"; 13 | public static final int USER_ID = 0; 14 | 15 | private final Method getServiceMethod; 16 | 17 | private WindowManager windowManager; 18 | private DisplayManager displayManager; 19 | private InputManager inputManager; 20 | private PowerManager powerManager; 21 | private StatusBarManager statusBarManager; 22 | private ClipboardManager clipboardManager; 23 | private ActivityManager activityManager; 24 | 25 | public ServiceManager() { 26 | try { 27 | getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); 28 | } catch (Exception e) { 29 | throw new AssertionError(e); 30 | } 31 | } 32 | 33 | private IInterface getService(String service, String type) { 34 | try { 35 | IBinder binder = (IBinder) getServiceMethod.invoke(null, service); 36 | Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); 37 | return (IInterface) asInterfaceMethod.invoke(null, binder); 38 | } catch (Exception e) { 39 | throw new AssertionError(e); 40 | } 41 | } 42 | 43 | public WindowManager getWindowManager() { 44 | if (windowManager == null) { 45 | windowManager = new WindowManager(getService("window", "android.view.IWindowManager")); 46 | } 47 | return windowManager; 48 | } 49 | 50 | public DisplayManager getDisplayManager() { 51 | if (displayManager == null) { 52 | displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager")); 53 | } 54 | return displayManager; 55 | } 56 | 57 | public InputManager getInputManager() { 58 | if (inputManager == null) { 59 | inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager")); 60 | } 61 | return inputManager; 62 | } 63 | 64 | public PowerManager getPowerManager() { 65 | if (powerManager == null) { 66 | powerManager = new PowerManager(getService("power", "android.os.IPowerManager")); 67 | } 68 | return powerManager; 69 | } 70 | 71 | public StatusBarManager getStatusBarManager() { 72 | if (statusBarManager == null) { 73 | statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService")); 74 | } 75 | return statusBarManager; 76 | } 77 | 78 | public ClipboardManager getClipboardManager() { 79 | if (clipboardManager == null) { 80 | IInterface clipboard = getService("clipboard", "android.content.IClipboard"); 81 | if (clipboard == null) { 82 | // Some devices have no clipboard manager 83 | // 84 | // 85 | return null; 86 | } 87 | clipboardManager = new ClipboardManager(clipboard); 88 | } 89 | return clipboardManager; 90 | } 91 | 92 | public ActivityManager getActivityManager() { 93 | if (activityManager == null) { 94 | try { 95 | // On old Android versions, the ActivityManager is not exposed via AIDL, 96 | // so use ActivityManagerNative.getDefault() 97 | Class cls = Class.forName("android.app.ActivityManagerNative"); 98 | Method getDefaultMethod = cls.getDeclaredMethod("getDefault"); 99 | IInterface am = (IInterface) getDefaultMethod.invoke(null); 100 | activityManager = new ActivityManager(am); 101 | } catch (Exception e) { 102 | throw new AssertionError(e); 103 | } 104 | } 105 | 106 | return activityManager; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import com.genymobile.scrcpy.Ln; 4 | 5 | import android.os.IInterface; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Method; 9 | 10 | public class StatusBarManager { 11 | 12 | private final IInterface manager; 13 | private Method expandNotificationsPanelMethod; 14 | private Method expandSettingsPanelMethod; 15 | private boolean expandSettingsPanelMethodNewVersion = true; 16 | private Method collapsePanelsMethod; 17 | 18 | public StatusBarManager(IInterface manager) { 19 | this.manager = manager; 20 | } 21 | 22 | private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException { 23 | if (expandNotificationsPanelMethod == null) { 24 | expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); 25 | } 26 | return expandNotificationsPanelMethod; 27 | } 28 | 29 | private Method getExpandSettingsPanel() throws NoSuchMethodException { 30 | if (expandSettingsPanelMethod == null) { 31 | try { 32 | // Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/ 33 | expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class); 34 | } catch (NoSuchMethodException e) { 35 | // old version 36 | expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel"); 37 | expandSettingsPanelMethodNewVersion = false; 38 | } 39 | } 40 | return expandSettingsPanelMethod; 41 | } 42 | 43 | private Method getCollapsePanelsMethod() throws NoSuchMethodException { 44 | if (collapsePanelsMethod == null) { 45 | collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); 46 | } 47 | return collapsePanelsMethod; 48 | } 49 | 50 | public void expandNotificationsPanel() { 51 | try { 52 | Method method = getExpandNotificationsPanelMethod(); 53 | method.invoke(manager); 54 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 55 | Ln.e("Could not invoke method", e); 56 | } 57 | } 58 | 59 | public void expandSettingsPanel() { 60 | try { 61 | Method method = getExpandSettingsPanel(); 62 | if (expandSettingsPanelMethodNewVersion) { 63 | // new version 64 | method.invoke(manager, (Object) null); 65 | } else { 66 | // old version 67 | method.invoke(manager); 68 | } 69 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 70 | Ln.e("Could not invoke method", e); 71 | } 72 | } 73 | 74 | public void collapsePanels() { 75 | try { 76 | Method method = getCollapsePanelsMethod(); 77 | method.invoke(manager); 78 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 79 | Ln.e("Could not invoke method", e); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import com.genymobile.scrcpy.Ln; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.graphics.Rect; 7 | import android.os.Build; 8 | import android.os.IBinder; 9 | import android.view.Surface; 10 | 11 | import java.lang.reflect.Method; 12 | 13 | @SuppressLint("PrivateApi") 14 | public final class SurfaceControl { 15 | 16 | private static final Class CLASS; 17 | 18 | // see 19 | public static final int POWER_MODE_OFF = 0; 20 | public static final int POWER_MODE_NORMAL = 2; 21 | 22 | static { 23 | try { 24 | CLASS = Class.forName("android.view.SurfaceControl"); 25 | } catch (ClassNotFoundException e) { 26 | throw new AssertionError(e); 27 | } 28 | } 29 | 30 | private static Method getBuiltInDisplayMethod; 31 | private static Method setDisplayPowerModeMethod; 32 | 33 | private SurfaceControl() { 34 | // only static methods 35 | } 36 | 37 | public static void openTransaction() { 38 | try { 39 | CLASS.getMethod("openTransaction").invoke(null); 40 | } catch (Exception e) { 41 | throw new AssertionError(e); 42 | } 43 | } 44 | 45 | public static void closeTransaction() { 46 | try { 47 | CLASS.getMethod("closeTransaction").invoke(null); 48 | } catch (Exception e) { 49 | throw new AssertionError(e); 50 | } 51 | } 52 | 53 | public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) { 54 | try { 55 | CLASS.getMethod("setDisplayProjection", IBinder.class, int.class, Rect.class, Rect.class) 56 | .invoke(null, displayToken, orientation, layerStackRect, displayRect); 57 | } catch (Exception e) { 58 | throw new AssertionError(e); 59 | } 60 | } 61 | 62 | public static void setDisplayLayerStack(IBinder displayToken, int layerStack) { 63 | try { 64 | CLASS.getMethod("setDisplayLayerStack", IBinder.class, int.class).invoke(null, displayToken, layerStack); 65 | } catch (Exception e) { 66 | throw new AssertionError(e); 67 | } 68 | } 69 | 70 | public static void setDisplaySurface(IBinder displayToken, Surface surface) { 71 | try { 72 | CLASS.getMethod("setDisplaySurface", IBinder.class, Surface.class).invoke(null, displayToken, surface); 73 | } catch (Exception e) { 74 | throw new AssertionError(e); 75 | } 76 | } 77 | 78 | public static IBinder createDisplay(String name, boolean secure) throws Exception { 79 | return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure); 80 | } 81 | 82 | private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { 83 | if (getBuiltInDisplayMethod == null) { 84 | // the method signature has changed in Android Q 85 | // 86 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 87 | getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); 88 | } else { 89 | getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); 90 | } 91 | } 92 | return getBuiltInDisplayMethod; 93 | } 94 | 95 | public static IBinder getBuiltInDisplay() { 96 | 97 | try { 98 | Method method = getGetBuiltInDisplayMethod(); 99 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 100 | // call getBuiltInDisplay(0) 101 | return (IBinder) method.invoke(null, 0); 102 | } 103 | 104 | // call getInternalDisplayToken() 105 | return (IBinder) method.invoke(null); 106 | } catch (ReflectiveOperationException e) { 107 | Ln.e("Could not invoke method", e); 108 | return null; 109 | } 110 | } 111 | 112 | private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException { 113 | if (setDisplayPowerModeMethod == null) { 114 | setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); 115 | } 116 | return setDisplayPowerModeMethod; 117 | } 118 | 119 | public static boolean setDisplayPowerMode(IBinder displayToken, int mode) { 120 | try { 121 | Method method = getSetDisplayPowerModeMethod(); 122 | method.invoke(null, displayToken, mode); 123 | return true; 124 | } catch (ReflectiveOperationException e) { 125 | Ln.e("Could not invoke method", e); 126 | return false; 127 | } 128 | } 129 | 130 | public static void destroyDisplay(IBinder displayToken) { 131 | try { 132 | CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken); 133 | } catch (Exception e) { 134 | throw new AssertionError(e); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import com.genymobile.scrcpy.Ln; 4 | 5 | import android.os.IInterface; 6 | import android.view.IRotationWatcher; 7 | 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.lang.reflect.Method; 10 | 11 | public final class WindowManager { 12 | private final IInterface manager; 13 | private Method getRotationMethod; 14 | private Method freezeRotationMethod; 15 | private Method isRotationFrozenMethod; 16 | private Method thawRotationMethod; 17 | private Method removeRotationWatcherMethod; 18 | 19 | public WindowManager(IInterface manager) { 20 | this.manager = manager; 21 | } 22 | 23 | private Method getGetRotationMethod() throws NoSuchMethodException { 24 | if (getRotationMethod == null) { 25 | Class cls = manager.getClass(); 26 | try { 27 | // method changed since this commit: 28 | // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2 29 | getRotationMethod = cls.getMethod("getDefaultDisplayRotation"); 30 | } catch (NoSuchMethodException e) { 31 | // old version 32 | getRotationMethod = cls.getMethod("getRotation"); 33 | } 34 | } 35 | return getRotationMethod; 36 | } 37 | 38 | private Method getFreezeRotationMethod() throws NoSuchMethodException { 39 | if (freezeRotationMethod == null) { 40 | freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); 41 | } 42 | return freezeRotationMethod; 43 | } 44 | 45 | private Method getIsRotationFrozenMethod() throws NoSuchMethodException { 46 | if (isRotationFrozenMethod == null) { 47 | isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); 48 | } 49 | return isRotationFrozenMethod; 50 | } 51 | 52 | private Method getThawRotationMethod() throws NoSuchMethodException { 53 | if (thawRotationMethod == null) { 54 | thawRotationMethod = manager.getClass().getMethod("thawRotation"); 55 | } 56 | return thawRotationMethod; 57 | } 58 | 59 | private Method getRemoveRotationWatcherMethod() throws NoSuchMethodException { 60 | if (removeRotationWatcherMethod == null) { 61 | removeRotationWatcherMethod = manager.getClass().getMethod("removeRotationWatcher"); 62 | } 63 | return removeRotationWatcherMethod; 64 | } 65 | 66 | public int getRotation() { 67 | try { 68 | Method method = getGetRotationMethod(); 69 | return (int) method.invoke(manager); 70 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 71 | Ln.e("Could not invoke method", e); 72 | return 0; 73 | } 74 | } 75 | 76 | public void freezeRotation(int rotation) { 77 | try { 78 | Method method = getFreezeRotationMethod(); 79 | method.invoke(manager, rotation); 80 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 81 | Ln.e("Could not invoke method", e); 82 | } 83 | } 84 | 85 | public boolean isRotationFrozen() { 86 | try { 87 | Method method = getIsRotationFrozenMethod(); 88 | return (boolean) method.invoke(manager); 89 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 90 | Ln.e("Could not invoke method", e); 91 | return false; 92 | } 93 | } 94 | 95 | public void thawRotation() { 96 | try { 97 | Method method = getThawRotationMethod(); 98 | method.invoke(manager); 99 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 100 | Ln.e("Could not invoke method", e); 101 | } 102 | } 103 | 104 | public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { 105 | try { 106 | Class cls = manager.getClass(); 107 | try { 108 | // display parameter added since this commit: 109 | // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 110 | cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); 111 | } catch (NoSuchMethodException e) { 112 | // old version 113 | cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); 114 | } 115 | } catch (Exception e) { 116 | throw new AssertionError(e); 117 | } 118 | } 119 | 120 | public void unregisterRotationWatcher(IRotationWatcher rotationWatcher) { 121 | try { 122 | Method method = getRemoveRotationWatcherMethod(); 123 | method.invoke(manager, rotationWatcher); 124 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 125 | Ln.e("Could not invoke method", e); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.util.List; 7 | 8 | public class CodecOptionsTest { 9 | 10 | @Test 11 | public void testIntegerImplicit() { 12 | List codecOptions = CodecOption.parse("some_key=5"); 13 | 14 | Assert.assertEquals(1, codecOptions.size()); 15 | 16 | CodecOption option = codecOptions.get(0); 17 | Assert.assertEquals("some_key", option.getKey()); 18 | Assert.assertEquals(5, option.getValue()); 19 | } 20 | 21 | @Test 22 | public void testInteger() { 23 | List codecOptions = CodecOption.parse("some_key:int=5"); 24 | 25 | Assert.assertEquals(1, codecOptions.size()); 26 | 27 | CodecOption option = codecOptions.get(0); 28 | Assert.assertEquals("some_key", option.getKey()); 29 | Assert.assertTrue(option.getValue() instanceof Integer); 30 | Assert.assertEquals(5, option.getValue()); 31 | } 32 | 33 | @Test 34 | public void testLong() { 35 | List codecOptions = CodecOption.parse("some_key:long=5"); 36 | 37 | Assert.assertEquals(1, codecOptions.size()); 38 | 39 | CodecOption option = codecOptions.get(0); 40 | Assert.assertEquals("some_key", option.getKey()); 41 | Assert.assertTrue(option.getValue() instanceof Long); 42 | Assert.assertEquals(5L, option.getValue()); 43 | } 44 | 45 | @Test 46 | public void testFloat() { 47 | List codecOptions = CodecOption.parse("some_key:float=4.5"); 48 | 49 | Assert.assertEquals(1, codecOptions.size()); 50 | 51 | CodecOption option = codecOptions.get(0); 52 | Assert.assertEquals("some_key", option.getKey()); 53 | Assert.assertTrue(option.getValue() instanceof Float); 54 | Assert.assertEquals(4.5f, option.getValue()); 55 | } 56 | 57 | @Test 58 | public void testString() { 59 | List codecOptions = CodecOption.parse("some_key:string=some_value"); 60 | 61 | Assert.assertEquals(1, codecOptions.size()); 62 | 63 | CodecOption option = codecOptions.get(0); 64 | Assert.assertEquals("some_key", option.getKey()); 65 | Assert.assertTrue(option.getValue() instanceof String); 66 | Assert.assertEquals("some_value", option.getValue()); 67 | } 68 | 69 | @Test 70 | public void testStringEscaped() { 71 | List codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key"); 72 | 73 | Assert.assertEquals(1, codecOptions.size()); 74 | 75 | CodecOption option = codecOptions.get(0); 76 | Assert.assertEquals("some_key", option.getKey()); 77 | Assert.assertTrue(option.getValue() instanceof String); 78 | Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue()); 79 | } 80 | 81 | @Test 82 | public void testList() { 83 | List codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c"); 84 | 85 | Assert.assertEquals(5, codecOptions.size()); 86 | 87 | CodecOption option; 88 | 89 | option = codecOptions.get(0); 90 | Assert.assertEquals("a", option.getKey()); 91 | Assert.assertTrue(option.getValue() instanceof Integer); 92 | Assert.assertEquals(1, option.getValue()); 93 | 94 | option = codecOptions.get(1); 95 | Assert.assertEquals("b", option.getKey()); 96 | Assert.assertTrue(option.getValue() instanceof Integer); 97 | Assert.assertEquals(2, option.getValue()); 98 | 99 | option = codecOptions.get(2); 100 | Assert.assertEquals("c", option.getKey()); 101 | Assert.assertTrue(option.getValue() instanceof Long); 102 | Assert.assertEquals(3L, option.getValue()); 103 | 104 | option = codecOptions.get(3); 105 | Assert.assertEquals("d", option.getKey()); 106 | Assert.assertTrue(option.getValue() instanceof Float); 107 | Assert.assertEquals(4.5f, option.getValue()); 108 | 109 | option = codecOptions.get(4); 110 | Assert.assertEquals("e", option.getKey()); 111 | Assert.assertTrue(option.getValue() instanceof String); 112 | Assert.assertEquals("a,b=c", option.getValue()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.DataOutputStream; 8 | import java.io.IOException; 9 | import java.nio.charset.StandardCharsets; 10 | 11 | public class DeviceMessageWriterTest { 12 | 13 | @Test 14 | public void testSerializeClipboard() throws IOException { 15 | DeviceMessageWriter writer = new DeviceMessageWriter(); 16 | 17 | String text = "aéûoç"; 18 | byte[] data = text.getBytes(StandardCharsets.UTF_8); 19 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 20 | DataOutputStream dos = new DataOutputStream(bos); 21 | dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); 22 | dos.writeInt(data.length); 23 | dos.write(data); 24 | 25 | byte[] expected = bos.toByteArray(); 26 | 27 | DeviceMessage msg = DeviceMessage.createClipboard(text); 28 | bos = new ByteArrayOutputStream(); 29 | writer.writeTo(msg, bos); 30 | 31 | byte[] actual = bos.toByteArray(); 32 | 33 | Assert.assertArrayEquals(expected, actual); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | 8 | public class StringUtilsTest { 9 | 10 | @Test 11 | public void testUtf8Truncate() { 12 | String s = "aÉbÔc"; 13 | byte[] utf8 = s.getBytes(StandardCharsets.UTF_8); 14 | Assert.assertEquals(7, utf8.length); 15 | 16 | int count; 17 | 18 | count = StringUtils.getUtf8TruncationIndex(utf8, 1); 19 | Assert.assertEquals(1, count); 20 | 21 | count = StringUtils.getUtf8TruncationIndex(utf8, 2); 22 | Assert.assertEquals(1, count); // É is 2 bytes-wide 23 | 24 | count = StringUtils.getUtf8TruncationIndex(utf8, 3); 25 | Assert.assertEquals(3, count); 26 | 27 | count = StringUtils.getUtf8TruncationIndex(utf8, 4); 28 | Assert.assertEquals(4, count); 29 | 30 | count = StringUtils.getUtf8TruncationIndex(utf8, 5); 31 | Assert.assertEquals(4, count); // Ô is 2 bytes-wide 32 | 33 | count = StringUtils.getUtf8TruncationIndex(utf8, 6); 34 | Assert.assertEquals(6, count); 35 | 36 | count = StringUtils.getUtf8TruncationIndex(utf8, 7); 37 | Assert.assertEquals(7, count); 38 | 39 | count = StringUtils.getUtf8TruncationIndex(utf8, 8); 40 | Assert.assertEquals(7, count); // no more chars 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':server' 2 | --------------------------------------------------------------------------------