Compare commits

..

7 Commits

Author SHA1 Message Date
a58de783c8 Add option to lock video orientation
Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-02-24 22:50:41 +01:00
12e269b9f0 Retrieve screen info once
The method getScreenInfo() is synchronized, and the result may change
between calls.

Call it once and store the result in a local variable.
2020-02-24 21:08:32 +01:00
f903cd376d Documentation rectifications
PR #1151 <https://github.com/Genymobile/scrcpy/pull/1151>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-02-16 16:04:17 +01:00
e8127375ae Add Chocolatey for Windows install
PR #1144 <https://github.com/Genymobile/scrcpy/pull/1144>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-02-15 22:26:30 +01:00
1144f64214 Indicate that -s can also be used for TCP/IP 2020-02-06 18:42:08 +01:00
39356602ed Mention scrcpy Debian package in README 2020-01-19 16:19:51 +01:00
0fb22c3e98 Happy new year 2020! 2020-01-19 16:04:20 +01:00
30 changed files with 406 additions and 522 deletions

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 client. messages_ from the server.
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 its muxes the raw If a [recorder] is present (i.e. `--record` is enabled), then it 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

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-2019 Romain Vimont Copyright (C) 2018-2020 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.

View File

@ -475,7 +475,7 @@ _²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상
## 라이선스 ## 라이선스
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2019 Romain Vimont Copyright (C) 2018-2020 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.

View File

@ -37,8 +37,11 @@ control it using keyboard and mouse.
### Linux ### Linux
On Linux, you typically need to [build the app manually][BUILD]. Don't worry, In Debian (_testing_ and _sid_ for now):
it's not that hard.
```
apt install scrcpy
```
A [Snap] package is available: [`scrcpy`][snap-link]. A [Snap] package is available: [`scrcpy`][snap-link].
@ -56,6 +59,10 @@ 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
@ -70,6 +77,20 @@ 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].
@ -137,14 +158,12 @@ scrcpy -b 2M # short version
#### Limit frame rate #### Limit frame rate
The capture frame rate can be limited: On devices with Android >= 10, 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.
@ -158,6 +177,21 @@ 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:
@ -216,6 +250,13 @@ 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
@ -491,7 +532,7 @@ Read the [developers page].
## Licence ## Licence
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2019 Romain Vimont Copyright (C) 2018-2020 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.

View File

@ -76,9 +76,11 @@ 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()
@ -96,15 +98,20 @@ conf.set_quoted('PREFIX', get_option('prefix'))
# directory as the executable) # directory as the executable)
conf.set('PORTABLE', get_option('portable')) conf.set('PORTABLE', get_option('portable'))
# the default client TCP port range for the "adb reverse" tunnel # the default client TCP port for the "adb reverse" tunnel
# overridden by option --port # overridden by option --port
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183') conf.set('DEFAULT_LOCAL_PORT', '27183')
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
# the default max video size for both dimensions, in pixels # the default max video size for both dimensions, in pixels
# 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

View File

@ -41,9 +41,15 @@ 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 (officially supported since Android 10, but may work on earlier versions). Limit the framerate of screen capture (only supported on devices with Android >= 10).
.TP .TP
.BI "\-m, \-\-max\-size " value .BI "\-m, \-\-max\-size " value
@ -60,10 +66,10 @@ Disable device control (mirror the device in read\-only).
Do not display device (only when screen recording is enabled). Do not display device (only when screen recording is enabled).
.TP .TP
.BI "\-p, \-\-port " port[:port] .BI "\-p, \-\-port " port
Set the TCP port (range) used by the client to listen. Set the TCP port the client listens on.
Default is 27183:27199. Default is 27183.
.TP .TP
.B \-\-prefer\-text .B \-\-prefer\-text
@ -261,7 +267,7 @@ Copyright \(co 2018 Genymobile
Genymobile Genymobile
.UE .UE
Copyright \(co 2018\-2019 Copyright \(co 2018\-2020
.MT rom@rom1v.com .MT rom@rom1v.com
Romain Vimont Romain Vimont
.ME .ME

View File

@ -41,9 +41,15 @@ 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 (officially supported\n" " Limit the frame rate of screen capture (only supported on\n"
" since Android 10, but may work on earlier versions).\n" " devices with Android >= 10).\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"
@ -58,9 +64,9 @@ scrcpy_print_usage(const char *arg0) {
" Do not display device (only when screen recording is\n" " Do not display device (only when screen recording is\n"
" enabled).\n" " enabled).\n"
"\n" "\n"
" -p, --port port[:port]\n" " -p, --port port\n"
" Set the TCP port (range) used by the client to listen.\n" " Set the TCP port the client listens on.\n"
" Default is %d:%d.\n" " Default is %d.\n"
"\n" "\n"
" --prefer-text\n" " --prefer-text\n"
" Inject alpha characters and space as text events instead of\n" " Inject alpha characters and space as text events instead of\n"
@ -193,7 +199,8 @@ 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_LOCAL_PORT_RANGE_FIRST, DEFAULT_LOCAL_PORT_RANGE_LAST); DEFAULT_LOCK_VIDEO_ORIENTATION, DEFAULT_LOCK_VIDEO_ORIENTATION >= 0 ? "" : " (unlocked)",
DEFAULT_LOCAL_PORT);
} }
static bool static bool
@ -221,27 +228,6 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min,
return true; return true;
} }
static size_t
parse_integers_arg(const char *s, size_t max_items, long *out, long min,
long max, const char *name) {
size_t count = parse_integers(s, ':', max_items, out);
if (!count) {
LOGE("Could not parse %s: %s", name, s);
return 0;
}
for (size_t i = 0; i < count; ++i) {
long value = out[i];
if (value < min || value > max) {
LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)",
name, value, min, max);
return 0;
}
}
return count;
}
static bool static bool
parse_bit_rate(const char *s, uint32_t *bit_rate) { parse_bit_rate(const char *s, uint32_t *bit_rate) {
long value; long value;
@ -280,6 +266,19 @@ 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;
@ -307,30 +306,14 @@ parse_window_dimension(const char *s, uint16_t *dimension) {
} }
static bool static bool
parse_port_range(const char *s, struct port_range *port_range) { parse_port(const char *s, uint16_t *port) {
long values[2]; long value;
size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port"); bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port");
if (!count) { if (!ok) {
return false; return false;
} }
uint16_t v0 = (uint16_t) values[0]; *port = (uint16_t) value;
if (count == 1) {
port_range->first = v0;
port_range->last = v0;
return true;
}
assert(count == 2);
uint16_t v1 = (uint16_t) values[1];
if (v0 < v1) {
port_range->first = v0;
port_range->last = v1;
} else {
port_range->first = v1;
port_range->last = v0;
}
return true; return true;
} }
@ -364,51 +347,54 @@ 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'},
{"max-fps", required_argument, NULL, OPT_MAX_FPS}, {"lock-video-orientation", required_argument, NULL,
{"max-size", required_argument, NULL, 'm'}, OPT_LOCK_VIDEO_ORIENTATION},
{"no-control", no_argument, NULL, 'n'}, {"max-fps", required_argument, NULL, OPT_MAX_FPS},
{"no-display", no_argument, NULL, 'N'}, {"max-size", required_argument, NULL, 'm'},
{"port", required_argument, NULL, 'p'}, {"no-control", no_argument, NULL, 'n'},
{"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"no-display", no_argument, NULL, 'N'},
{"record", required_argument, NULL, 'r'}, {"port", required_argument, NULL, 'p'},
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET},
{"render-expired-frames", no_argument, NULL, {"record", required_argument, NULL, 'r'},
OPT_RENDER_EXPIRED_FRAMES}, {"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
{"serial", required_argument, NULL, 's'}, {"render-expired-frames", no_argument, NULL,
{"show-touches", no_argument, NULL, 't'}, OPT_RENDER_EXPIRED_FRAMES},
{"turn-screen-off", no_argument, NULL, 'S'}, {"serial", required_argument, NULL, 's'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"show-touches", no_argument, NULL, 't'},
{"version", no_argument, NULL, 'v'}, {"turn-screen-off", no_argument, NULL, 'S'},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"window-x", required_argument, NULL, OPT_WINDOW_X}, {"version", no_argument, NULL, 'v'},
{"window-y", required_argument, NULL, OPT_WINDOW_Y}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
{"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, {"window-x", required_argument, NULL, OPT_WINDOW_X},
{"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, {"window-y", required_argument, NULL, OPT_WINDOW_Y},
{"window-borderless", no_argument, NULL, {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH},
OPT_WINDOW_BORDERLESS}, {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT},
{NULL, 0, NULL, 0 }, {"window-borderless", no_argument, NULL,
OPT_WINDOW_BORDERLESS},
{NULL, 0, NULL, 0 },
}; };
struct scrcpy_options *opts = &args->opts; struct scrcpy_options *opts = &args->opts;
@ -454,6 +440,11 @@ 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;
@ -461,7 +452,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
opts->display = false; opts->display = false;
break; break;
case 'p': case 'p':
if (!parse_port_range(optarg, &opts->port_range)) { if (!parse_port(optarg, &opts->port)) {
return false; return false;
} }
break; break;

View File

@ -4,6 +4,9 @@
#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"
@ -55,32 +58,6 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
return idx; return idx;
} }
static void
show_adb_installation_msg() {
#ifndef __WINDOWS__
static const struct {
const char *binary;
const char *command;
} pkg_managers[] = {
{"apt", "apt install adb"},
{"apt-get", "apt-get install adb"},
{"brew", "brew cask install android-platform-tools"},
{"dnf", "dnf install android-tools"},
{"emerge", "emerge dev-util/android-tools"},
{"pacman", "pacman -S android-tools"},
};
for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) {
if (cmd_search(pkg_managers[i].binary)) {
LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command);
return;
}
}
#endif
LOGI("You may download and install 'adb' from "
"https://developer.android.com/studio/releases/platform-tools");
}
static void static void
show_adb_err_msg(enum process_result err, const char *const argv[]) { show_adb_err_msg(enum process_result err, const char *const argv[]) {
char buf[512]; char buf[512];
@ -94,7 +71,6 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) {
LOGE("Command not found: %s", buf); LOGE("Command not found: %s", buf);
LOGE("(make 'adb' accessible from your PATH or define its full" LOGE("(make 'adb' accessible from your PATH or define its full"
"path in the ADB environment variable)"); "path in the ADB environment variable)");
show_adb_installation_msg();
break; break;
case PROCESS_SUCCESS: case PROCESS_SUCCESS:
// do nothing // do nothing
@ -229,3 +205,14 @@ 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);
}

View File

@ -43,11 +43,6 @@ enum process_result {
PROCESS_ERROR_MISSING_BINARY, PROCESS_ERROR_MISSING_BINARY,
}; };
#ifndef __WINDOWS__
bool
cmd_search(const char *file);
#endif
enum process_result enum process_result
cmd_execute(const char *const argv[], process_t *process); cmd_execute(const char *const argv[], process_t *process);

View File

@ -27,9 +27,4 @@ struct position {
struct point point; struct point point;
}; };
struct port_range {
uint16_t first;
uint16_t last;
};
#endif #endif

View File

@ -280,10 +280,11 @@ scrcpy(const struct scrcpy_options *options) {
bool record = !!options->record_filename; bool record = !!options->record_filename;
struct server_params params = { struct server_params params = {
.crop = options->crop, .crop = options->crop,
.port_range = options->port_range, .local_port = options->port,
.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)) {

View File

@ -5,7 +5,6 @@
#include <stdint.h> #include <stdint.h>
#include "config.h" #include "config.h"
#include "common.h"
#include "input_manager.h" #include "input_manager.h"
#include "recorder.h" #include "recorder.h"
@ -16,10 +15,11 @@ struct scrcpy_options {
const char *window_title; const char *window_title;
const char *push_target; const char *push_target;
enum recorder_format record_format; enum recorder_format record_format;
struct port_range port_range; uint16_t port;
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;
@ -42,13 +42,11 @@ struct scrcpy_options {
.window_title = NULL, \ .window_title = NULL, \
.push_target = NULL, \ .push_target = NULL, \
.record_format = RECORDER_FORMAT_AUTO, \ .record_format = RECORDER_FORMAT_AUTO, \
.port_range = { \ .port = DEFAULT_LOCAL_PORT, \
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
.last = DEFAULT_LOCAL_PORT_RANGE_LAST, \
}, \
.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, \

View File

@ -6,13 +6,11 @@
#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"
@ -20,39 +18,20 @@
#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 char * static const 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
#ifdef __WINDOWS__ return server_path_env;
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 server_path; return DEFAULT_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) {
@ -88,17 +67,12 @@ get_server_path(void) {
static bool static bool
push_server(const char *serial) { push_server(const char *serial) {
char *server_path = get_server_path(); const 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");
} }
@ -126,6 +100,17 @@ disable_tunnel_forward(const char *serial, uint16_t local_port) {
return process_check_success(process, "adb forward --remove"); return process_check_success(process, "adb forward --remove");
} }
static bool
enable_tunnel(struct server *server) {
if (enable_tunnel_reverse(server->serial, server->local_port)) {
return true;
}
LOGW("'adb reverse' failed, fallback to 'adb forward'");
server->tunnel_forward = true;
return enable_tunnel_forward(server->serial, server->local_port);
}
static bool static bool
disable_tunnel(struct server *server) { disable_tunnel(struct server *server) {
if (server->tunnel_forward) { if (server->tunnel_forward) {
@ -134,108 +119,16 @@ disable_tunnel(struct server *server) {
return disable_tunnel_reverse(server->serial); return disable_tunnel_reverse(server->serial);
} }
static socket_t
listen_on_port(uint16_t port) {
#define IPV4_LOCALHOST 0x7F000001
return net_listen(IPV4_LOCALHOST, port, 1);
}
static bool
enable_tunnel_reverse_any_port(struct server *server,
struct port_range port_range) {
uint16_t port = port_range.first;
for (;;) {
if (!enable_tunnel_reverse(server->serial, port)) {
// the command itself failed, it will fail on any port
return false;
}
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no
// need to try to connect until the server socket is listening on the
// device.
server->server_socket = listen_on_port(port);
if (server->server_socket != INVALID_SOCKET) {
// success
server->local_port = port;
return true;
}
// failure, disable tunnel and try another port
if (!disable_tunnel_reverse(server->serial)) {
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
}
// check before incrementing to avoid overflow on port 65535
if (port < port_range.last) {
LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16,
port, port + 1);
port++;
continue;
}
if (port_range.first == port_range.last) {
LOGE("Could not listen on port %" PRIu16, port_range.first);
} else {
LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16,
port_range.first, port_range.last);
}
return false;
}
}
static bool
enable_tunnel_forward_any_port(struct server *server,
struct port_range port_range) {
server->tunnel_forward = true;
uint16_t port = port_range.first;
for (;;) {
if (enable_tunnel_forward(server->serial, port)) {
// success
server->local_port = port;
return true;
}
if (port < port_range.last) {
LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16,
port, port + 1);
port++;
continue;
}
if (port_range.first == port_range.last) {
LOGE("Could not forward port %" PRIu16, port_range.first);
} else {
LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16,
port_range.first, port_range.last);
}
return false;
}
}
static bool
enable_tunnel_any_port(struct server *server, struct port_range port_range) {
if (enable_tunnel_reverse_any_port(server, port_range)) {
return true;
}
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// "adb forward", so the app socket is the client
LOGW("'adb reverse' failed, fallback to 'adb forward'");
return enable_tunnel_forward_any_port(server, port_range);
}
static process_t static process_t
execute_server(struct server *server, const struct server_params *params) { 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,
@ -251,6 +144,7 @@ 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)
@ -270,6 +164,13 @@ execute_server(struct server *server, const struct server_params *params) {
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
} }
#define IPV4_LOCALHOST 0x7F000001
static socket_t
listen_on_port(uint16_t port) {
return net_listen(IPV4_LOCALHOST, port, 1);
}
static socket_t static socket_t
connect_and_read_byte(uint16_t port) { connect_and_read_byte(uint16_t port) {
socket_t socket = net_connect(IPV4_LOCALHOST, port); socket_t socket = net_connect(IPV4_LOCALHOST, port);
@ -323,7 +224,7 @@ server_init(struct server *server) {
bool bool
server_start(struct server *server, const char *serial, server_start(struct server *server, const char *serial,
const struct server_params *params) { const struct server_params *params) {
server->port_range = params->port_range; server->local_port = params->local_port;
if (serial) { if (serial) {
server->serial = SDL_strdup(serial); server->serial = SDL_strdup(serial);
@ -337,11 +238,30 @@ server_start(struct server *server, const char *serial,
return false; return false;
} }
if (!enable_tunnel_any_port(server, params->port_range)) { if (!enable_tunnel(server)) {
SDL_free(server->serial); SDL_free(server->serial);
return false; return false;
} }
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// "adb forward", so the app socket is the client
if (!server->tunnel_forward) {
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no
// need to try to connect until the server socket is listening on the
// device.
server->server_socket = listen_on_port(params->local_port);
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, params->local_port);
disable_tunnel(server);
SDL_free(server->serial);
return false;
}
}
// server will connect to our server socket // server will connect to our server socket
server->process = execute_server(server, params); server->process = execute_server(server, params);

View File

@ -6,7 +6,6 @@
#include "config.h" #include "config.h"
#include "command.h" #include "command.h"
#include "common.h"
#include "util/net.h" #include "util/net.h"
struct server { struct server {
@ -15,33 +14,29 @@ struct server {
socket_t server_socket; // only used if !tunnel_forward socket_t server_socket; // only used if !tunnel_forward
socket_t video_socket; socket_t video_socket;
socket_t control_socket; socket_t control_socket;
struct port_range port_range; uint16_t local_port;
uint16_t local_port; // selected from port_range
bool tunnel_enabled; bool tunnel_enabled;
bool tunnel_forward; // use "adb forward" instead of "adb reverse" bool tunnel_forward; // use "adb forward" instead of "adb reverse"
}; };
#define SERVER_INITIALIZER { \ #define SERVER_INITIALIZER { \
.serial = NULL, \ .serial = NULL, \
.process = PROCESS_NONE, \ .process = PROCESS_NONE, \
.server_socket = INVALID_SOCKET, \ .server_socket = INVALID_SOCKET, \
.video_socket = INVALID_SOCKET, \ .video_socket = INVALID_SOCKET, \
.control_socket = INVALID_SOCKET, \ .control_socket = INVALID_SOCKET, \
.port_range = { \ .local_port = 0, \
.first = 0, \ .tunnel_enabled = false, \
.last = 0, \ .tunnel_forward = false, \
}, \
.local_port = 0, \
.tunnel_enabled = false, \
.tunnel_forward = false, \
} }
struct server_params { struct server_params {
const char *crop; const char *crop;
struct port_range port_range; uint16_t local_port;
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;
}; };

View File

@ -14,50 +14,12 @@
#include <limits.h> #include <limits.h>
#include <signal.h> #include <signal.h>
#include <stdlib.h> #include <stdlib.h>
#include <sys/stat.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>
#include "util/log.h" #include "util/log.h"
bool
cmd_search(const char *file) {
char *path = getenv("PATH");
if (!path)
return false;
path = strdup(path);
if (!path)
return false;
bool ret = false;
size_t file_len = strlen(file);
char *saveptr;
for (char *dir = strtok_r(path, ":", &saveptr); dir;
dir = strtok_r(NULL, ":", &saveptr)) {
size_t dir_len = strlen(dir);
char *fullpath = malloc(dir_len + file_len + 2);
if (!fullpath)
continue;
memcpy(fullpath, dir, dir_len);
fullpath[dir_len] = '/';
memcpy(fullpath + dir_len + 1, file, file_len + 1);
struct stat sb;
bool fullpath_executable = stat(fullpath, &sb) == 0 &&
sb.st_mode & S_IXUSR;
free(fullpath);
if (fullpath_executable) {
ret = true;
break;
}
}
free(path);
return ret;
}
enum process_result enum process_result
cmd_execute(const char *const argv[], pid_t *pid) { cmd_execute(const char *const argv[], pid_t *pid) {
int fd[2]; int fd[2];
@ -165,14 +127,3 @@ 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
app/src/sys/unix/net.c Normal file
View File

@ -0,0 +1,21 @@
#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);
}

View File

@ -1,7 +1,5 @@
#include "command.h" #include "command.h"
#include <sys/stat.h>
#include "config.h" #include "config.h"
#include "util/log.h" #include "util/log.h"
#include "util/str_util.h" #include "util/str_util.h"
@ -92,22 +90,3 @@ 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
app/src/sys/win/net.c Normal file
View File

@ -0,0 +1,25 @@
#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);
}

View File

@ -1,7 +1,6 @@
#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"
@ -116,32 +115,3 @@ 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
}

View File

@ -81,35 +81,6 @@ parse_integer(const char *s, long *out) {
return true; return true;
} }
size_t
parse_integers(const char *s, const char sep, size_t max_items, long *out) {
size_t count = 0;
char *endptr;
do {
errno = 0;
long value = strtol(s, &endptr, 0);
if (errno == ERANGE) {
return 0;
}
if (endptr == s || (*endptr != sep && *endptr != '\0')) {
return 0;
}
out[count++] = value;
if (*endptr == sep) {
if (count >= max_items) {
// max items already reached, could not accept a new item
return 0;
}
// parse the next token during the next iteration
s = endptr + 1;
}
} while (*endptr != '\0');
return count;
}
bool bool
parse_integer_with_suffix(const char *s, long *out) { parse_integer_with_suffix(const char *s, long *out) {
char *endptr; char *endptr;

View File

@ -31,11 +31,6 @@ strquote(const char *src);
bool bool
parse_integer(const char *s, long *out); parse_integer(const char *s, long *out);
// parse s as integers separated by sep (for example '1234:2000')
// returns the number of integers on success, 0 on failure
size_t
parse_integers(const char *s, const char sep, size_t max_items, long *out);
// parse s as an integer into value // parse s as an integer into value
// like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as // like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as
// suffix // suffix

View File

@ -48,9 +48,10 @@ 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:1236", "--port", "1234",
"--push-target", "/sdcard/Movies", "--push-target", "/sdcard/Movies",
"--record", "file", "--record", "file",
"--record-format", "mkv", "--record-format", "mkv",
@ -78,8 +79,8 @@ 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->port_range.first == 1234); assert(opts->lock_video_orientation == 2);
assert(opts->port_range.last == 1236); 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"));
assert(opts->record_format == RECORDER_FORMAT_MKV); assert(opts->record_format == RECORDER_FORMAT_MKV);

View File

@ -187,55 +187,6 @@ static void test_parse_integer(void) {
assert(!ok); // out-of-range assert(!ok); // out-of-range
} }
static void test_parse_integers(void) {
long values[5];
size_t count = parse_integers("1234", ':', 5, values);
assert(count == 1);
assert(values[0] == 1234);
count = parse_integers("1234:5678", ':', 5, values);
assert(count == 2);
assert(values[0] == 1234);
assert(values[1] == 5678);
count = parse_integers("1234:5678", ':', 2, values);
assert(count == 2);
assert(values[0] == 1234);
assert(values[1] == 5678);
count = parse_integers("1234:-5678", ':', 2, values);
assert(count == 2);
assert(values[0] == 1234);
assert(values[1] == -5678);
count = parse_integers("1:2:3:4:5", ':', 5, values);
assert(count == 5);
assert(values[0] == 1);
assert(values[1] == 2);
assert(values[2] == 3);
assert(values[3] == 4);
assert(values[4] == 5);
count = parse_integers("1234:5678", ':', 1, values);
assert(count == 0); // max_items == 1
count = parse_integers("1:2:3:4:5", ':', 3, values);
assert(count == 0); // max_items == 3
count = parse_integers(":1234", ':', 5, values);
assert(count == 0); // invalid
count = parse_integers("1234:", ':', 5, values);
assert(count == 0); // invalid
count = parse_integers("1234:", ':', 1, values);
assert(count == 0); // invalid, even when max_items == 1
count = parse_integers("1234::5678", ':', 5, values);
assert(count == 0); // invalid
}
static void test_parse_integer_with_suffix(void) { static void test_parse_integer_with_suffix(void) {
long value; long value;
bool ok = parse_integer_with_suffix("1234", &value); bool ok = parse_integer_with_suffix("1234", &value);
@ -298,7 +249,6 @@ int main(void) {
test_strquote(); test_strquote();
test_utf8_truncate(); test_utf8_truncate();
test_parse_integer(); test_parse_integer();
test_parse_integers();
test_parse_integer_with_suffix(); test_parse_integer_with_suffix();
return 0; return 0;
} }

View File

@ -22,10 +22,13 @@ 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
@ -48,11 +51,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();
boolean rotated = (displayInfo.getRotation() & 1) != 0; int rotation = displayInfo.getRotation();
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 (rotated) { if (rotation % 2 != 0) { // 180s preserve dimensions
// 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);
} }
@ -64,7 +67,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, rotated); return new ScreenInfo(contentRect, videoSize, rotation);
} }
private static String formatCrop(Rect rect) { private static String formatCrop(Rect rect) {
@ -99,22 +102,55 @@ 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 = position.getPoint(); Point point = devicePosition.getPoint();
int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); int convertedX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth();
int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); int convertedY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight();
return new Point(scaledX, scaledY); return new Point(convertedX, convertedY);
} }
public static String getDeviceName() { public static String getDeviceName() {

View File

@ -6,6 +6,7 @@ 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
@ -35,6 +36,14 @@ 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;
} }

View File

@ -23,6 +23,19 @@ 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) {

View File

@ -19,7 +19,6 @@ 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;
@ -28,19 +27,21 @@ 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 iFrameInterval) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, 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) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation) {
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); this(sendFrameMeta, bitRate, maxFps, lockedVideoOrientation, DEFAULT_I_FRAME_INTERVAL);
} }
@Override @Override
@ -63,12 +64,14 @@ public class ScreenEncoder implements Device.RotationListener {
do { do {
MediaCodec codec = createCodec(); MediaCodec codec = createCodec();
IBinder display = createDisplay(); IBinder display = createDisplay();
Rect contentRect = device.getScreenInfo().getContentRect(); ScreenInfo screenInfo = device.getScreenInfo();
Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); Rect contentRect = screenInfo.getContentRect();
setSize(format, videoRect.width(), videoRect.height()); Rect videoRect = screenInfo.getVideoSize().toRect();
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, contentRect, videoRect); setDisplaySurface(display, surface, videoRotation, contentRect, videoRect);
codec.start(); codec.start();
try { try {
alive = encode(codec, fd); alive = encode(codec, fd);
@ -151,10 +154,11 @@ 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) {
// The key existed privately before Android 10: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// <https://android.googlesource.com/platform/frameworks/base/+/625f0aad9f7a259b6881006ad8710adce57d1384%5E%21/> format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps);
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437> } else {
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); Ln.w("Max FPS is only supported since Android 10, the option has been ignored");
}
} }
return format; return format;
} }
@ -167,16 +171,21 @@ 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 width, int height) { private static void setSize(MediaFormat format, int orientation, int width, int height) {
format.setInteger(MediaFormat.KEY_WIDTH, width); if (orientation % 2 == 0) {
format.setInteger(MediaFormat.KEY_HEIGHT, height); format.setInteger(MediaFormat.KEY_WIDTH, width);
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, Rect deviceRect, Rect displayRect) { private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect) {
SurfaceControl.openTransaction(); SurfaceControl.openTransaction();
try { try {
SurfaceControl.setDisplaySurface(display, surface); SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, 0); SurfaceControl.setDisplayLayerStack(display, 0);
} finally { } finally {
SurfaceControl.closeTransaction(); SurfaceControl.closeTransaction();

View File

@ -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 boolean rotated; private final int rotation;
public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { public ScreenInfo(Rect contentRect, Size videoSize, int rotation) {
this.contentRect = contentRect; this.contentRect = contentRect;
this.videoSize = videoSize; this.videoSize = videoSize;
this.rotated = rotated; this.rotation = rotation;
} }
public Rect getContentRect() { public Rect getContentRect() {
@ -21,11 +21,25 @@ public final class ScreenInfo {
return videoSize; return videoSize;
} }
public ScreenInfo withRotation(int rotation) { public int getRotation() {
boolean newRotated = (rotation & 1) != 0; return rotation;
if (rotated == newRotated) { }
public ScreenInfo withRotation(int newRotation) {
if (newRotation == rotation) {
return this; return this;
} }
return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated); // true if changed between portrait and landscape
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);
} }
} }

View File

@ -19,7 +19,8 @@ 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);
@ -79,8 +80,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 != 8) { if (args.length != 9) {
throw new IllegalArgumentException("Expecting 8 parameters"); throw new IllegalArgumentException("Expecting 9 parameters");
} }
Options options = new Options(); Options options = new Options();
@ -94,17 +95,20 @@ 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[4]); boolean tunnelForward = Boolean.parseBoolean(args[5]);
options.setTunnelForward(tunnelForward); options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[5]); Rect crop = parseCrop(args[6]);
options.setCrop(crop); options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[6]); boolean sendFrameMeta = Boolean.parseBoolean(args[7]);
options.setSendFrameMeta(sendFrameMeta); options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[7]); boolean control = Boolean.parseBoolean(args[8]);
options.setControl(control); options.setControl(control);
return options; return options;

View File

@ -73,7 +73,7 @@ public final class Workarounds {
mInitialApplicationField.set(activityThread, app); mInitialApplicationField.set(activityThread, app);
} catch (Throwable throwable) { } catch (Throwable throwable) {
// this is a workaround, so failing is not an error // this is a workaround, so failing is not an error
Ln.d("Could not fill app info: " + throwable.getMessage()); Ln.w("Could not fill app info: " + throwable.getMessage());
} }
} }
} }