Compare commits

..
Author SHA1 Message Date
Romain Vimont 9648b7a387 Handle down/up actions for BACK or POWER ON
Pressing right-click generates either BACK or POWER ON, depending on the
current screen state.

Forward DOWN and UP events separately to the device, to generate key
events accordingly.

Ref #1062 <https://github.com/Genymobile/scrcpy/issues/1062>
2020-01-08 22:48:22 +01:00
Romain Vimont 149702753f Handle down/up actions separately for HOME
Pressing middle-click generated both the DOWN and UP keyevent HOME.

Instead, generate DOWN on button pressed and UP on button released.

Ref #1062 <https://github.com/Genymobile/scrcpy/issues/1062>
2020-01-08 22:13:10 +01:00
Romain Vimont 83d48267a7 Accept --max-fps before Android 10
KEY_MAX_FPS_TO_ENCODER existed privately before Android 10:
<https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
2019-12-19 11:52:09 +01:00
Romain Vimont db6252e52b Simplify net.c
The platform-specific code for net.c was implemented in sys/*/net.c.

But the differences are quite limited, so use ifdef-blocks in the single
net.c instead.
2019-12-15 22:04:09 +01:00
Romain Vimont 229eeb24a2 Merge pull request #1002 into dev
Fix utf-8 char path in windows

<https://github.com/Genymobile/scrcpy/pull/1002>
2019-12-15 13:24:17 +01:00
Yu-Chen Lin f9786e5034 Get env in windows correctly
Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-12-14 18:47:54 +01:00
Yu-Chen Lin 78a320a763 Fix utf-8 char path in windows
The file 'E:\安安\scrcpy-win64-v1.12.1-1-g31bd950\scrcpy-server'
exists, however, it will show msg as follow:

    INFO: scrcpy 1.12.1 <https://github.com/Genymobile/scrcpy>
    stat: No such file or directory
    ERROR: 'E:\安安\scrcpy-win64-v1.12.1-1-g31bd950\scrcpy-server' does
    not exist or is not a regular file
    Press any key to continue...

This patch fixes it.

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-12-14 18:47:30 +01:00
Romain Vimont 7d5845196e Fix memory leak on portable builds
The function get_server_path() sometimes returned an owned string,
sometimes a non-owned string.

Always return an allocated (owned) string, and free it after usage.
2019-12-14 18:23:25 +01:00
32 changed files with 278 additions and 359 deletions
+2 -2
View File
@@ -189,7 +189,7 @@ The client uses 4 threads:
recording, recording,
- the **controller** thread, sending _control messages_ to the server, - the **controller** thread, sending _control messages_ to the server,
- the **receiver** thread (managed by the controller), receiving _device - the **receiver** thread (managed by the controller), receiving _device
messages_ from the server. messages_ from the client.
In addition, another thread can be started if necessary to handle APK In addition, another thread can be started if necessary to handle APK
installation or file push requests (via drag&drop on the main window) or to installation or file push requests (via drag&drop on the main window) or to
@@ -214,7 +214,7 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and
rendering frame (with proper synchronization). Thus, it immediatly starts rendering frame (with proper synchronization). Thus, it immediatly starts
to decode a new frame while the main thread renders the last one. to decode a new frame while the main thread renders the last one.
If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw
H.264 packet to the output video file. H.264 packet to the output video file.
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h [stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
+1 -1
View File
@@ -188,7 +188,7 @@
identification within third-party archives. identification within third-party archives.
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2020 Romain Vimont Copyright (C) 2018-2019 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
+1 -1
View File
@@ -475,7 +475,7 @@ _²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상
## 라이선스 ## 라이선스
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2020 Romain Vimont Copyright (C) 2018-2019 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
+6 -47
View File
@@ -37,11 +37,8 @@ control it using keyboard and mouse.
### Linux ### Linux
In Debian (_testing_ and _sid_ for now): On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
it's not that hard.
```
apt install scrcpy
```
A [Snap] package is available: [`scrcpy`][snap-link]. A [Snap] package is available: [`scrcpy`][snap-link].
@@ -59,10 +56,6 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild [Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy [ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
You could also [build the app manually][BUILD] (don't worry, it's not that
hard).
### Windows ### Windows
@@ -77,20 +70,6 @@ For Windows, for simplicity, prebuilt archives with all the dependencies
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win32-v1.12.1.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win32-v1.12.1.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip
It is also available in [Chocolatey]:
[Chocolatey]: https://chocolatey.org/
```bash
choco install scrcpy
```
You need `adb`, accessible from your `PATH`. If you don't have it yet:
```bash
choco install adb
```
You can also [build the app manually][BUILD]. You can also [build the app manually][BUILD].
@@ -158,12 +137,14 @@ scrcpy -b 2M # short version
#### Limit frame rate #### Limit frame rate
On devices with Android >= 10, the capture frame rate can be limited: The capture frame rate can be limited:
```bash ```bash
scrcpy --max-fps 15 scrcpy --max-fps 15
``` ```
This is officially supported since Android 10, but may work on earlier versions.
#### Crop #### Crop
The device screen may be cropped to mirror only part of the screen. The device screen may be cropped to mirror only part of the screen.
@@ -177,21 +158,6 @@ scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
If `--max-size` is also specified, resizing is applied after cropping. If `--max-size` is also specified, resizing is applied after cropping.
#### Lock video orientation
To lock the orientation of the mirroring:
```bash
scrcpy --lock-video-orientation 0 # natural orientation
scrcpy --lock-video-orientation 1 # 90° counterclockwise
scrcpy --lock-video-orientation 2 # 180°
scrcpy --lock-video-orientation 3 # 90° clockwise
```
This affects recording orientation.
### Recording ### Recording
It is possible to record the screen while mirroring: It is possible to record the screen while mirroring:
@@ -250,13 +216,6 @@ scrcpy --serial 0123456789abcdef
scrcpy -s 0123456789abcdef # short version scrcpy -s 0123456789abcdef # short version
``` ```
If the device is connected over TCP/IP:
```bash
scrcpy --serial 192.168.0.1:5555
scrcpy -s 192.168.0.1:5555 # short version
```
You can start several instances of _scrcpy_ for several devices. You can start several instances of _scrcpy_ for several devices.
#### SSH tunnel #### SSH tunnel
@@ -532,7 +491,7 @@ Read the [developers page].
## Licence ## Licence
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2020 Romain Vimont Copyright (C) 2018-2019 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
-8
View File
@@ -76,11 +76,9 @@ cc = meson.get_compiler('c')
if host_machine.system() == 'windows' if host_machine.system() == 'windows'
src += [ 'src/sys/win/command.c' ] src += [ 'src/sys/win/command.c' ]
src += [ 'src/sys/win/net.c' ]
dependencies += cc.find_library('ws2_32') dependencies += cc.find_library('ws2_32')
else else
src += [ 'src/sys/unix/command.c' ] src += [ 'src/sys/unix/command.c' ]
src += [ 'src/sys/unix/net.c' ]
endif endif
conf = configuration_data() conf = configuration_data()
@@ -106,12 +104,6 @@ conf.set('DEFAULT_LOCAL_PORT', '27183')
# overridden by option --max-size # overridden by option --max-size
conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
# the default video orientation
# natural device orientation is 0 and each increment adds 90 degrees
# counterclockwise
# overridden by option --lock-video-orientation
conf.set('DEFAULT_LOCK_VIDEO_ORIENTATION', '-1') # -1: unlocked
# the default video bitrate, in bits/second # the default video bitrate, in bits/second
# overridden by option --bit-rate # overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
+2 -8
View File
@@ -41,15 +41,9 @@ Start in fullscreen.
.B \-h, \-\-help .B \-h, \-\-help
Print this help. Print this help.
.TP
.BI "\-\-lock\-video\-orientation " value
Lock video orientation to \fIvalue\fR. Values are integers in the range [-1..3]. Natural device orientation is 0 and each increment adds 90 degrees counterclockwise.
Default is -1 (unlocked).
.TP .TP
.BI "\-\-max\-fps " value .BI "\-\-max\-fps " value
Limit the framerate of screen capture (only supported on devices with Android >= 10). Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions).
.TP .TP
.BI "\-m, \-\-max\-size " value .BI "\-m, \-\-max\-size " value
@@ -267,7 +261,7 @@ Copyright \(co 2018 Genymobile
Genymobile Genymobile
.UE .UE
Copyright \(co 2018\-2020 Copyright \(co 2018\-2019
.MT rom@rom1v.com .MT rom@rom1v.com
Romain Vimont Romain Vimont
.ME .ME
+43 -71
View File
@@ -41,15 +41,9 @@ scrcpy_print_usage(const char *arg0) {
" -h, --help\n" " -h, --help\n"
" Print this help.\n" " Print this help.\n"
"\n" "\n"
" --lock-video-orientation value\n"
" Lock video orientation to value. Values are integers in the\n"
" range [-1..3]. Natural device orientation is 0 and each\n"
" increment adds 90 degrees counterclockwise.\n"
" Default is %d%s.\n"
"\n"
" --max-fps value\n" " --max-fps value\n"
" Limit the frame rate of screen capture (only supported on\n" " Limit the frame rate of screen capture (officially supported\n"
" devices with Android >= 10).\n" " since Android 10, but may work on earlier versions).\n"
"\n" "\n"
" -m, --max-size value\n" " -m, --max-size value\n"
" Limit both the width and height of the video to value. The\n" " Limit both the width and height of the video to value. The\n"
@@ -199,7 +193,6 @@ scrcpy_print_usage(const char *arg0) {
arg0, arg0,
DEFAULT_BIT_RATE, DEFAULT_BIT_RATE,
DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)",
DEFAULT_LOCK_VIDEO_ORIENTATION, DEFAULT_LOCK_VIDEO_ORIENTATION >= 0 ? "" : " (unlocked)",
DEFAULT_LOCAL_PORT); DEFAULT_LOCAL_PORT);
} }
@@ -266,19 +259,6 @@ parse_max_fps(const char *s, uint16_t *max_fps) {
return true; return true;
} }
static bool
parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) {
long value;
bool ok = parse_integer_arg(s, &value, false, -1, 3,
"lock video orientation");
if (!ok) {
return false;
}
*lock_video_orientation = (int8_t) value;
return true;
}
static bool static bool
parse_window_position(const char *s, int16_t *position) { parse_window_position(const char *s, int16_t *position) {
long value; long value;
@@ -347,54 +327,51 @@ guess_record_format(const char *filename) {
return 0; return 0;
} }
#define OPT_RENDER_EXPIRED_FRAMES 1000 #define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001 #define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002 #define OPT_PUSH_TARGET 1002
#define OPT_ALWAYS_ON_TOP 1003 #define OPT_ALWAYS_ON_TOP 1003
#define OPT_CROP 1004 #define OPT_CROP 1004
#define OPT_RECORD_FORMAT 1005 #define OPT_RECORD_FORMAT 1005
#define OPT_PREFER_TEXT 1006 #define OPT_PREFER_TEXT 1006
#define OPT_WINDOW_X 1007 #define OPT_WINDOW_X 1007
#define OPT_WINDOW_Y 1008 #define OPT_WINDOW_Y 1008
#define OPT_WINDOW_WIDTH 1009 #define OPT_WINDOW_WIDTH 1009
#define OPT_WINDOW_HEIGHT 1010 #define OPT_WINDOW_HEIGHT 1010
#define OPT_WINDOW_BORDERLESS 1011 #define OPT_WINDOW_BORDERLESS 1011
#define OPT_MAX_FPS 1012 #define OPT_MAX_FPS 1012
#define OPT_LOCK_VIDEO_ORIENTATION 1013
bool bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
static const struct option long_options[] = { static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
{"bit-rate", required_argument, NULL, 'b'}, {"bit-rate", required_argument, NULL, 'b'},
{"crop", required_argument, NULL, OPT_CROP}, {"crop", required_argument, NULL, OPT_CROP},
{"fullscreen", no_argument, NULL, 'f'}, {"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"lock-video-orientation", required_argument, NULL, {"max-fps", required_argument, NULL, OPT_MAX_FPS},
OPT_LOCK_VIDEO_ORIENTATION}, {"max-size", required_argument, NULL, 'm'},
{"max-fps", required_argument, NULL, OPT_MAX_FPS}, {"no-control", no_argument, NULL, 'n'},
{"max-size", required_argument, NULL, 'm'}, {"no-display", no_argument, NULL, 'N'},
{"no-control", no_argument, NULL, 'n'}, {"port", required_argument, NULL, 'p'},
{"no-display", no_argument, NULL, 'N'}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET},
{"port", required_argument, NULL, 'p'}, {"record", required_argument, NULL, 'r'},
{"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
{"record", required_argument, NULL, 'r'}, {"render-expired-frames", no_argument, NULL,
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, OPT_RENDER_EXPIRED_FRAMES},
{"render-expired-frames", no_argument, NULL, {"serial", required_argument, NULL, 's'},
OPT_RENDER_EXPIRED_FRAMES}, {"show-touches", no_argument, NULL, 't'},
{"serial", required_argument, NULL, 's'}, {"turn-screen-off", no_argument, NULL, 'S'},
{"show-touches", no_argument, NULL, 't'}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"turn-screen-off", no_argument, NULL, 'S'}, {"version", no_argument, NULL, 'v'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
{"version", no_argument, NULL, 'v'}, {"window-x", required_argument, NULL, OPT_WINDOW_X},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"window-y", required_argument, NULL, OPT_WINDOW_Y},
{"window-x", required_argument, NULL, OPT_WINDOW_X}, {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH},
{"window-y", required_argument, NULL, OPT_WINDOW_Y}, {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT},
{"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, {"window-borderless", no_argument, NULL,
{"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, OPT_WINDOW_BORDERLESS},
{"window-borderless", no_argument, NULL, {NULL, 0, NULL, 0 },
OPT_WINDOW_BORDERLESS},
{NULL, 0, NULL, 0 },
}; };
struct scrcpy_options *opts = &args->opts; struct scrcpy_options *opts = &args->opts;
@@ -440,11 +417,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
return false; return false;
} }
break; break;
case OPT_LOCK_VIDEO_ORIENTATION:
if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) {
return false;
}
break;
case 'n': case 'n':
opts->control = false; opts->control = false;
break; break;
-14
View File
@@ -4,9 +4,6 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "config.h" #include "config.h"
#include "common.h" #include "common.h"
@@ -205,14 +202,3 @@ process_check_success(process_t proc, const char *name) {
} }
return true; return true;
} }
bool
is_regular_file(const char *path) {
struct stat path_stat;
int r = stat(path, &path_stat);
if (r) {
perror("stat");
return false;
}
return S_ISREG(path_stat.st_mode);
}
+2
View File
@@ -75,6 +75,8 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
buf[1] = msg->set_screen_power_mode.mode; buf[1] = msg->set_screen_power_mode.mode;
return 2; return 2;
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
buf[1] = msg->back_or_screen_on.action;
return 2;
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_GET_CLIPBOARD: case CONTROL_MSG_TYPE_GET_CLIPBOARD:
+3
View File
@@ -66,6 +66,9 @@ struct control_msg {
struct { struct {
enum screen_power_mode mode; enum screen_power_mode mode;
} set_screen_power_mode; } set_screen_power_mode;
struct {
enum android_keyevent_action action;
} back_or_screen_on;
}; };
}; };
+28 -18
View File
@@ -99,9 +99,14 @@ action_menu(struct controller *controller, int actions) {
// turn the screen on if it was off, press BACK otherwise // turn the screen on if it was off, press BACK otherwise
static void static void
press_back_or_turn_screen_on(struct controller *controller) { press_back_or_turn_screen_on(struct controller *controller, int action) {
assert(action == ACTION_DOWN || action == ACTION_UP);
struct control_msg msg; struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
msg.back_or_screen_on.action = action == ACTION_DOWN
? AKEY_EVENT_ACTION_DOWN
: AKEY_EVENT_ACTION_UP;
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'press back or turn screen on'"); LOGW("Could not request 'press back or turn screen on'");
@@ -521,31 +526,36 @@ input_manager_process_mouse_button(struct input_manager *im,
// simulated from touch events, so it's a duplicate // simulated from touch events, so it's a duplicate
return; return;
} }
if (event->type == SDL_MOUSEBUTTONDOWN) {
if (control && event->button == SDL_BUTTON_RIGHT) { // double-click on black borders resize to fit the device screen
press_back_or_turn_screen_on(im->controller); if (event->type == SDL_MOUSEBUTTONDOWN
&& event->button == SDL_BUTTON_LEFT
&& event->clicks == 2) {
bool outside = is_outside_device_screen(im, event->x, event->y);
if (outside) {
screen_resize_to_fit(im->screen);
return; return;
} }
if (control && event->button == SDL_BUTTON_MIDDLE) {
action_home(im->controller, ACTION_DOWN | ACTION_UP);
return;
}
// double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
bool outside =
is_outside_device_screen(im, event->x, event->y);
if (outside) {
screen_resize_to_fit(im->screen);
return;
}
}
// otherwise, send the click event to the device
} }
if (!control) { if (!control) {
return; return;
} }
if (event->button == SDL_BUTTON_MIDDLE) {
int action = event->type == SDL_MOUSEBUTTONDOWN ? ACTION_DOWN
: ACTION_UP;
action_home(im->controller, action);
return;
}
if (event->button == SDL_BUTTON_RIGHT) {
int action = event->type == SDL_MOUSEBUTTONDOWN ? ACTION_DOWN
: ACTION_UP;
press_back_or_turn_screen_on(im->controller, action);
return;
}
struct control_msg msg; struct control_msg msg;
if (convert_mouse_button(event, im->screen, &msg)) { if (convert_mouse_button(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) { if (!controller_push_msg(im->controller, &msg)) {
-1
View File
@@ -284,7 +284,6 @@ scrcpy(const struct scrcpy_options *options) {
.max_size = options->max_size, .max_size = options->max_size,
.bit_rate = options->bit_rate, .bit_rate = options->bit_rate,
.max_fps = options->max_fps, .max_fps = options->max_fps,
.lock_video_orientation = options->lock_video_orientation,
.control = options->control, .control = options->control,
}; };
if (!server_start(&server, options->serial, &params)) { if (!server_start(&server, options->serial, &params)) {
-2
View File
@@ -19,7 +19,6 @@ struct scrcpy_options {
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;
int8_t lock_video_orientation;
int16_t window_x; int16_t window_x;
int16_t window_y; int16_t window_y;
uint16_t window_width; uint16_t window_width;
@@ -46,7 +45,6 @@ struct scrcpy_options {
.max_size = DEFAULT_MAX_SIZE, \ .max_size = DEFAULT_MAX_SIZE, \
.bit_rate = DEFAULT_BIT_RATE, \ .bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \ .max_fps = 0, \
.lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \
.window_x = -1, \ .window_x = -1, \
.window_y = -1, \ .window_y = -1, \
.window_width = 0, \ .window_width = 0, \
+31 -8
View File
@@ -6,11 +6,13 @@
#include <libgen.h> #include <libgen.h>
#include <stdio.h> #include <stdio.h>
#include <SDL2/SDL_timer.h> #include <SDL2/SDL_timer.h>
#include <SDL2/SDL_platform.h>
#include "config.h" #include "config.h"
#include "command.h" #include "command.h"
#include "util/log.h" #include "util/log.h"
#include "util/net.h" #include "util/net.h"
#include "util/str_util.h"
#define SOCKET_NAME "scrcpy" #define SOCKET_NAME "scrcpy"
#define SERVER_FILENAME "scrcpy-server" #define SERVER_FILENAME "scrcpy-server"
@@ -18,20 +20,39 @@
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME
#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" #define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
static const char * static char *
get_server_path(void) { get_server_path(void) {
#ifdef __WINDOWS__
const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH");
#else
const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); const char *server_path_env = getenv("SCRCPY_SERVER_PATH");
#endif
if (server_path_env) { if (server_path_env) {
LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env);
// if the envvar is set, use it // if the envvar is set, use it
return server_path_env; #ifdef __WINDOWS__
char *server_path = utf8_from_wide_char(server_path_env);
#else
char *server_path = SDL_strdup(server_path_env);
#endif
if (!server_path) {
LOGE("Could not allocate memory");
return NULL;
}
LOGD("Using SCRCPY_SERVER_PATH: %s", server_path);
return server_path;
} }
#ifndef PORTABLE #ifndef PORTABLE
LOGD("Using server: " DEFAULT_SERVER_PATH); LOGD("Using server: " DEFAULT_SERVER_PATH);
char *server_path = SDL_strdup(DEFAULT_SERVER_PATH);
if (!server_path) {
LOGE("Could not allocate memory");
return NULL;
}
// the absolute path is hardcoded // the absolute path is hardcoded
return DEFAULT_SERVER_PATH; return server_path;
#else #else
// use scrcpy-server in the same directory as the executable // use scrcpy-server in the same directory as the executable
char *executable_path = get_executable_path(); char *executable_path = get_executable_path();
if (!executable_path) { if (!executable_path) {
@@ -67,12 +88,17 @@ get_server_path(void) {
static bool static bool
push_server(const char *serial) { push_server(const char *serial) {
const char *server_path = get_server_path(); char *server_path = get_server_path();
if (!server_path) {
return false;
}
if (!is_regular_file(server_path)) { if (!is_regular_file(server_path)) {
LOGE("'%s' does not exist or is not a regular file\n", server_path); LOGE("'%s' does not exist or is not a regular file\n", server_path);
SDL_free(server_path);
return false; return false;
} }
process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH); process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH);
SDL_free(server_path);
return process_check_success(process, "adb push"); return process_check_success(process, "adb push");
} }
@@ -124,11 +150,9 @@ execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6]; char max_size_string[6];
char bit_rate_string[11]; char bit_rate_string[11];
char max_fps_string[6]; char max_fps_string[6];
char lock_video_orientation_string[3];
sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_fps_string, "%"PRIu16, params->max_fps); sprintf(max_fps_string, "%"PRIu16, params->max_fps);
sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation);
const char *const cmd[] = { const char *const cmd[] = {
"shell", "shell",
"CLASSPATH=" DEVICE_SERVER_PATH, "CLASSPATH=" DEVICE_SERVER_PATH,
@@ -144,7 +168,6 @@ execute_server(struct server *server, const struct server_params *params) {
max_size_string, max_size_string,
bit_rate_string, bit_rate_string,
max_fps_string, max_fps_string,
lock_video_orientation_string,
server->tunnel_forward ? "true" : "false", server->tunnel_forward ? "true" : "false",
params->crop ? params->crop : "-", params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp) "true", // always send frame meta (packet boundaries + timestamp)
-1
View File
@@ -36,7 +36,6 @@ struct server_params {
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;
int8_t lock_video_orientation;
bool control; bool control;
}; };
+12
View File
@@ -15,6 +15,7 @@
#include <signal.h> #include <signal.h>
#include <stdlib.h> #include <stdlib.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
@@ -127,3 +128,14 @@ get_executable_path(void) {
return NULL; return NULL;
#endif #endif
} }
bool
is_regular_file(const char *path) {
struct stat path_stat;
if (stat(path, &path_stat)) {
perror("stat");
return false;
}
return S_ISREG(path_stat.st_mode);
}
-21
View File
@@ -1,21 +0,0 @@
#include "util/net.h"
#include <unistd.h>
#include "config.h"
bool
net_init(void) {
// do nothing
return true;
}
void
net_cleanup(void) {
// do nothing
}
bool
net_close(socket_t socket) {
return !close(socket);
}
+21
View File
@@ -4,6 +4,8 @@
#include "util/log.h" #include "util/log.h"
#include "util/str_util.h" #include "util/str_util.h"
#include <sys/stat.h>
static int static int
build_cmd(char *cmd, size_t len, const char *const argv[]) { build_cmd(char *cmd, size_t len, const char *const argv[]) {
// Windows command-line parsing is WTF: // Windows command-line parsing is WTF:
@@ -90,3 +92,22 @@ get_executable_path(void) {
buf[len] = '\0'; buf[len] = '\0';
return utf8_from_wide_char(buf); return utf8_from_wide_char(buf);
} }
bool
is_regular_file(const char *path) {
wchar_t *wide_path = utf8_to_wide_char(path);
if (!wide_path) {
LOGC("Could not allocate wide char string");
return false;
}
struct _stat path_stat;
int r = _wstat(wide_path, &path_stat);
SDL_free(wide_path);
if (r) {
perror("stat");
return false;
}
return S_ISREG(path_stat.st_mode);
}
-25
View File
@@ -1,25 +0,0 @@
#include "util/net.h"
#include "config.h"
#include "util/log.h"
bool
net_init(void) {
WSADATA wsa;
int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0;
if (res < 0) {
LOGC("WSAStartup failed with error %d", res);
return false;
}
return true;
}
void
net_cleanup(void) {
WSACleanup();
}
bool
net_close(socket_t socket) {
return !closesocket(socket);
}
+30
View File
@@ -1,6 +1,7 @@
#include "net.h" #include "net.h"
#include <stdio.h> #include <stdio.h>
#include <SDL2/SDL_platform.h>
#include "config.h" #include "config.h"
#include "log.h" #include "log.h"
@@ -115,3 +116,32 @@ bool
net_shutdown(socket_t socket, int how) { net_shutdown(socket_t socket, int how) {
return !shutdown(socket, how); return !shutdown(socket, how);
} }
bool
net_init(void) {
#ifdef __WINDOWS__
WSADATA wsa;
int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0;
if (res < 0) {
LOGC("WSAStartup failed with error %d", res);
return false;
}
#endif
return true;
}
void
net_cleanup(void) {
#ifdef __WINDOWS__
WSACleanup();
#endif
}
bool
net_close(socket_t socket) {
#ifdef __WINDOWS__
return !closesocket(socket);
#else
return !close(socket);
#endif
}
-2
View File
@@ -48,7 +48,6 @@ static void test_options(void) {
"--fullscreen", "--fullscreen",
"--max-fps", "30", "--max-fps", "30",
"--max-size", "1024", "--max-size", "1024",
"--lock-video-orientation", "2",
// "--no-control" is not compatible with "--turn-screen-off" // "--no-control" is not compatible with "--turn-screen-off"
// "--no-display" is not compatible with "--fulscreen" // "--no-display" is not compatible with "--fulscreen"
"--port", "1234", "--port", "1234",
@@ -79,7 +78,6 @@ static void test_options(void) {
assert(opts->fullscreen); assert(opts->fullscreen);
assert(opts->max_fps == 30); assert(opts->max_fps == 30);
assert(opts->max_size == 1024); assert(opts->max_size == 1024);
assert(opts->lock_video_orientation == 2);
assert(opts->port == 1234); assert(opts->port == 1234);
assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->push_target, "/sdcard/Movies"));
assert(!strcmp(opts->record_filename, "file")); assert(!strcmp(opts->record_filename, "file"));
+5 -1
View File
@@ -140,14 +140,18 @@ static void test_serialize_inject_scroll_event(void) {
static void test_serialize_back_or_screen_on(void) { static void test_serialize_back_or_screen_on(void) {
struct control_msg msg = { struct control_msg msg = {
.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
.back_or_screen_on = {
.action = AKEY_EVENT_ACTION_UP,
},
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 2);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
AKEY_EVENT_ACTION_UP,
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
@@ -85,6 +85,13 @@ public final class ControlMessage {
return msg; return msg;
} }
public static ControlMessage createBackOrScreenOn(int action) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_BACK_OR_SCREEN_ON;
msg.action = action;
return msg;
}
public static ControlMessage createEmpty(int type) { public static ControlMessage createEmpty(int type) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = type; msg.type = type;
@@ -13,6 +13,7 @@ public class ControlMessageReader {
private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21; private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21;
private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
private static final int BACK_OR_SCREEN_ON_PAYLOAD_LENGTH = 1;
public static final int TEXT_MAX_LENGTH = 300; public static final int TEXT_MAX_LENGTH = 300;
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
@@ -73,6 +74,8 @@ public class ControlMessageReader {
msg = parseSetScreenPowerMode(); msg = parseSetScreenPowerMode();
break; break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
msg = parseBackOrScreenOn();
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_GET_CLIPBOARD:
@@ -164,6 +167,14 @@ public class ControlMessageReader {
return ControlMessage.createSetScreenPowerMode(mode); return ControlMessage.createSetScreenPowerMode(mode);
} }
private ControlMessage parseBackOrScreenOn() {
if (buffer.remaining() < BACK_OR_SCREEN_ON_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
return ControlMessage.createBackOrScreenOn(action);
}
private static Position readPosition(ByteBuffer buffer) { private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt(); int x = buffer.getInt();
int y = buffer.getInt(); int y = buffer.getInt();
@@ -26,6 +26,9 @@ public class Controller {
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
private boolean backPressed;
private boolean powerPressed;
public Controller(Device device, DesktopConnection connection) { public Controller(Device device, DesktopConnection connection) {
this.device = device; this.device = device;
this.connection = connection; this.connection = connection;
@@ -88,7 +91,7 @@ public class Controller {
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
break; break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
pressBackOrTurnScreenOn(); pressBackOrTurnScreenOn(msg.getAction());
break; break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel(); device.expandNotificationPanel();
@@ -223,8 +226,33 @@ public class Controller {
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
} }
private boolean pressBackOrTurnScreenOn() { private boolean pressBackOrTurnScreenOn(int action) {
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; int keycode = -1;
return injectKeycode(keycode); if (action == KeyEvent.ACTION_UP) {
// if BACK or POWER were pressed, this action is the corresponding release event (regardless of the screen state)
if (backPressed) {
keycode = KeyEvent.KEYCODE_BACK;
backPressed = false;
} else if (powerPressed) {
keycode = KeyEvent.KEYCODE_POWER;
powerPressed = false;
}
}
if (keycode == -1) {
keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
}
if (action == KeyEvent.ACTION_DOWN) {
if (keycode == KeyEvent.KEYCODE_BACK) {
backPressed = true;
powerPressed = false;
} else {
backPressed = false;
powerPressed = true;
}
}
return injectKeyEvent(action, keycode, 0, 0);
} }
} }
@@ -22,13 +22,10 @@ public final class Device {
private final ServiceManager serviceManager = new ServiceManager(); private final ServiceManager serviceManager = new ServiceManager();
private final int lockedVideoOrientation;
private ScreenInfo screenInfo; private ScreenInfo screenInfo;
private RotationListener rotationListener; private RotationListener rotationListener;
public Device(Options options) { public Device(Options options) {
lockedVideoOrientation = options.getLockedVideoOrientation();
screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize());
registerRotationWatcher(new IRotationWatcher.Stub() { registerRotationWatcher(new IRotationWatcher.Stub() {
@Override @Override
@@ -51,11 +48,11 @@ public final class Device {
private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { private ScreenInfo computeScreenInfo(Rect crop, int maxSize) {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
int rotation = displayInfo.getRotation(); boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize(); Size deviceSize = displayInfo.getSize();
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
if (crop != null) { if (crop != null) {
if (rotation % 2 != 0) { // 180s preserve dimensions if (rotated) {
// the crop (provided by the user) is expressed in the natural orientation // the crop (provided by the user) is expressed in the natural orientation
crop = flipRect(crop); crop = flipRect(crop);
} }
@@ -67,7 +64,7 @@ public final class Device {
} }
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
return new ScreenInfo(contentRect, videoSize, rotation); return new ScreenInfo(contentRect, videoSize, rotated);
} }
private static String formatCrop(Rect rect) { private static String formatCrop(Rect rect) {
@@ -102,55 +99,22 @@ public final class Device {
return new Size(w, h); return new Size(w, h);
} }
/**
* Return the rotation to apply to the device rotation to get the requested locked video orientation
*
* @param deviceRotation the device rotation
* @return the rotation offset
*/
public int getVideoRotation(int deviceRotation) {
if (lockedVideoOrientation == -1) {
// no offset
return 0;
}
return (deviceRotation + 4 - lockedVideoOrientation) % 4;
}
/**
* Return the rotation to apply to the requested locked video orientation to get the device rotation
* @param deviceRotation the device rotation
* @return the (reverse) rotation offset
*/
private int getReverseVideoRotation(int deviceRotation) {
if (lockedVideoOrientation == -1) {
// no offset
return 0;
}
return (lockedVideoOrientation + 4 - deviceRotation) % 4;
}
public Point getPhysicalPoint(Position position) { public Point getPhysicalPoint(Position position) {
// it hides the field on purpose, to read it with a lock // it hides the field on purpose, to read it with a lock
@SuppressWarnings("checkstyle:HiddenField") @SuppressWarnings("checkstyle:HiddenField")
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
Size videoSize = screenInfo.getVideoSize(); Size videoSize = screenInfo.getVideoSize();
Size clientVideoSize = position.getScreenSize();
int deviceRotation = screenInfo.getRotation();
int reverseVideoRotation = getReverseVideoRotation(deviceRotation);
// reverse the video rotation to apply the events
Position devicePosition = position.rotate(reverseVideoRotation);
Size clientVideoSize = devicePosition.getScreenSize();
if (!videoSize.equals(clientVideoSize)) { if (!videoSize.equals(clientVideoSize)) {
// The client sends a click relative to a video with wrong dimensions, // The client sends a click relative to a video with wrong dimensions,
// the device may have been rotated since the event was generated, so ignore the event // the device may have been rotated since the event was generated, so ignore the event
return null; return null;
} }
Rect contentRect = screenInfo.getContentRect(); Rect contentRect = screenInfo.getContentRect();
Point point = devicePosition.getPoint(); Point point = position.getPoint();
int convertedX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth();
int convertedY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight();
return new Point(convertedX, convertedY); return new Point(scaledX, scaledY);
} }
public static String getDeviceName() { public static String getDeviceName() {
@@ -6,7 +6,6 @@ public class Options {
private int maxSize; private int maxSize;
private int bitRate; private int bitRate;
private int maxFps; private int maxFps;
private int lockedVideoOrientation;
private boolean tunnelForward; private boolean tunnelForward;
private Rect crop; private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean sendFrameMeta; // send PTS so that the client may record properly
@@ -36,14 +35,6 @@ public class Options {
this.maxFps = maxFps; this.maxFps = maxFps;
} }
public int getLockedVideoOrientation() {
return lockedVideoOrientation;
}
public void setLockedVideoOrientation(int lockedVideoOrientation) {
this.lockedVideoOrientation = lockedVideoOrientation;
}
public boolean isTunnelForward() { public boolean isTunnelForward() {
return tunnelForward; return tunnelForward;
} }
@@ -23,19 +23,6 @@ public class Position {
return screenSize; return screenSize;
} }
public Position rotate(int rotation) {
switch (rotation) {
case 1:
return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate());
case 2:
return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize);
case 3:
return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate());
default:
return this;
}
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) { if (this == o) {
@@ -19,6 +19,7 @@ public class ScreenEncoder implements Device.RotationListener {
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
private static final int NO_PTS = -1; private static final int NO_PTS = -1;
@@ -27,21 +28,19 @@ public class ScreenEncoder implements Device.RotationListener {
private int bitRate; private int bitRate;
private int maxFps; private int maxFps;
private int lockedVideoOrientation;
private int iFrameInterval; private int iFrameInterval;
private boolean sendFrameMeta; private boolean sendFrameMeta;
private long ptsOrigin; private long ptsOrigin;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, int iFrameInterval) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) {
this.sendFrameMeta = sendFrameMeta; this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate; this.bitRate = bitRate;
this.maxFps = maxFps; this.maxFps = maxFps;
this.lockedVideoOrientation = lockedVideoOrientation;
this.iFrameInterval = iFrameInterval; this.iFrameInterval = iFrameInterval;
} }
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
this(sendFrameMeta, bitRate, maxFps, lockedVideoOrientation, DEFAULT_I_FRAME_INTERVAL); this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
} }
@Override @Override
@@ -64,14 +63,12 @@ public class ScreenEncoder implements Device.RotationListener {
do { do {
MediaCodec codec = createCodec(); MediaCodec codec = createCodec();
IBinder display = createDisplay(); IBinder display = createDisplay();
ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = device.getScreenInfo().getContentRect();
Rect contentRect = screenInfo.getContentRect(); Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
Rect videoRect = screenInfo.getVideoSize().toRect(); setSize(format, videoRect.width(), videoRect.height());
int videoRotation = device.getVideoRotation(screenInfo.getRotation());
setSize(format, videoRotation, videoRect.width(), videoRect.height());
configure(codec, format); configure(codec, format);
Surface surface = codec.createInputSurface(); Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, videoRotation, contentRect, videoRect); setDisplaySurface(display, surface, contentRect, videoRect);
codec.start(); codec.start();
try { try {
alive = encode(codec, fd); alive = encode(codec, fd);
@@ -154,11 +151,10 @@ public class ScreenEncoder implements Device.RotationListener {
// display the very first frame, and recover from bad quality when no new frames // display the very first frame, and recover from bad quality when no new frames
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
if (maxFps > 0) { if (maxFps > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // The key existed privately before Android 10:
format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps); // <https://android.googlesource.com/platform/frameworks/base/+/625f0aad9f7a259b6881006ad8710adce57d1384%5E%21/>
} else { // <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
Ln.w("Max FPS is only supported since Android 10, the option has been ignored"); format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
}
} }
return format; return format;
} }
@@ -171,21 +167,16 @@ public class ScreenEncoder implements Device.RotationListener {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} }
private static void setSize(MediaFormat format, int orientation, int width, int height) { private static void setSize(MediaFormat format, int width, int height) {
if (orientation % 2 == 0) { format.setInteger(MediaFormat.KEY_WIDTH, width);
format.setInteger(MediaFormat.KEY_WIDTH, width); format.setInteger(MediaFormat.KEY_HEIGHT, height);
format.setInteger(MediaFormat.KEY_HEIGHT, height);
return;
}
format.setInteger(MediaFormat.KEY_WIDTH, height);
format.setInteger(MediaFormat.KEY_HEIGHT, width);
} }
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect) { private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) {
SurfaceControl.openTransaction(); SurfaceControl.openTransaction();
try { try {
SurfaceControl.setDisplaySurface(display, surface); SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, 0); SurfaceControl.setDisplayLayerStack(display, 0);
} finally { } finally {
SurfaceControl.closeTransaction(); SurfaceControl.closeTransaction();
@@ -5,12 +5,12 @@ import android.graphics.Rect;
public final class ScreenInfo { public final class ScreenInfo {
private final Rect contentRect; // device size, possibly cropped private final Rect contentRect; // device size, possibly cropped
private final Size videoSize; private final Size videoSize;
private final int rotation; private final boolean rotated;
public ScreenInfo(Rect contentRect, Size videoSize, int rotation) { public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) {
this.contentRect = contentRect; this.contentRect = contentRect;
this.videoSize = videoSize; this.videoSize = videoSize;
this.rotation = rotation; this.rotated = rotated;
} }
public Rect getContentRect() { public Rect getContentRect() {
@@ -21,25 +21,11 @@ public final class ScreenInfo {
return videoSize; return videoSize;
} }
public int getRotation() { public ScreenInfo withRotation(int rotation) {
return rotation; boolean newRotated = (rotation & 1) != 0;
} if (rotated == newRotated) {
public ScreenInfo withRotation(int newRotation) {
if (newRotation == rotation) {
return this; return this;
} }
// true if changed between portrait and landscape return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated);
boolean orientationChanged = (rotation + newRotation) % 2 != 0;
Rect newContentRect;
Size newVideoSize;
if (orientationChanged) {
newContentRect = Device.flipRect(contentRect);
newVideoSize = videoSize.rotate();
} else {
newContentRect = contentRect;
newVideoSize = videoSize;
}
return new ScreenInfo(newContentRect, newVideoSize, newRotation);
} }
} }
@@ -19,8 +19,7 @@ public final class Server {
final Device device = new Device(options); final Device device = new Device(options);
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
options.getLockedVideoOrientation());
if (options.getControl()) { if (options.getControl()) {
Controller controller = new Controller(device, connection); Controller controller = new Controller(device, connection);
@@ -80,8 +79,8 @@ public final class Server {
"The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")"); "The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")");
} }
if (args.length != 9) { if (args.length != 8) {
throw new IllegalArgumentException("Expecting 9 parameters"); throw new IllegalArgumentException("Expecting 8 parameters");
} }
Options options = new Options(); Options options = new Options();
@@ -95,20 +94,17 @@ public final class Server {
int maxFps = Integer.parseInt(args[3]); int maxFps = Integer.parseInt(args[3]);
options.setMaxFps(maxFps); options.setMaxFps(maxFps);
int lockedVideoOrientation = Integer.parseInt(args[4]);
options.setLockedVideoOrientation(lockedVideoOrientation);
// use "adb forward" instead of "adb tunnel"? (so the server must listen) // use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[5]); boolean tunnelForward = Boolean.parseBoolean(args[4]);
options.setTunnelForward(tunnelForward); options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[6]); Rect crop = parseCrop(args[5]);
options.setCrop(crop); options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[7]); boolean sendFrameMeta = Boolean.parseBoolean(args[6]);
options.setSendFrameMeta(sendFrameMeta); options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[8]); boolean control = Boolean.parseBoolean(args[7]);
options.setControl(control); options.setControl(control);
return options; return options;
@@ -145,6 +145,7 @@ public class ControlMessageReaderTest {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos); DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON);
dos.writeByte(KeyEvent.ACTION_UP);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@@ -152,6 +153,7 @@ public class ControlMessageReaderTest {
ControlMessage event = reader.next(); ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
} }
@Test @Test