Compare commits

...

7 Commits

Author SHA1 Message Date
Romain Vimont
eb10385a2b Replace SDL_Atomic by stdatomic from C11
There is no reason to use SDL atomics.
2020-04-02 19:19:11 +02:00
Romain Vimont
3603939475 Do not log success on failure
If calling the private API does not work, an exception is printed. In
that case, do not log that the action succeeded.
2020-04-01 11:28:48 +02:00
Romain Vimont
d76c7c3029 Do not warn on terminating the server
If the server is already dead, terminating it fails. This is expected.
2020-03-29 21:31:12 +02:00
Romain Vimont
f2f032a494 Do not block on accept() if server died
The server may die before connecting to the client. In that case, the
client was blocked indefinitely (until Ctrl+C) on accept().

To avoid the problem, close the server socket once the server process is
dead.
2020-03-29 21:31:09 +02:00
Romain Vimont
728b976aae Wait server from a separate thread
Create a thread just to wait for the server process exit.

This paves the way to simply wake up a blocking accept() in a portable
way.
2020-03-29 21:24:58 +02:00
Romain Vimont
ff583bdde8 Refactor server_start() error handling
This avoids cleanup duplication.
2020-03-29 21:24:31 +02:00
e_vigurskiy
429153abfb Add display id parameter
Add --display command line parameter to specify a display id.
2020-03-28 23:56:37 +01:00
20 changed files with 345 additions and 60 deletions

View File

@ -353,6 +353,21 @@ scrcpy --no-control
scrcpy -n scrcpy -n
``` ```
#### Display
If several displays are available, it is possible to select the display to
mirror:
```bash
scrcpy --display 1
```
The list of display ids can be retrieved by:
```
adb shell dumpsys display # search "mDisplayId=" in the output
```
#### Turn screen off #### Turn screen off
It is possible to turn the device screen off while mirroring on start with a It is possible to turn the device screen off while mirroring on start with a

View File

@ -33,6 +33,15 @@ The values are expressed in the device natural orientation (typically, portrait
.B \-\-max\-size .B \-\-max\-size
value is computed on the cropped size. value is computed on the cropped size.
.TP
.BI "\-\-display " id
Specify the display id to mirror.
The list of possible display ids can be listed by "adb shell dumpsys display"
(search "mDisplayId=" in the output).
Default is 0.
.TP .TP
.B \-f, \-\-fullscreen .B \-f, \-\-fullscreen
Start in fullscreen. Start in fullscreen.

View File

@ -36,6 +36,15 @@ scrcpy_print_usage(const char *arg0) {
" (typically, portrait for a phone, landscape for a tablet).\n" " (typically, portrait for a phone, landscape for a tablet).\n"
" Any --max-size value is computed on the cropped size.\n" " Any --max-size value is computed on the cropped size.\n"
"\n" "\n"
" --display id\n"
" Specify the display id to mirror.\n"
"\n"
" The list of possible display ids can be listed by:\n"
" adb shell dumpsys display\n"
" (search \"mDisplayId=\" in the output)\n"
"\n"
" Default is 0.\n"
"\n"
" -f, --fullscreen\n" " -f, --fullscreen\n"
" Start in fullscreen.\n" " Start in fullscreen.\n"
"\n" "\n"
@ -363,6 +372,18 @@ parse_port_range(const char *s, struct port_range *port_range) {
return true; return true;
} }
static bool
parse_display_id(const char *s, uint16_t *display_id) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "display id");
if (!ok) {
return false;
}
*display_id = (uint16_t) value;
return true;
}
static bool static bool
parse_record_format(const char *optarg, enum recorder_format *format) { parse_record_format(const char *optarg, enum recorder_format *format) {
if (!strcmp(optarg, "mp4")) { if (!strcmp(optarg, "mp4")) {
@ -407,6 +428,7 @@ guess_record_format(const char *filename) {
#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 #define OPT_LOCK_VIDEO_ORIENTATION 1013
#define OPT_DISPLAY_ID 1014
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[]) {
@ -414,6 +436,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
{"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},
{"display", required_argument, NULL, OPT_DISPLAY_ID},
{"fullscreen", no_argument, NULL, 'f'}, {"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"lock-video-orientation", required_argument, NULL, {"lock-video-orientation", required_argument, NULL,
@ -462,6 +485,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case OPT_CROP: case OPT_CROP:
opts->crop = optarg; opts->crop = optarg;
break; break;
case OPT_DISPLAY_ID:
if (!parse_display_id(optarg, &opts->display_id)) {
return false;
}
break;
case 'f': case 'f':
opts->fullscreen = true; opts->fullscreen = true;
break; break;

View File

@ -23,7 +23,7 @@ fps_counter_init(struct fps_counter *counter) {
} }
counter->thread = NULL; counter->thread = NULL;
SDL_AtomicSet(&counter->started, 0); atomic_init(&counter->started, 0);
// no need to initialize the other fields, they are unused until started // no need to initialize the other fields, they are unused until started
return true; return true;
@ -35,6 +35,16 @@ fps_counter_destroy(struct fps_counter *counter) {
SDL_DestroyMutex(counter->mutex); SDL_DestroyMutex(counter->mutex);
} }
static inline bool
is_started(struct fps_counter *counter) {
return atomic_load_explicit(&counter->started, memory_order_acquire);
}
static inline void
set_started(struct fps_counter *counter, bool started) {
atomic_store_explicit(&counter->started, started, memory_order_release);
}
// must be called with mutex locked // must be called with mutex locked
static void static void
display_fps(struct fps_counter *counter) { display_fps(struct fps_counter *counter) {
@ -70,10 +80,10 @@ run_fps_counter(void *data) {
mutex_lock(counter->mutex); mutex_lock(counter->mutex);
while (!counter->interrupted) { while (!counter->interrupted) {
while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) { while (!counter->interrupted && !is_started(counter)) {
cond_wait(counter->state_cond, counter->mutex); cond_wait(counter->state_cond, counter->mutex);
} }
while (!counter->interrupted && SDL_AtomicGet(&counter->started)) { while (!counter->interrupted && is_started(counter)) {
uint32_t now = SDL_GetTicks(); uint32_t now = SDL_GetTicks();
check_interval_expired(counter, now); check_interval_expired(counter, now);
@ -96,7 +106,7 @@ fps_counter_start(struct fps_counter *counter) {
counter->nr_skipped = 0; counter->nr_skipped = 0;
mutex_unlock(counter->mutex); mutex_unlock(counter->mutex);
SDL_AtomicSet(&counter->started, 1); set_started(counter, true);
cond_signal(counter->state_cond); cond_signal(counter->state_cond);
// counter->thread is always accessed from the same thread, no need to lock // counter->thread is always accessed from the same thread, no need to lock
@ -114,13 +124,13 @@ fps_counter_start(struct fps_counter *counter) {
void void
fps_counter_stop(struct fps_counter *counter) { fps_counter_stop(struct fps_counter *counter) {
SDL_AtomicSet(&counter->started, 0); set_started(counter, false);
cond_signal(counter->state_cond); cond_signal(counter->state_cond);
} }
bool bool
fps_counter_is_started(struct fps_counter *counter) { fps_counter_is_started(struct fps_counter *counter) {
return SDL_AtomicGet(&counter->started); return is_started(counter);
} }
void void
@ -145,7 +155,7 @@ fps_counter_join(struct fps_counter *counter) {
void void
fps_counter_add_rendered_frame(struct fps_counter *counter) { fps_counter_add_rendered_frame(struct fps_counter *counter) {
if (!SDL_AtomicGet(&counter->started)) { if (!is_started(counter)) {
return; return;
} }
@ -158,7 +168,7 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) {
void void
fps_counter_add_skipped_frame(struct fps_counter *counter) { fps_counter_add_skipped_frame(struct fps_counter *counter) {
if (!SDL_AtomicGet(&counter->started)) { if (!is_started(counter)) {
return; return;
} }

View File

@ -1,9 +1,9 @@
#ifndef FPSCOUNTER_H #ifndef FPSCOUNTER_H
#define FPSCOUNTER_H #define FPSCOUNTER_H
#include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_mutex.h> #include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
@ -16,7 +16,7 @@ struct fps_counter {
// atomic so that we can check without locking the mutex // atomic so that we can check without locking the mutex
// if the FPS counter is disabled, we don't want to lock unnecessarily // if the FPS counter is disabled, we don't want to lock unnecessarily
SDL_atomic_t started; atomic_bool started;
// the following fields are protected by the mutex // the following fields are protected by the mutex
bool interrupted; bool interrupted;

View File

@ -286,6 +286,7 @@ scrcpy(const struct scrcpy_options *options) {
.max_fps = options->max_fps, .max_fps = options->max_fps,
.lock_video_orientation = options->lock_video_orientation, .lock_video_orientation = options->lock_video_orientation,
.control = options->control, .control = options->control,
.display_id = options->display_id,
}; };
if (!server_start(&server, options->serial, &params)) { if (!server_start(&server, options->serial, &params)) {
return false; return false;

View File

@ -25,6 +25,7 @@ struct scrcpy_options {
int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto"
uint16_t window_width; uint16_t window_width;
uint16_t window_height; uint16_t window_height;
uint16_t display_id;
bool show_touches; bool show_touches;
bool fullscreen; bool fullscreen;
bool always_on_top; bool always_on_top;
@ -55,6 +56,7 @@ struct scrcpy_options {
.window_y = WINDOW_POSITION_UNDEFINED, \ .window_y = WINDOW_POSITION_UNDEFINED, \
.window_width = 0, \ .window_width = 0, \
.window_height = 0, \ .window_height = 0, \
.display_id = 0, \
.show_touches = false, \ .show_touches = false, \
.fullscreen = false, \ .fullscreen = false, \
.always_on_top = false, \ .always_on_top = false, \

View File

@ -5,6 +5,7 @@
#include <inttypes.h> #include <inttypes.h>
#include <libgen.h> #include <libgen.h>
#include <stdio.h> #include <stdio.h>
#include <SDL2/SDL_thread.h>
#include <SDL2/SDL_timer.h> #include <SDL2/SDL_timer.h>
#include <SDL2/SDL_platform.h> #include <SDL2/SDL_platform.h>
@ -234,10 +235,12 @@ execute_server(struct server *server, const struct server_params *params) {
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]; char lock_video_orientation_string[3];
char display_id_string[6];
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); sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation);
sprintf(display_id_string, "%"PRIu16, params->display_id);
const char *const cmd[] = { const char *const cmd[] = {
"shell", "shell",
"CLASSPATH=" DEVICE_SERVER_PATH, "CLASSPATH=" DEVICE_SERVER_PATH,
@ -264,6 +267,7 @@ execute_server(struct server *server, const struct server_params *params) {
params->crop ? params->crop : "-", params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp) "true", // always send frame meta (packet boundaries + timestamp)
params->control ? "true" : "false", params->control ? "true" : "false",
display_id_string,
}; };
#ifdef SERVER_DEBUGGER #ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port " LOGI("Server debugger waiting for a client on device port "
@ -314,14 +318,12 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) {
} }
static void static void
close_socket(socket_t *socket) { close_socket(socket_t socket) {
assert(*socket != INVALID_SOCKET); assert(socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR); net_shutdown(socket, SHUT_RDWR);
if (!net_close(*socket)) { if (!net_close(socket)) {
LOGW("Could not close socket"); LOGW("Could not close socket");
return;
} }
*socket = INVALID_SOCKET;
} }
void void
@ -329,6 +331,22 @@ server_init(struct server *server) {
*server = (struct server) SERVER_INITIALIZER; *server = (struct server) SERVER_INITIALIZER;
} }
static int
run_wait_server(void *data) {
struct server *server = data;
cmd_simple_wait(server->process, NULL); // ignore exit code
// no need for synchronization, server_socket is initialized before this
// thread was created
if (server->server_socket != INVALID_SOCKET
&& !atomic_flag_test_and_set(&server->server_socket_closed)) {
// On Linux, accept() is unblocked by shutdown(), but on Windows, it is
// unblocked by closesocket(). Therefore, call both (close_socket()).
close_socket(server->server_socket);
}
LOGD("Server terminated");
return 0;
}
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) {
@ -342,30 +360,49 @@ server_start(struct server *server, const char *serial,
} }
if (!push_server(serial)) { if (!push_server(serial)) {
SDL_free(server->serial); goto error1;
return false;
} }
if (!enable_tunnel_any_port(server, params->port_range)) { if (!enable_tunnel_any_port(server, params->port_range)) {
SDL_free(server->serial); goto error1;
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);
if (server->process == PROCESS_NONE) { if (server->process == PROCESS_NONE) {
if (!server->tunnel_forward) { goto error2;
close_socket(&server->server_socket); }
}
disable_tunnel(server); // If the server process dies before connecting to the server socket, then
SDL_free(server->serial); // the client will be stuck forever on accept(). To avoid the problem, we
return false; // must be able to wake up the accept() call when the server dies. To keep
// things simple and multiplatform, just spawn a new thread waiting for the
// server process and calling shutdown()/close() on the server socket if
// necessary to wake up any accept() blocking call.
server->wait_server_thread =
SDL_CreateThread(run_wait_server, "wait-server", server);
if (!server->wait_server_thread) {
cmd_terminate(server->process);
cmd_simple_wait(server->process, NULL); // ignore exit code
goto error2;
} }
server->tunnel_enabled = true; server->tunnel_enabled = true;
return true; return true;
error2:
if (!server->tunnel_forward) {
bool was_closed =
atomic_flag_test_and_set(&server->server_socket_closed);
// the thread is not started, the flag could not be already set
assert(!was_closed);
close_socket(server->server_socket);
}
disable_tunnel(server);
error1:
SDL_free(server->serial);
return false;
} }
bool bool
@ -383,7 +420,11 @@ server_connect_to(struct server *server) {
} }
// we don't need the server socket anymore // we don't need the server socket anymore
close_socket(&server->server_socket); if (!atomic_flag_test_and_set(&server->server_socket_closed)) {
// close it from here
close_socket(server->server_socket);
// otherwise, it is closed by run_wait_server()
}
} else { } else {
uint32_t attempts = 100; uint32_t attempts = 100;
uint32_t delay = 100; // ms uint32_t delay = 100; // ms
@ -410,29 +451,27 @@ server_connect_to(struct server *server) {
void void
server_stop(struct server *server) { server_stop(struct server *server) {
if (server->server_socket != INVALID_SOCKET) { if (server->server_socket != INVALID_SOCKET
close_socket(&server->server_socket); && !atomic_flag_test_and_set(&server->server_socket_closed)) {
close_socket(server->server_socket);
} }
if (server->video_socket != INVALID_SOCKET) { if (server->video_socket != INVALID_SOCKET) {
close_socket(&server->video_socket); close_socket(server->video_socket);
} }
if (server->control_socket != INVALID_SOCKET) { if (server->control_socket != INVALID_SOCKET) {
close_socket(&server->control_socket); close_socket(server->control_socket);
} }
assert(server->process != PROCESS_NONE); assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) { cmd_terminate(server->process);
LOGW("Could not terminate server");
}
cmd_simple_wait(server->process, NULL); // ignore exit code
LOGD("Server terminated");
if (server->tunnel_enabled) { if (server->tunnel_enabled) {
// ignore failure // ignore failure
disable_tunnel(server); disable_tunnel(server);
} }
SDL_WaitThread(server->wait_server_thread, NULL);
} }
void void

View File

@ -1,8 +1,10 @@
#ifndef SERVER_H #ifndef SERVER_H
#define SERVER_H #define SERVER_H
#include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <SDL2/SDL_thread.h>
#include "config.h" #include "config.h"
#include "command.h" #include "command.h"
@ -12,6 +14,8 @@
struct server { struct server {
char *serial; char *serial;
process_t process; process_t process;
SDL_Thread *wait_server_thread;
atomic_flag server_socket_closed;
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;
@ -24,6 +28,8 @@ struct server {
#define SERVER_INITIALIZER { \ #define SERVER_INITIALIZER { \
.serial = NULL, \ .serial = NULL, \
.process = PROCESS_NONE, \ .process = PROCESS_NONE, \
.wait_server_thread = NULL, \
.server_socket_closed = ATOMIC_FLAG_INIT, \
.server_socket = INVALID_SOCKET, \ .server_socket = INVALID_SOCKET, \
.video_socket = INVALID_SOCKET, \ .video_socket = INVALID_SOCKET, \
.control_socket = INVALID_SOCKET, \ .control_socket = INVALID_SOCKET, \
@ -44,6 +50,7 @@ struct server_params {
uint16_t max_fps; uint16_t max_fps;
int8_t lock_video_orientation; int8_t lock_video_orientation;
bool control; bool control;
uint16_t display_id;
}; };
// init default values // init default values

View File

@ -75,19 +75,29 @@ public class Controller {
ControlMessage msg = connection.receiveControlMessage(); ControlMessage msg = connection.receiveControlMessage();
switch (msg.getType()) { switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE: case ControlMessage.TYPE_INJECT_KEYCODE:
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); if (device.supportsInputEvents()) {
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
}
break; break;
case ControlMessage.TYPE_INJECT_TEXT: case ControlMessage.TYPE_INJECT_TEXT:
injectText(msg.getText()); if (device.supportsInputEvents()) {
injectText(msg.getText());
}
break; break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT: case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); if (device.supportsInputEvents()) {
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
}
break; break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT: case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); if (device.supportsInputEvents()) {
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
}
break; break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
pressBackOrTurnScreenOn(); if (device.supportsInputEvents()) {
pressBackOrTurnScreenOn();
}
break; break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel(); device.expandNotificationPanel();
@ -103,7 +113,9 @@ public class Controller {
device.setClipboardText(msg.getText()); device.setClipboardText(msg.getText());
break; break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
device.setScreenPowerMode(msg.getAction()); if (device.supportsInputEvents()) {
device.setScreenPowerMode(msg.getAction());
}
break; break;
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
device.rotateDevice(); device.rotateDevice();

View File

@ -1,5 +1,6 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager; import com.genymobile.scrcpy.wrappers.WindowManager;
@ -25,9 +26,35 @@ public final class Device {
private ScreenInfo screenInfo; private ScreenInfo screenInfo;
private RotationListener rotationListener; private RotationListener rotationListener;
/**
* Logical display identifier
*/
private final int displayId;
/**
* The surface flinger layer stack associated with this logical display
*/
private final int layerStack;
/**
* The FLAG_PRESENTATION from the DisplayInfo
*/
private final boolean isPresentationDisplay;
public Device(Options options) { public Device(Options options) {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); displayId = options.getDisplayId();
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
int[] displayIds = serviceManager.getDisplayManager().getDisplayIds();
throw new InvalidDisplayIdException(displayId, displayIds);
}
int displayInfoFlags = displayInfo.getFlags();
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
layerStack = displayInfo.getLayerStack();
isPresentationDisplay = (displayInfoFlags & DisplayInfo.FLAG_PRESENTATION) != 0;
registerRotationWatcher(new IRotationWatcher.Stub() { registerRotationWatcher(new IRotationWatcher.Stub() {
@Override @Override
public void onRotationChanged(int rotation) throws RemoteException { public void onRotationChanged(int rotation) throws RemoteException {
@ -41,12 +68,24 @@ public final class Device {
} }
} }
}); });
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
}
if (!supportsInputEvents()) {
Ln.w("Input events are not supported for displays with FLAG_PRESENTATION enabled for devices with API lower than 29");
}
} }
public synchronized ScreenInfo getScreenInfo() { public synchronized ScreenInfo getScreenInfo() {
return screenInfo; return screenInfo;
} }
public int getLayerStack() {
return layerStack;
}
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")
@ -76,7 +115,22 @@ public final class Device {
return Build.MODEL; return Build.MODEL;
} }
public boolean supportsInputEvents() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return true;
}
return !isPresentationDisplay;
}
public boolean injectInputEvent(InputEvent inputEvent, int mode) { public boolean injectInputEvent(InputEvent inputEvent, int mode) {
if (!supportsInputEvents()) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
}
if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) {
return false;
}
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
} }
@ -109,8 +163,10 @@ public final class Device {
} }
public void setClipboardText(String text) { public void setClipboardText(String text) {
serviceManager.getClipboardManager().setText(text); boolean ok = serviceManager.getClipboardManager().setText(text);
Ln.i("Device clipboard set"); if (ok) {
Ln.i("Device clipboard set");
}
} }
/** /**
@ -122,8 +178,10 @@ public final class Device {
Ln.e("Could not get built-in display"); Ln.e("Could not get built-in display");
return; return;
} }
SurfaceControl.setDisplayPowerMode(d, mode); boolean ok = SurfaceControl.setDisplayPowerMode(d, mode);
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); if (ok) {
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
}
} }
/** /**

View File

@ -1,12 +1,25 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
public final class DisplayInfo { public final class DisplayInfo {
private final int displayId;
private final Size size; private final Size size;
private final int rotation; private final int rotation;
private final int layerStack;
private final int flags;
public DisplayInfo(Size size, int rotation) { public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
public static final int FLAG_PRESENTATION = 0x00000008;
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) {
this.displayId = displayId;
this.size = size; this.size = size;
this.rotation = rotation; this.rotation = rotation;
this.layerStack = layerStack;
this.flags = flags;
}
public int getDisplayId() {
return displayId;
} }
public Size getSize() { public Size getSize() {
@ -16,5 +29,13 @@ public final class DisplayInfo {
public int getRotation() { public int getRotation() {
return rotation; return rotation;
} }
public int getLayerStack() {
return layerStack;
}
public int getFlags() {
return flags;
}
} }

View File

@ -0,0 +1,21 @@
package com.genymobile.scrcpy;
public class InvalidDisplayIdException extends RuntimeException {
private final int displayId;
private final int[] availableDisplayIds;
public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) {
super("There is no display having id " + displayId);
this.displayId = displayId;
this.availableDisplayIds = availableDisplayIds;
}
public int getDisplayId() {
return displayId;
}
public int[] getAvailableDisplayIds() {
return availableDisplayIds;
}
}

View File

@ -11,6 +11,7 @@ public class Options {
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
private boolean control; private boolean control;
private int displayId;
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
@ -75,4 +76,12 @@ public class Options {
public void setControl(boolean control) { public void setControl(boolean control) {
this.control = control; this.control = control;
} }
public int getDisplayId() {
return displayId;
}
public void setDisplayId(int displayId) {
this.displayId = displayId;
}
} }

View File

@ -71,10 +71,12 @@ public class ScreenEncoder implements Device.RotationListener {
// does not include the locked video orientation // does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation(); int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
setSize(format, videoRect.width(), videoRect.height()); setSize(format, videoRect.width(), videoRect.height());
configure(codec, format); configure(codec, format);
Surface surface = codec.createInputSurface(); Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
codec.start(); codec.start();
try { try {
alive = encode(codec, fd); alive = encode(codec, fd);
@ -177,12 +179,12 @@ public class ScreenEncoder implements Device.RotationListener {
format.setInteger(MediaFormat.KEY_HEIGHT, height); format.setInteger(MediaFormat.KEY_HEIGHT, height);
} }
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect) { private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
SurfaceControl.openTransaction(); SurfaceControl.openTransaction();
try { try {
SurfaceControl.setDisplaySurface(display, surface); SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, 0); SurfaceControl.setDisplayLayerStack(display, layerStack);
} finally { } finally {
SurfaceControl.closeTransaction(); SurfaceControl.closeTransaction();
} }

View File

@ -80,8 +80,8 @@ public final class Server {
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
} }
if (args.length != 9) { if (args.length != 10) {
throw new IllegalArgumentException("Expecting 9 parameters"); throw new IllegalArgumentException("Expecting 10 parameters");
} }
Options options = new Options(); Options options = new Options();
@ -111,6 +111,9 @@ public final class Server {
boolean control = Boolean.parseBoolean(args[8]); boolean control = Boolean.parseBoolean(args[8]);
options.setControl(control); options.setControl(control);
int displayId = Integer.parseInt(args[9]);
options.setDisplayId(displayId);
return options; return options;
} }
@ -149,6 +152,16 @@ public final class Server {
} }
} }
} }
if (e instanceof InvalidDisplayIdException) {
InvalidDisplayIdException idie = (InvalidDisplayIdException) e;
int[] displayIds = idie.getAvailableDisplayIds();
if (displayIds != null && displayIds.length > 0) {
Ln.e("Try to use one of the available display ids:");
for (int id : displayIds) {
Ln.e(" scrcpy --display " + id);
}
}
}
} }
public static void main(String... args) throws Exception { public static void main(String... args) throws Exception {

View File

@ -74,13 +74,15 @@ public class ClipboardManager {
} }
} }
public void setText(CharSequence text) { public boolean setText(CharSequence text) {
try { try {
Method method = getSetPrimaryClipMethod(); Method method = getSetPrimaryClipMethod();
ClipData clipData = ClipData.newPlainText(null, text); ClipData clipData = ClipData.newPlainText(null, text);
setPrimaryClip(method, manager, clipData); setPrimaryClip(method, manager, clipData);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return false;
} }
} }
} }

View File

@ -12,15 +12,28 @@ public final class DisplayManager {
this.manager = manager; this.manager = manager;
} }
public DisplayInfo getDisplayInfo() { public DisplayInfo getDisplayInfo(int displayId) {
try { try {
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
if (displayInfo == null) {
return null;
}
Class<?> cls = displayInfo.getClass(); Class<?> cls = displayInfo.getClass();
// width and height already take the rotation into account // width and height already take the rotation into account
int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo); int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo); int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
return new DisplayInfo(new Size(width, height), rotation); int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public int[] getDisplayIds() {
try {
return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager);
} catch (Exception e) { } catch (Exception e) {
throw new AssertionError(e); throw new AssertionError(e);
} }

View File

@ -17,6 +17,8 @@ public final class InputManager {
private final IInterface manager; private final IInterface manager;
private Method injectInputEventMethod; private Method injectInputEventMethod;
private static Method setDisplayIdMethod;
public InputManager(IInterface manager) { public InputManager(IInterface manager) {
this.manager = manager; this.manager = manager;
} }
@ -37,4 +39,23 @@ public final class InputManager {
return false; return false;
} }
} }
private static Method getSetDisplayIdMethod() throws NoSuchMethodException {
if (setDisplayIdMethod == null) {
setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class);
}
return setDisplayIdMethod;
}
public static boolean setDisplayId(InputEvent inputEvent, int displayId) {
try {
Method method = getSetDisplayIdMethod();
method.invoke(inputEvent, displayId);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
// just a warning, it might happen on old devices
Ln.w("Cannot associate a display id to the input event");
return false;
}
}
} }

View File

@ -121,12 +121,14 @@ public final class SurfaceControl {
return setDisplayPowerModeMethod; return setDisplayPowerModeMethod;
} }
public static void setDisplayPowerMode(IBinder displayToken, int mode) { public static boolean setDisplayPowerMode(IBinder displayToken, int mode) {
try { try {
Method method = getSetDisplayPowerModeMethod(); Method method = getSetDisplayPowerModeMethod();
method.invoke(null, displayToken, mode); method.invoke(null, displayToken, mode);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return false;
} }
} }