Compare commits
53 Commits
traits.7
...
server_thr
Author | SHA1 | Date | |
---|---|---|---|
c37d455fa2 | |||
6e8df74c41 | |||
52f5c6d4c1 | |||
efb531943d | |||
4dcda82582 | |||
5369c4f754 | |||
233f8e6cc4 | |||
9a7d351d67 | |||
d00ee640c0 | |||
ae6ec7a194 | |||
84f17fdeab | |||
1cde68a1fa | |||
45e7280148 | |||
41a0383d7c | |||
d39161f753 | |||
5af9d0ee0f | |||
fd0dc6c0cd | |||
151bc16644 | |||
ffc00210e9 | |||
243854a408 | |||
8b90dc61b9 | |||
2a5dfc1c17 | |||
e3fccc5a5e | |||
0541f1bff2 | |||
0272e6dc77 | |||
2a94a2b119 | |||
e91acdb0c4 | |||
6f5ad21f57 | |||
08b3086ffc | |||
deab7da761 | |||
f7a1b67d66 | |||
cbed38799e | |||
5beb7d6c02 | |||
5980183a33 | |||
fe8de893ca | |||
a974483c15 | |||
1b072a24c4 | |||
08f1fd46c8 | |||
2ddf760c09 | |||
5d9e96dc4e | |||
de9b79ec2d | |||
55806e7d31 | |||
21b590b766 | |||
d7e6589677 | |||
b4ee9f27ce | |||
6fa63cf6f8 | |||
50eecdab28 | |||
9576283907 | |||
66c581851f | |||
bb4614d558 | |||
aaf7875d92 | |||
b9c3f65fd8 | |||
d0739911a3 |
82
README.md
82
README.md
@ -198,6 +198,7 @@ If `--max-size` is also specified, resizing is applied after cropping.
|
||||
To lock the orientation of the mirroring:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation # initial (current) orientation
|
||||
scrcpy --lock-video-orientation 0 # natural orientation
|
||||
scrcpy --lock-video-orientation 1 # 90° counterclockwise
|
||||
scrcpy --lock-video-orientation 2 # 180°
|
||||
@ -225,7 +226,9 @@ error will give the available encoders:
|
||||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### Recording
|
||||
### Capture
|
||||
|
||||
#### Recording
|
||||
|
||||
It is possible to record the screen while mirroring:
|
||||
|
||||
@ -249,6 +252,58 @@ variation] does not impact the recorded file.
|
||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
|
||||
#### v4l2loopback
|
||||
|
||||
On Linux, it is possible to send the video stream to a v4l2 loopback device, so
|
||||
that the Android device can be opened like a webcam by any v4l2-capable tool.
|
||||
|
||||
The module `v4l2loopback` must be installed:
|
||||
|
||||
```bash
|
||||
sudo apt install v4l2loopback-dkms
|
||||
```
|
||||
|
||||
To create a v4l2 device:
|
||||
|
||||
```bash
|
||||
sudo modprobe v4l2loopback
|
||||
```
|
||||
|
||||
This will create a new video device in `/dev/videoN`, where `N` is an integer
|
||||
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available
|
||||
to create several devices or devices with specific IDs).
|
||||
|
||||
To list the enabled devices:
|
||||
|
||||
```bash
|
||||
# requires v4l-utils package
|
||||
v4l2-ctl --list-devices
|
||||
|
||||
# simple but might be sufficient
|
||||
ls /dev/video*
|
||||
```
|
||||
|
||||
To start scrcpy using a v4l2 sink:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-sink=/dev/videoN
|
||||
scrcpy --v4l2-sink=/dev/videoN -N # --no-display to disable mirroring window
|
||||
```
|
||||
|
||||
(replace `N` by the device ID, check with `ls /dev/video*`)
|
||||
|
||||
Once enabled, you can open your video stream with a v4l2-capable tool:
|
||||
|
||||
```bash
|
||||
ffplay -i /dev/videoN
|
||||
vlc v4l2:///dev/videoN # VLC might add some buffering delay
|
||||
```
|
||||
|
||||
For example, you could capture the video within [OBS].
|
||||
|
||||
[OBS]: https://obsproject.com/fr
|
||||
|
||||
|
||||
### Connection
|
||||
|
||||
#### Wireless
|
||||
@ -686,10 +741,10 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
||||
| Rotate display left | <kbd>MOD</kbd>+<kbd>←</kbd> _(left)_
|
||||
| Rotate display right | <kbd>MOD</kbd>+<kbd>→</kbd> _(right)_
|
||||
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-click¹_
|
||||
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
|
||||
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
|
||||
| Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Right-click²_
|
||||
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd>
|
||||
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd> \| _4th-click³_
|
||||
| Click on `MENU` (unlock screen) | <kbd>MOD</kbd>+<kbd>m</kbd>
|
||||
| Click on `VOLUME_UP` | <kbd>MOD</kbd>+<kbd>↑</kbd> _(up)_
|
||||
| Click on `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(down)_
|
||||
@ -698,18 +753,27 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
||||
| Turn device screen off (keep mirroring) | <kbd>MOD</kbd>+<kbd>o</kbd>
|
||||
| Turn device screen on | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
|
||||
| Rotate device screen | <kbd>MOD</kbd>+<kbd>r</kbd>
|
||||
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd>
|
||||
| Collapse notification panel | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
|
||||
| Copy to clipboard³ | <kbd>MOD</kbd>+<kbd>c</kbd>
|
||||
| Cut to clipboard³ | <kbd>MOD</kbd>+<kbd>x</kbd>
|
||||
| Synchronize clipboards and paste³ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd> \| _5th-click³_
|
||||
| Expand settings panel | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _Double-5th-click³_
|
||||
| Collapse panels | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
|
||||
| Copy to clipboard⁴ | <kbd>MOD</kbd>+<kbd>c</kbd>
|
||||
| Cut to clipboard⁴ | <kbd>MOD</kbd>+<kbd>x</kbd>
|
||||
| Synchronize clipboards and paste⁴ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
||||
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_
|
||||
|
||||
_¹Double-click on black borders to remove them._
|
||||
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
||||
_³Only on Android >= 7._
|
||||
_³4th and 5th mouse buttons, if your mouse has them._
|
||||
_⁴Only on Android >= 7._
|
||||
|
||||
Shortcuts with repeated keys are executted by releasing and pressing the key a
|
||||
second time. For example, to execute "Expand settings panel":
|
||||
|
||||
1. Press and keep pressing <kbd>MOD</kbd>.
|
||||
2. Then double-press <kbd>n</kbd>.
|
||||
3. Finally, release <kbd>MOD</kbd>.
|
||||
|
||||
All <kbd>Ctrl</kbd>+_key_ shortcuts are forwarded to the device, so they are
|
||||
handled by the active application.
|
||||
|
14
app/scrcpy.1
14
app/scrcpy.1
@ -83,10 +83,12 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
|
||||
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
|
||||
|
||||
.TP
|
||||
.BI "\-\-lock\-video\-orientation " value
|
||||
Lock video orientation to \fIvalue\fR. Possible values are -1 (unlocked), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise.
|
||||
.BI "\-\-lock\-video\-orientation " [value]
|
||||
Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise.
|
||||
|
||||
Default is -1 (unlocked).
|
||||
Default is "unlocked".
|
||||
|
||||
Passing the option without argument is equivalent to passing "initial".
|
||||
|
||||
.TP
|
||||
.BI "\-\-max\-fps " value
|
||||
@ -183,6 +185,12 @@ Enable "show touches" on start, restore the initial value on exit.
|
||||
|
||||
It only shows physical touches (not clicks from scrcpy).
|
||||
|
||||
.TP
|
||||
.BI "\-\-v4l2-sink " /dev/videoN
|
||||
Output to v4l2loopback device.
|
||||
|
||||
It requires to lock the video orientation (see --lock-video-orientation).
|
||||
|
||||
.TP
|
||||
.BI "\-V, \-\-verbosity " value
|
||||
Set the log level ("debug", "info", "warn" or "error").
|
||||
|
@ -79,12 +79,15 @@ scrcpy_print_usage(const char *arg0) {
|
||||
" This is a workaround for some devices not behaving as\n"
|
||||
" expected when setting the device clipboard programmatically.\n"
|
||||
"\n"
|
||||
" --lock-video-orientation value\n"
|
||||
" --lock-video-orientation [value]\n"
|
||||
" Lock video orientation to value.\n"
|
||||
" Possible values are -1 (unlocked), 0, 1, 2 and 3.\n"
|
||||
" Possible values are \"unlocked\", \"initial\" (locked to the\n"
|
||||
" initial orientation), 0, 1, 2 and 3.\n"
|
||||
" Natural device orientation is 0, and each increment adds a\n"
|
||||
" 90 degrees rotation counterclockwise.\n"
|
||||
" Default is -1 (unlocked).\n"
|
||||
" Default is \"unlocked\".\n"
|
||||
" Passing the option without argument is equivalent to passing\n"
|
||||
" \"initial\".\n"
|
||||
"\n"
|
||||
" --max-fps value\n"
|
||||
" Limit the frame rate of screen capture (officially supported\n"
|
||||
@ -173,11 +176,13 @@ scrcpy_print_usage(const char *arg0) {
|
||||
" on exit.\n"
|
||||
" It only shows physical touches (not clicks from scrcpy).\n"
|
||||
"\n"
|
||||
" --v4l2_sink /dev/videoN\n"
|
||||
#ifdef HAVE_V4L2
|
||||
" --v4l2-sink /dev/videoN\n"
|
||||
" Output to v4l2loopback device.\n"
|
||||
" It requires to lock the video orientation (see\n"
|
||||
" --lock-video-orientation).\n"
|
||||
"\n"
|
||||
#endif
|
||||
" -V, --verbosity value\n"
|
||||
" Set the log level (debug, info, warn or error).\n"
|
||||
#ifndef NDEBUG
|
||||
@ -388,15 +393,27 @@ parse_max_fps(const char *s, uint16_t *max_fps) {
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) {
|
||||
parse_lock_video_orientation(const char *s,
|
||||
enum sc_lock_video_orientation *lock_mode) {
|
||||
if (!s || !strcmp(s, "initial")) {
|
||||
// Without argument, lock the initial orientation
|
||||
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(s, "unlocked")) {
|
||||
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED;
|
||||
return true;
|
||||
}
|
||||
|
||||
long value;
|
||||
bool ok = parse_integer_arg(s, &value, false, -1, 3,
|
||||
bool ok = parse_integer_arg(s, &value, false, 0, 3,
|
||||
"lock video orientation");
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*lock_video_orientation = (int8_t) value;
|
||||
*lock_mode = (enum sc_lock_video_orientation) value;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -686,7 +703,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||
{"fullscreen", no_argument, NULL, 'f'},
|
||||
{"help", no_argument, NULL, 'h'},
|
||||
{"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE},
|
||||
{"lock-video-orientation", required_argument, NULL,
|
||||
{"lock-video-orientation", optional_argument, NULL,
|
||||
OPT_LOCK_VIDEO_ORIENTATION},
|
||||
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
|
||||
{"max-size", required_argument, NULL, 'm'},
|
||||
@ -709,7 +726,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||
{"stay-awake", no_argument, NULL, 'w'},
|
||||
{"turn-screen-off", no_argument, NULL, 'S'},
|
||||
#ifdef HAVE_V4L2
|
||||
{"v4l2_sink", required_argument, NULL, OPT_V4L2_SINK},
|
||||
{"v4l2-sink", required_argument, NULL, OPT_V4L2_SINK},
|
||||
#endif
|
||||
{"verbosity", required_argument, NULL, 'V'},
|
||||
{"version", no_argument, NULL, 'v'},
|
||||
@ -774,7 +791,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||
}
|
||||
break;
|
||||
case OPT_LOCK_VIDEO_ORIENTATION:
|
||||
if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) {
|
||||
if (!parse_lock_video_orientation(optarg,
|
||||
&opts->lock_video_orientation)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@ -894,29 +912,34 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||
case OPT_POWER_OFF_ON_CLOSE:
|
||||
opts->power_off_on_close = true;
|
||||
break;
|
||||
#ifdef HAVE_V4L2
|
||||
case OPT_V4L2_SINK:
|
||||
opts->v4l2_device = optarg;
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
if (!opts->display && !opts->record_filename && !opts->v4l2_device) {
|
||||
LOGE("-N/--no-display requires either screen recording (-r/--record)"
|
||||
#ifdef HAVE_V4L2
|
||||
" or sink to v4l2loopback device (--v4l2_sink)"
|
||||
#endif
|
||||
);
|
||||
" or sink to v4l2loopback device (--v4l2-sink)");
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
if (opts->v4l2_device && opts->lock_video_orientation == -1) {
|
||||
if (opts->v4l2_device && opts->lock_video_orientation
|
||||
== SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
||||
LOGI("Video orientation is locked for v4l2 sink. "
|
||||
"See --lock-video-orientation.");
|
||||
opts->lock_video_orientation = 0;
|
||||
opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
|
||||
}
|
||||
#else
|
||||
if (!opts->display && !opts->record_filename) {
|
||||
LOGE("-N/--no-display requires screen recording (-r/--record)");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -81,7 +81,8 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
|
||||
buf[1] = msg->set_screen_power_mode.mode;
|
||||
return 2;
|
||||
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||
case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
|
||||
case CONTROL_MSG_TYPE_COLLAPSE_PANELS:
|
||||
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
||||
case CONTROL_MSG_TYPE_ROTATE_DEVICE:
|
||||
// no additional data
|
||||
|
@ -27,7 +27,8 @@ enum control_msg_type {
|
||||
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
|
||||
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
|
||||
CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
|
||||
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
|
||||
CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
|
||||
CONTROL_MSG_TYPE_COLLAPSE_PANELS,
|
||||
CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
||||
CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
||||
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
||||
|
@ -111,6 +111,8 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) {
|
||||
// A frame lost should not make the whole pipeline fail. The error, if
|
||||
// any, is already logged.
|
||||
(void) ok;
|
||||
|
||||
av_frame_unref(decoder->frame);
|
||||
} else if (ret != AVERROR(EAGAIN)) {
|
||||
LOGE("Could not receive video frame: %d", ret);
|
||||
return false;
|
||||
|
@ -72,6 +72,10 @@ input_manager_init(struct input_manager *im,
|
||||
im->sdl_shortcut_mods.count = shortcut_mods->count;
|
||||
|
||||
im->vfinger_down = false;
|
||||
|
||||
im->last_keycode = SDLK_UNKNOWN;
|
||||
im->last_mod = 0;
|
||||
im->key_repeat = 0;
|
||||
}
|
||||
|
||||
static void
|
||||
@ -179,9 +183,19 @@ expand_notification_panel(struct controller *controller) {
|
||||
}
|
||||
|
||||
static void
|
||||
collapse_notification_panel(struct controller *controller) {
|
||||
expand_settings_panel(struct controller *controller) {
|
||||
struct control_msg msg;
|
||||
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL;
|
||||
msg.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL;
|
||||
|
||||
if (!controller_push_msg(controller, &msg)) {
|
||||
LOGW("Could not request 'expand settings panel'");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
collapse_panels(struct controller *controller) {
|
||||
struct control_msg msg;
|
||||
msg.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS;
|
||||
|
||||
if (!controller_push_msg(controller, &msg)) {
|
||||
LOGW("Could not request 'collapse notification panel'");
|
||||
@ -384,16 +398,27 @@ input_manager_process_key(struct input_manager *im,
|
||||
// control: indicates the state of the command-line option --no-control
|
||||
bool control = im->control;
|
||||
|
||||
bool smod = is_shortcut_mod(im, event->keysym.mod);
|
||||
|
||||
struct controller *controller = im->controller;
|
||||
|
||||
SDL_Keycode keycode = event->keysym.sym;
|
||||
uint16_t mod = event->keysym.mod;
|
||||
bool down = event->type == SDL_KEYDOWN;
|
||||
bool ctrl = event->keysym.mod & KMOD_CTRL;
|
||||
bool shift = event->keysym.mod & KMOD_SHIFT;
|
||||
bool repeat = event->repeat;
|
||||
|
||||
bool smod = is_shortcut_mod(im, mod);
|
||||
|
||||
if (down && !repeat) {
|
||||
if (keycode == im->last_keycode && mod == im->last_mod) {
|
||||
++im->key_repeat;
|
||||
} else {
|
||||
im->key_repeat = 0;
|
||||
im->last_keycode = keycode;
|
||||
im->last_mod = mod;
|
||||
}
|
||||
}
|
||||
|
||||
// The shortcut modifier is pressed
|
||||
if (smod) {
|
||||
int action = down ? ACTION_DOWN : ACTION_UP;
|
||||
@ -498,9 +523,11 @@ input_manager_process_key(struct input_manager *im,
|
||||
case SDLK_n:
|
||||
if (control && !repeat && down) {
|
||||
if (shift) {
|
||||
collapse_notification_panel(controller);
|
||||
} else {
|
||||
collapse_panels(controller);
|
||||
} else if (im->key_repeat == 0) {
|
||||
expand_notification_panel(controller);
|
||||
} else {
|
||||
expand_settings_panel(controller);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@ -666,7 +693,11 @@ input_manager_process_mouse_button(struct input_manager *im,
|
||||
return;
|
||||
}
|
||||
if (control && event->button == SDL_BUTTON_X2 && down) {
|
||||
expand_notification_panel(im->controller);
|
||||
if (event->clicks < 2) {
|
||||
expand_notification_panel(im->controller);
|
||||
} else {
|
||||
expand_settings_panel(im->controller);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (control && event->button == SDL_BUTTON_RIGHT) {
|
||||
|
@ -33,6 +33,13 @@ struct input_manager {
|
||||
} sdl_shortcut_mods;
|
||||
|
||||
bool vfinger_down;
|
||||
|
||||
// Tracks the number of identical consecutive shortcut key down events.
|
||||
// Not to be confused with event->repeat, which counts the number of
|
||||
// system-generated repeated key presses.
|
||||
unsigned key_repeat;
|
||||
SDL_Keycode last_keycode;
|
||||
uint16_t last_mod;
|
||||
};
|
||||
|
||||
void
|
||||
|
@ -243,10 +243,6 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
||||
|
||||
bool
|
||||
scrcpy(const struct scrcpy_options *options) {
|
||||
if (!server_init(&server)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ret = false;
|
||||
|
||||
bool server_started = false;
|
||||
@ -263,6 +259,7 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
|
||||
bool record = !!options->record_filename;
|
||||
struct server_params params = {
|
||||
.serial = options->serial,
|
||||
.log_level = options->log_level,
|
||||
.crop = options->crop,
|
||||
.port_range = options->port_range,
|
||||
@ -279,7 +276,12 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
.force_adb_forward = options->force_adb_forward,
|
||||
.power_off_on_close = options->power_off_on_close,
|
||||
};
|
||||
if (!server_start(&server, options->serial, ¶ms)) {
|
||||
|
||||
if (!server_init(&server, ¶ms)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!server_start(&server)) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
@ -304,7 +306,6 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
struct decoder *dec = NULL;
|
||||
if (options->display) {
|
||||
if (!fps_counter_init(&fps_counter)) {
|
||||
goto end;
|
||||
@ -312,13 +313,20 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
fps_counter_initialized = true;
|
||||
|
||||
if (options->control) {
|
||||
if (!file_handler_init(&file_handler, server.serial,
|
||||
if (!file_handler_init(&file_handler, options->serial,
|
||||
options->push_target)) {
|
||||
goto end;
|
||||
}
|
||||
file_handler_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
struct decoder *dec = NULL;
|
||||
bool needs_decoder = options->display;
|
||||
#ifdef HAVE_V4L2
|
||||
needs_decoder |= !!options->v4l2_device;
|
||||
#endif
|
||||
if (needs_decoder) {
|
||||
decoder_init(&decoder);
|
||||
dec = &decoder;
|
||||
}
|
||||
|
@ -20,6 +20,16 @@ enum sc_record_format {
|
||||
SC_RECORD_FORMAT_MKV,
|
||||
};
|
||||
|
||||
enum sc_lock_video_orientation {
|
||||
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
|
||||
// lock the current orientation when scrcpy starts
|
||||
SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
|
||||
SC_LOCK_VIDEO_ORIENTATION_0 = 0,
|
||||
SC_LOCK_VIDEO_ORIENTATION_1,
|
||||
SC_LOCK_VIDEO_ORIENTATION_2,
|
||||
SC_LOCK_VIDEO_ORIENTATION_3,
|
||||
};
|
||||
|
||||
#define SC_MAX_SHORTCUT_MODS 8
|
||||
|
||||
enum sc_shortcut_mod {
|
||||
@ -60,7 +70,7 @@ struct scrcpy_options {
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
uint16_t max_fps;
|
||||
int8_t lock_video_orientation;
|
||||
enum sc_lock_video_orientation lock_video_orientation;
|
||||
uint8_t rotation;
|
||||
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||
@ -108,7 +118,7 @@ struct scrcpy_options {
|
||||
.max_size = 0, \
|
||||
.bit_rate = DEFAULT_BIT_RATE, \
|
||||
.max_fps = 0, \
|
||||
.lock_video_orientation = -1, \
|
||||
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \
|
||||
.rotation = 0, \
|
||||
.window_x = SC_WINDOW_POSITION_UNDEFINED, \
|
||||
.window_y = SC_WINDOW_POSITION_UNDEFINED, \
|
||||
|
119
app/src/server.c
119
app/src/server.c
@ -58,7 +58,7 @@ get_server_path(void) {
|
||||
LOGE("Could not get executable path, "
|
||||
"using " SERVER_FILENAME " from current directory");
|
||||
// not found, use current directory
|
||||
return SERVER_FILENAME;
|
||||
return strdup(SERVER_FILENAME);
|
||||
}
|
||||
char *dir = dirname(executable_path);
|
||||
size_t dirlen = strlen(dir);
|
||||
@ -70,7 +70,7 @@ get_server_path(void) {
|
||||
LOGE("Could not alloc server path string, "
|
||||
"using " SERVER_FILENAME " from current directory");
|
||||
free(executable_path);
|
||||
return SERVER_FILENAME;
|
||||
return strdup(SERVER_FILENAME);
|
||||
}
|
||||
|
||||
memcpy(server_path, dir, dirlen);
|
||||
@ -128,9 +128,10 @@ disable_tunnel_forward(const char *serial, uint16_t local_port) {
|
||||
static bool
|
||||
disable_tunnel(struct server *server) {
|
||||
if (server->tunnel_forward) {
|
||||
return disable_tunnel_forward(server->serial, server->local_port);
|
||||
return disable_tunnel_forward(server->params.serial,
|
||||
server->local_port);
|
||||
}
|
||||
return disable_tunnel_reverse(server->serial);
|
||||
return disable_tunnel_reverse(server->params.serial);
|
||||
}
|
||||
|
||||
static socket_t
|
||||
@ -144,7 +145,7 @@ enable_tunnel_reverse_any_port(struct server *server,
|
||||
struct sc_port_range port_range) {
|
||||
uint16_t port = port_range.first;
|
||||
for (;;) {
|
||||
if (!enable_tunnel_reverse(server->serial, port)) {
|
||||
if (!enable_tunnel_reverse(server->params.serial, port)) {
|
||||
// the command itself failed, it will fail on any port
|
||||
return false;
|
||||
}
|
||||
@ -163,7 +164,7 @@ enable_tunnel_reverse_any_port(struct server *server,
|
||||
}
|
||||
|
||||
// failure, disable tunnel and try another port
|
||||
if (!disable_tunnel_reverse(server->serial)) {
|
||||
if (!disable_tunnel_reverse(server->params.serial)) {
|
||||
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
|
||||
}
|
||||
|
||||
@ -191,7 +192,7 @@ enable_tunnel_forward_any_port(struct server *server,
|
||||
server->tunnel_forward = true;
|
||||
uint16_t port = port_range.first;
|
||||
for (;;) {
|
||||
if (enable_tunnel_forward(server->serial, port)) {
|
||||
if (enable_tunnel_forward(server->params.serial, port)) {
|
||||
// success
|
||||
server->local_port = port;
|
||||
return true;
|
||||
@ -306,7 +307,7 @@ execute_server(struct server *server, const struct server_params *params) {
|
||||
// Port: 5005
|
||||
// Then click on "Debug"
|
||||
#endif
|
||||
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
|
||||
return adb_execute(server->params.serial, cmd, ARRAY_LEN(cmd));
|
||||
}
|
||||
|
||||
static socket_t
|
||||
@ -352,21 +353,75 @@ close_socket(socket_t socket) {
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
server_params_destroy(struct server_params *params) {
|
||||
// The server stores a copy of the params provided by the user
|
||||
free((char *) params->crop);
|
||||
free((char *) params->codec_options);
|
||||
free((char *) params->encoder_name);
|
||||
}
|
||||
|
||||
static bool
|
||||
server_params_copy(struct server_params *dst, const struct server_params *src) {
|
||||
// params reference user-allocated memory, so we must copy them to handle
|
||||
// them from a separate thread
|
||||
|
||||
*dst = *src;
|
||||
|
||||
dst->crop = NULL;
|
||||
dst->codec_options = NULL;
|
||||
dst->encoder_name = NULL;
|
||||
|
||||
if (src->crop) {
|
||||
dst->crop = strdup(src->crop);
|
||||
if (!dst->crop) {
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
if (src->codec_options) {
|
||||
dst->codec_options = strdup(src->codec_options);
|
||||
if (!dst->codec_options) {
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
if (src->encoder_name) {
|
||||
dst->encoder_name = strdup(src->encoder_name);
|
||||
if (!dst->encoder_name) {
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
error:
|
||||
server_params_destroy(dst);
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
bool
|
||||
server_init(struct server *server) {
|
||||
server->serial = NULL;
|
||||
server_init(struct server *server, const struct server_params *params) {
|
||||
if (!server_params_copy(&server->params, params)) {
|
||||
LOGE("Could not copy server params");
|
||||
return false;
|
||||
}
|
||||
|
||||
server->process = PROCESS_NONE;
|
||||
atomic_flag_clear_explicit(&server->server_socket_closed,
|
||||
memory_order_relaxed);
|
||||
|
||||
bool ok = sc_mutex_init(&server->mutex);
|
||||
if (!ok) {
|
||||
server_params_destroy(&server->params);
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&server->process_terminated_cond);
|
||||
if (!ok) {
|
||||
sc_mutex_destroy(&server->mutex);
|
||||
server_params_destroy(&server->params);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -406,31 +461,41 @@ run_wait_server(void *data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool
|
||||
server_start(struct server *server, const char *serial,
|
||||
const struct server_params *params) {
|
||||
if (serial) {
|
||||
server->serial = strdup(serial);
|
||||
if (!server->serial) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static int
|
||||
run_server(void *data) {
|
||||
struct server *server = data;
|
||||
|
||||
if (!push_server(serial)) {
|
||||
goto error1;
|
||||
const struct server_params *params = &server->params;
|
||||
const struct server_callbacks *cbs = &server->cbs;
|
||||
void *userdata = server->userdata;
|
||||
|
||||
if (!push_server(params->serial)) {
|
||||
cbs->on_connection_failed(server);
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (!enable_tunnel_any_port(server, params->port_range,
|
||||
params->force_adb_forward)) {
|
||||
goto error1;
|
||||
cbs->on_connection_failed(server);
|
||||
goto end;
|
||||
}
|
||||
|
||||
// server will connect to our server socket
|
||||
server->process = execute_server(server, params);
|
||||
if (server->process == PROCESS_NONE) {
|
||||
goto error2;
|
||||
cbs->on_connection_failed(server);
|
||||
goto end;
|
||||
}
|
||||
|
||||
process_wait(server->process, false); // ignore exit code
|
||||
|
||||
end:
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool
|
||||
server_start(struct server *server) {
|
||||
|
||||
// If the server process dies before connecting to the server socket, then
|
||||
// the client will be stuck forever on accept(). To avoid the problem, we
|
||||
// must be able to wake up the accept() call when the server dies. To keep
|
||||
@ -442,14 +507,14 @@ server_start(struct server *server, const char *serial,
|
||||
if (!ok) {
|
||||
process_terminate(server->process);
|
||||
process_wait(server->process, true); // ignore exit code
|
||||
goto error2;
|
||||
goto error;
|
||||
}
|
||||
|
||||
server->tunnel_enabled = true;
|
||||
|
||||
return true;
|
||||
|
||||
error2:
|
||||
error:
|
||||
if (!server->tunnel_forward) {
|
||||
bool was_closed =
|
||||
atomic_flag_test_and_set(&server->server_socket_closed);
|
||||
@ -459,8 +524,7 @@ error2:
|
||||
close_socket(server->server_socket);
|
||||
}
|
||||
disable_tunnel(server);
|
||||
error1:
|
||||
free(server->serial);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -558,4 +622,5 @@ server_destroy(struct server *server) {
|
||||
free(server->serial);
|
||||
sc_cond_destroy(&server->process_terminated_cond);
|
||||
sc_mutex_destroy(&server->mutex);
|
||||
server_params_destroy(&server->params);
|
||||
}
|
||||
|
@ -13,6 +13,25 @@
|
||||
#include "util/net.h"
|
||||
#include "util/thread.h"
|
||||
|
||||
struct server_params {
|
||||
enum sc_log_level log_level;
|
||||
const char *serial;
|
||||
const char *crop;
|
||||
const char *codec_options;
|
||||
const char *encoder_name;
|
||||
struct sc_port_range port_range;
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
uint16_t max_fps;
|
||||
int8_t lock_video_orientation;
|
||||
bool control;
|
||||
uint32_t display_id;
|
||||
bool show_touches;
|
||||
bool stay_awake;
|
||||
bool force_adb_forward;
|
||||
bool power_off_on_close;
|
||||
};
|
||||
|
||||
struct server {
|
||||
char *serial;
|
||||
process_t process;
|
||||
@ -29,34 +48,29 @@ struct server {
|
||||
uint16_t local_port; // selected from port_range
|
||||
bool tunnel_enabled;
|
||||
bool tunnel_forward; // use "adb forward" instead of "adb reverse"
|
||||
|
||||
// The internal allocated strings are copies owned by the server
|
||||
struct server_params params;
|
||||
|
||||
const struct server_callbacks *cbs;
|
||||
void *userdata;
|
||||
};
|
||||
|
||||
struct server_params {
|
||||
enum sc_log_level log_level;
|
||||
const char *crop;
|
||||
const char *codec_options;
|
||||
const char *encoder_name;
|
||||
struct sc_port_range port_range;
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
uint16_t max_fps;
|
||||
int8_t lock_video_orientation;
|
||||
bool control;
|
||||
uint32_t display_id;
|
||||
bool show_touches;
|
||||
bool stay_awake;
|
||||
bool force_adb_forward;
|
||||
bool power_off_on_close;
|
||||
struct server_callbacks {
|
||||
void (*on_connection_failed)(struct server *server);
|
||||
void (*on_connected)(struct server *server, const char *name,
|
||||
struct size size, void *userdata);
|
||||
void (*on_disconnected)(struct server *server, void *userdata);
|
||||
};
|
||||
|
||||
// init default values
|
||||
// init server fields
|
||||
bool
|
||||
server_init(struct server *server);
|
||||
server_init(struct server *server, const struct server_params *params);
|
||||
|
||||
// push, enable tunnel et start the server
|
||||
bool
|
||||
server_start(struct server *server, const char *serial,
|
||||
const struct server_params *params);
|
||||
server_start(struct server *server, const struct server_callbacks *cbs,
|
||||
void *userdata);
|
||||
|
||||
// block until the communication with the server is established
|
||||
bool
|
||||
|
@ -6,6 +6,8 @@
|
||||
/** Downcast frame_sink to sc_v4l2_sink */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_v4l2_sink, frame_sink)
|
||||
|
||||
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
||||
|
||||
static const AVOutputFormat *
|
||||
find_muxer(const char *name) {
|
||||
#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
|
||||
@ -24,7 +26,60 @@ find_muxer(const char *name) {
|
||||
}
|
||||
|
||||
static bool
|
||||
encode_and_send_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) {
|
||||
write_header(struct sc_v4l2_sink *vs, const AVPacket *packet) {
|
||||
AVStream *ostream = vs->format_ctx->streams[0];
|
||||
|
||||
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
|
||||
if (!extradata) {
|
||||
LOGC("Could not allocate extradata");
|
||||
return false;
|
||||
}
|
||||
|
||||
// copy the first packet to the extra data
|
||||
memcpy(extradata, packet->data, packet->size);
|
||||
|
||||
ostream->codecpar->extradata = extradata;
|
||||
ostream->codecpar->extradata_size = packet->size;
|
||||
|
||||
int ret = avformat_write_header(vs->format_ctx, NULL);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write header to %s", vs->device_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
rescale_packet(struct sc_v4l2_sink *vs, AVPacket *packet) {
|
||||
AVStream *ostream = vs->format_ctx->streams[0];
|
||||
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
|
||||
}
|
||||
|
||||
static bool
|
||||
write_packet(struct sc_v4l2_sink *vs, AVPacket *packet) {
|
||||
if (!vs->header_written) {
|
||||
bool ok = write_header(vs, packet);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
vs->header_written = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
rescale_packet(vs, packet);
|
||||
|
||||
bool ok = av_write_frame(vs->format_ctx, packet) >= 0;
|
||||
|
||||
// Failing to write the last frame is not very serious, no future frame may
|
||||
// depend on it, so the resulting file will still be valid
|
||||
(void) ok;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
encode_and_write_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) {
|
||||
int ret = avcodec_send_frame(vs->encoder_ctx, frame);
|
||||
if (ret < 0 && ret != AVERROR(EAGAIN)) {
|
||||
LOGE("Could not send v4l2 video frame: %d", ret);
|
||||
@ -35,8 +90,13 @@ encode_and_send_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) {
|
||||
ret = avcodec_receive_packet(vs->encoder_ctx, packet);
|
||||
if (ret == 0) {
|
||||
// A packet was received
|
||||
av_write_frame(vs->format_ctx, packet);
|
||||
|
||||
bool ok = write_packet(vs, packet);
|
||||
av_packet_unref(packet);
|
||||
if (!ok) {
|
||||
LOGW("Could not send packet to v4l2 sink");
|
||||
return false;
|
||||
}
|
||||
} else if (ret != AVERROR(EAGAIN)) {
|
||||
LOGE("Could not receive v4l2 video packet: %d", ret);
|
||||
return false;
|
||||
@ -64,7 +124,8 @@ run_v4l2_sink(void *data) {
|
||||
sc_mutex_unlock(&vs->mutex);
|
||||
|
||||
video_buffer_consume(&vs->vb, vs->frame);
|
||||
bool ok = encode_and_send_frame(vs, vs->frame);
|
||||
bool ok = encode_and_write_frame(vs, vs->frame);
|
||||
av_frame_unref(vs->frame);
|
||||
if (!ok) {
|
||||
LOGE("Could not send frame to v4l2 sink");
|
||||
break;
|
||||
@ -119,6 +180,12 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
|
||||
// still expects a pointer-to-non-const (it has not be updated accordingly)
|
||||
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
|
||||
vs->format_ctx->oformat = (AVOutputFormat *) format;
|
||||
vs->format_ctx->url = strdup(vs->device_name);
|
||||
if (!vs->format_ctx->url) {
|
||||
LOGE("Could not strdup v4l2 device name");
|
||||
goto error_avformat_free_context;
|
||||
return false;
|
||||
}
|
||||
|
||||
AVStream *ostream = avformat_new_stream(vs->format_ctx, encoder);
|
||||
if (!ostream) {
|
||||
@ -170,6 +237,9 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
|
||||
goto error_av_frame_free;
|
||||
}
|
||||
|
||||
vs->header_written = false;
|
||||
vs->stopped = false;
|
||||
|
||||
LOGI("v4l2 sink started to device: %s", vs->device_name);
|
||||
|
||||
return true;
|
||||
@ -196,6 +266,13 @@ error_video_buffer_destroy:
|
||||
|
||||
static void
|
||||
sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
|
||||
sc_mutex_lock(&vs->mutex);
|
||||
vs->stopped = true;
|
||||
sc_cond_signal(&vs->cond);
|
||||
sc_mutex_unlock(&vs->mutex);
|
||||
|
||||
sc_thread_join(&vs->thread, NULL);
|
||||
|
||||
av_frame_free(&vs->frame);
|
||||
avcodec_close(vs->encoder_ctx);
|
||||
avcodec_free_context(&vs->encoder_ctx);
|
||||
|
@ -23,6 +23,7 @@ struct sc_v4l2_sink {
|
||||
sc_mutex mutex;
|
||||
sc_cond cond;
|
||||
bool stopped;
|
||||
bool header_written;
|
||||
|
||||
AVFrame *frame;
|
||||
AVPacket packet;
|
||||
|
@ -177,9 +177,9 @@ static void test_serialize_expand_notification_panel(void) {
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_collapse_notification_panel(void) {
|
||||
static void test_serialize_expand_settings_panel(void) {
|
||||
struct control_msg msg = {
|
||||
.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
|
||||
.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
|
||||
};
|
||||
|
||||
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||
@ -187,7 +187,22 @@ static void test_serialize_collapse_notification_panel(void) {
|
||||
assert(size == 1);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
|
||||
CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_collapse_panels(void) {
|
||||
struct control_msg msg = {
|
||||
.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS,
|
||||
};
|
||||
|
||||
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = control_msg_serialize(&msg, buf);
|
||||
assert(size == 1);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
CONTROL_MSG_TYPE_COLLAPSE_PANELS,
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
@ -274,7 +289,8 @@ int main(int argc, char *argv[]) {
|
||||
test_serialize_inject_scroll_event();
|
||||
test_serialize_back_or_screen_on();
|
||||
test_serialize_expand_notification_panel();
|
||||
test_serialize_collapse_notification_panel();
|
||||
test_serialize_expand_settings_panel();
|
||||
test_serialize_collapse_panels();
|
||||
test_serialize_get_clipboard();
|
||||
test_serialize_set_clipboard();
|
||||
test_serialize_set_screen_power_mode();
|
||||
|
@ -3,6 +3,10 @@ package com.genymobile.scrcpy;
|
||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Base64;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
@ -15,25 +19,123 @@ public final class CleanUp {
|
||||
|
||||
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
|
||||
|
||||
// A simple struct to be passed from the main process to the cleanup process
|
||||
public static class Config implements Parcelable {
|
||||
|
||||
public static final Creator<Config> CREATOR = new Creator<Config>() {
|
||||
@Override
|
||||
public Config createFromParcel(Parcel in) {
|
||||
return new Config(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config[] newArray(int size) {
|
||||
return new Config[size];
|
||||
}
|
||||
};
|
||||
|
||||
private static final int FLAG_DISABLE_SHOW_TOUCHES = 1;
|
||||
private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2;
|
||||
private static final int FLAG_POWER_OFF_SCREEN = 4;
|
||||
|
||||
private int displayId;
|
||||
|
||||
// Restore the value (between 0 and 7), -1 to not restore
|
||||
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
|
||||
private int restoreStayOn = -1;
|
||||
|
||||
private boolean disableShowTouches;
|
||||
private boolean restoreNormalPowerMode;
|
||||
private boolean powerOffScreen;
|
||||
|
||||
public Config() {
|
||||
// Default constructor, the fields are initialized by CleanUp.configure()
|
||||
}
|
||||
|
||||
protected Config(Parcel in) {
|
||||
displayId = in.readInt();
|
||||
restoreStayOn = in.readInt();
|
||||
byte options = in.readByte();
|
||||
disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0;
|
||||
restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0;
|
||||
powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(displayId);
|
||||
dest.writeInt(restoreStayOn);
|
||||
byte options = 0;
|
||||
if (disableShowTouches) {
|
||||
options |= FLAG_DISABLE_SHOW_TOUCHES;
|
||||
}
|
||||
if (restoreNormalPowerMode) {
|
||||
options |= FLAG_RESTORE_NORMAL_POWER_MODE;
|
||||
}
|
||||
if (powerOffScreen) {
|
||||
options |= FLAG_POWER_OFF_SCREEN;
|
||||
}
|
||||
dest.writeByte(options);
|
||||
}
|
||||
|
||||
private boolean hasWork() {
|
||||
return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
byte[] serialize() {
|
||||
Parcel parcel = Parcel.obtain();
|
||||
writeToParcel(parcel, 0);
|
||||
byte[] bytes = parcel.marshall();
|
||||
parcel.recycle();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static Config deserialize(byte[] bytes) {
|
||||
Parcel parcel = Parcel.obtain();
|
||||
parcel.unmarshall(bytes, 0, bytes.length);
|
||||
parcel.setDataPosition(0);
|
||||
return CREATOR.createFromParcel(parcel);
|
||||
}
|
||||
|
||||
static Config fromBase64(String base64) {
|
||||
byte[] bytes = Base64.decode(base64, Base64.NO_WRAP);
|
||||
return deserialize(bytes);
|
||||
}
|
||||
|
||||
String toBase64() {
|
||||
byte[] bytes = serialize();
|
||||
return Base64.encodeToString(bytes, Base64.NO_WRAP);
|
||||
}
|
||||
}
|
||||
|
||||
private CleanUp() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode, boolean powerOffScreen, int displayId)
|
||||
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen)
|
||||
throws IOException {
|
||||
boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
|
||||
if (needProcess) {
|
||||
startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode, powerOffScreen, displayId);
|
||||
Config config = new Config();
|
||||
config.displayId = displayId;
|
||||
config.disableShowTouches = disableShowTouches;
|
||||
config.restoreStayOn = restoreStayOn;
|
||||
config.restoreNormalPowerMode = restoreNormalPowerMode;
|
||||
config.powerOffScreen = powerOffScreen;
|
||||
|
||||
if (config.hasWork()) {
|
||||
startProcess(config);
|
||||
} else {
|
||||
// There is no additional clean up to do when scrcpy dies
|
||||
unlinkSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode, boolean powerOffScreen,
|
||||
int displayId) throws IOException {
|
||||
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(
|
||||
restoreStayOn), String.valueOf(restoreNormalPowerMode), String.valueOf(powerOffScreen), String.valueOf(displayId)};
|
||||
private static void startProcess(Config config) throws IOException {
|
||||
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
builder.environment().put("CLASSPATH", SERVER_PATH);
|
||||
@ -60,31 +162,27 @@ public final class CleanUp {
|
||||
|
||||
Ln.i("Cleaning up");
|
||||
|
||||
boolean disableShowTouches = Boolean.parseBoolean(args[0]);
|
||||
int restoreStayOn = Integer.parseInt(args[1]);
|
||||
boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]);
|
||||
boolean powerOffScreen = Boolean.parseBoolean(args[3]);
|
||||
int displayId = Integer.parseInt(args[4]);
|
||||
Config config = Config.fromBase64(args[0]);
|
||||
|
||||
if (disableShowTouches || restoreStayOn != -1) {
|
||||
if (config.disableShowTouches || config.restoreStayOn != -1) {
|
||||
ServiceManager serviceManager = new ServiceManager();
|
||||
try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||
if (disableShowTouches) {
|
||||
if (config.disableShowTouches) {
|
||||
Ln.i("Disabling \"show touches\"");
|
||||
settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0");
|
||||
}
|
||||
if (restoreStayOn != -1) {
|
||||
if (config.restoreStayOn != -1) {
|
||||
Ln.i("Restoring \"stay awake\"");
|
||||
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
|
||||
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Device.isScreenOn()) {
|
||||
if (powerOffScreen) {
|
||||
if (config.powerOffScreen) {
|
||||
Ln.i("Power off screen");
|
||||
Device.powerOffScreen(displayId);
|
||||
} else if (restoreNormalPowerMode) {
|
||||
Device.powerOffScreen(config.displayId);
|
||||
} else if (config.restoreNormalPowerMode) {
|
||||
Ln.i("Restoring normal power mode");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
||||
}
|
||||
|
@ -11,11 +11,12 @@ public final class ControlMessage {
|
||||
public static final int TYPE_INJECT_SCROLL_EVENT = 3;
|
||||
public static final int TYPE_BACK_OR_SCREEN_ON = 4;
|
||||
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
|
||||
public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6;
|
||||
public static final int TYPE_GET_CLIPBOARD = 7;
|
||||
public static final int TYPE_SET_CLIPBOARD = 8;
|
||||
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
|
||||
public static final int TYPE_ROTATE_DEVICE = 10;
|
||||
public static final int TYPE_EXPAND_SETTINGS_PANEL = 6;
|
||||
public static final int TYPE_COLLAPSE_PANELS = 7;
|
||||
public static final int TYPE_GET_CLIPBOARD = 8;
|
||||
public static final int TYPE_SET_CLIPBOARD = 9;
|
||||
public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
|
||||
public static final int TYPE_ROTATE_DEVICE = 11;
|
||||
|
||||
private int type;
|
||||
private String text;
|
||||
|
@ -77,7 +77,8 @@ public class ControlMessageReader {
|
||||
msg = parseSetScreenPowerMode();
|
||||
break;
|
||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
msg = ControlMessage.createEmpty(type);
|
||||
|
@ -55,7 +55,7 @@ public class Controller {
|
||||
public void control() throws IOException {
|
||||
// on start, power on the device
|
||||
if (!Device.isScreenOn()) {
|
||||
device.injectKeycode(KeyEvent.KEYCODE_POWER);
|
||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER);
|
||||
|
||||
// dirty hack
|
||||
// After POWER is injected, the device is powered on asynchronously.
|
||||
@ -107,7 +107,10 @@ public class Controller {
|
||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
Device.expandNotificationPanel();
|
||||
break;
|
||||
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||
Device.expandSettingsPanel();
|
||||
break;
|
||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||
Device.collapsePanels();
|
||||
break;
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
@ -270,7 +273,7 @@ public class Controller {
|
||||
if (keepPowerModeOff) {
|
||||
schedulePowerModeOff();
|
||||
}
|
||||
return device.injectKeycode(KeyEvent.KEYCODE_POWER);
|
||||
return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER);
|
||||
}
|
||||
|
||||
private boolean setClipboard(String text, boolean paste) {
|
||||
@ -281,7 +284,7 @@ public class Controller {
|
||||
|
||||
// On Android >= 7, also press the PASTE key if requested
|
||||
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
|
||||
device.injectKeycode(KeyEvent.KEYCODE_PASTE);
|
||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE);
|
||||
}
|
||||
|
||||
return ok;
|
||||
|
@ -25,6 +25,9 @@ public final class Device {
|
||||
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
|
||||
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
|
||||
|
||||
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
|
||||
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
|
||||
|
||||
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
|
||||
|
||||
public interface RotationListener {
|
||||
@ -161,54 +164,39 @@ public final class Device {
|
||||
return supportsInputEvents;
|
||||
}
|
||||
|
||||
public static boolean injectEvent(InputEvent inputEvent, int mode, int displayId) {
|
||||
public static boolean injectEvent(InputEvent inputEvent, int displayId) {
|
||||
if (!supportsInputEvents(displayId)) {
|
||||
return false;
|
||||
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
||||
}
|
||||
|
||||
if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, mode);
|
||||
}
|
||||
|
||||
public boolean injectEvent(InputEvent inputEvent, int mode) {
|
||||
if (!supportsInputEvents()) {
|
||||
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
||||
}
|
||||
|
||||
return injectEvent(inputEvent, mode, displayId);
|
||||
}
|
||||
|
||||
public static boolean injectEventOnDisplay(InputEvent event, int displayId) {
|
||||
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, displayId);
|
||||
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
public boolean injectEvent(InputEvent event) {
|
||||
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
||||
return injectEvent(event, displayId);
|
||||
}
|
||||
|
||||
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
||||
InputDevice.SOURCE_KEYBOARD);
|
||||
return injectEventOnDisplay(event, displayId);
|
||||
return injectEvent(event, displayId);
|
||||
}
|
||||
|
||||
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
||||
InputDevice.SOURCE_KEYBOARD);
|
||||
return injectEvent(event);
|
||||
return injectKeyEvent(action, keyCode, repeat, metaState, displayId);
|
||||
}
|
||||
|
||||
public static boolean injectKeycode(int keyCode, int displayId) {
|
||||
public static boolean pressReleaseKeycode(int keyCode, int displayId) {
|
||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId);
|
||||
}
|
||||
|
||||
public boolean injectKeycode(int keyCode) {
|
||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
|
||||
public boolean pressReleaseKeycode(int keyCode) {
|
||||
return pressReleaseKeycode(keyCode, displayId);
|
||||
}
|
||||
|
||||
public static boolean isScreenOn() {
|
||||
@ -227,6 +215,10 @@ public final class Device {
|
||||
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel();
|
||||
}
|
||||
|
||||
public static void expandSettingsPanel() {
|
||||
SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel();
|
||||
}
|
||||
|
||||
public static void collapsePanels() {
|
||||
SERVICE_MANAGER.getStatusBarManager().collapsePanels();
|
||||
}
|
||||
@ -280,7 +272,7 @@ public final class Device {
|
||||
if (!isScreenOn()) {
|
||||
return true;
|
||||
}
|
||||
return injectKeycode(KeyEvent.KEYCODE_POWER, displayId);
|
||||
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -82,6 +82,12 @@ public final class ScreenInfo {
|
||||
|
||||
public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) {
|
||||
int rotation = displayInfo.getRotation();
|
||||
|
||||
if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) {
|
||||
// The user requested to lock the video orientation to the current orientation
|
||||
lockedVideoOrientation = rotation;
|
||||
}
|
||||
|
||||
Size deviceSize = displayInfo.getSize();
|
||||
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
|
||||
if (crop != null) {
|
||||
|
@ -50,7 +50,7 @@ public final class Server {
|
||||
}
|
||||
}
|
||||
|
||||
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true, options.getPowerOffScreenOnClose(), options.getDisplayId());
|
||||
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
|
||||
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
|
||||
|
@ -14,7 +14,7 @@ public class ActivityManager {
|
||||
|
||||
private final IInterface manager;
|
||||
private Method getContentProviderExternalMethod;
|
||||
private boolean getContentProviderExternalMethodLegacy;
|
||||
private boolean getContentProviderExternalMethodNewVersion = true;
|
||||
private Method removeContentProviderExternalMethod;
|
||||
|
||||
public ActivityManager(IInterface manager) {
|
||||
@ -29,7 +29,7 @@ public class ActivityManager {
|
||||
} catch (NoSuchMethodException e) {
|
||||
// old version
|
||||
getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
|
||||
getContentProviderExternalMethodLegacy = true;
|
||||
getContentProviderExternalMethodNewVersion = false;
|
||||
}
|
||||
}
|
||||
return getContentProviderExternalMethod;
|
||||
@ -46,7 +46,7 @@ public class ActivityManager {
|
||||
try {
|
||||
Method method = getGetContentProviderExternalMethod();
|
||||
Object[] args;
|
||||
if (!getContentProviderExternalMethodLegacy) {
|
||||
if (getContentProviderExternalMethodNewVersion) {
|
||||
// new version
|
||||
args = new Object[]{name, ServiceManager.USER_ID, token, null};
|
||||
} else {
|
||||
|
@ -11,6 +11,8 @@ public class StatusBarManager {
|
||||
|
||||
private final IInterface manager;
|
||||
private Method expandNotificationsPanelMethod;
|
||||
private Method expandSettingsPanelMethod;
|
||||
private boolean expandSettingsPanelMethodNewVersion = true;
|
||||
private Method collapsePanelsMethod;
|
||||
|
||||
public StatusBarManager(IInterface manager) {
|
||||
@ -24,6 +26,20 @@ public class StatusBarManager {
|
||||
return expandNotificationsPanelMethod;
|
||||
}
|
||||
|
||||
private Method getExpandSettingsPanel() throws NoSuchMethodException {
|
||||
if (expandSettingsPanelMethod == null) {
|
||||
try {
|
||||
// Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/
|
||||
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// old version
|
||||
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel");
|
||||
expandSettingsPanelMethodNewVersion = false;
|
||||
}
|
||||
}
|
||||
return expandSettingsPanelMethod;
|
||||
}
|
||||
|
||||
private Method getCollapsePanelsMethod() throws NoSuchMethodException {
|
||||
if (collapsePanelsMethod == null) {
|
||||
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
|
||||
@ -40,6 +56,21 @@ public class StatusBarManager {
|
||||
}
|
||||
}
|
||||
|
||||
public void expandSettingsPanel() {
|
||||
try {
|
||||
Method method = getExpandSettingsPanel();
|
||||
if (expandSettingsPanelMethodNewVersion) {
|
||||
// new version
|
||||
method.invoke(manager, (Object) null);
|
||||
} else {
|
||||
// old version
|
||||
method.invoke(manager);
|
||||
}
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void collapsePanels() {
|
||||
try {
|
||||
Method method = getCollapsePanelsMethod();
|
||||
|
@ -162,7 +162,7 @@ public class ControlMessageReaderTest {
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
|
||||
Assert.assertEquals(KeyEvent.ACTION_DOWN, event.getAction());
|
||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -182,19 +182,35 @@ public class ControlMessageReaderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCollapseNotificationPanelEvent() throws IOException {
|
||||
public void testParseExpandSettingsPanelEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL);
|
||||
dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType());
|
||||
Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCollapsePanelsEvent() throws IOException {
|
||||
ControlMessageReader reader = new ControlMessageReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Reference in New Issue
Block a user