Compare commits

...

5 Commits

Author SHA1 Message Date
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
16 changed files with 309 additions and 44 deletions

View File

@ -353,6 +353,21 @@ scrcpy --no-control
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
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
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
.B \-f, \-\-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"
" Any --max-size value is computed on the cropped size.\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"
" Start in fullscreen.\n"
"\n"
@ -363,6 +372,18 @@ parse_port_range(const char *s, struct port_range *port_range) {
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
parse_record_format(const char *optarg, enum recorder_format *format) {
if (!strcmp(optarg, "mp4")) {
@ -407,6 +428,7 @@ guess_record_format(const char *filename) {
#define OPT_WINDOW_BORDERLESS 1011
#define OPT_MAX_FPS 1012
#define OPT_LOCK_VIDEO_ORIENTATION 1013
#define OPT_DISPLAY_ID 1014
bool
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},
{"bit-rate", required_argument, NULL, 'b'},
{"crop", required_argument, NULL, OPT_CROP},
{"display", required_argument, NULL, OPT_DISPLAY_ID},
{"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'},
{"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:
opts->crop = optarg;
break;
case OPT_DISPLAY_ID:
if (!parse_display_id(optarg, &opts->display_id)) {
return false;
}
break;
case 'f':
opts->fullscreen = true;
break;

View File

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

View File

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

View File

@ -5,6 +5,7 @@
#include <inttypes.h>
#include <libgen.h>
#include <stdio.h>
#include <SDL2/SDL_thread.h>
#include <SDL2/SDL_timer.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 max_fps_string[6];
char lock_video_orientation_string[3];
char display_id_string[6];
sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation);
sprintf(display_id_string, "%"PRIu16, params->display_id);
const char *const cmd[] = {
"shell",
"CLASSPATH=" DEVICE_SERVER_PATH,
@ -264,6 +267,7 @@ execute_server(struct server *server, const struct server_params *params) {
params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp)
params->control ? "true" : "false",
display_id_string,
};
#ifdef SERVER_DEBUGGER
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
close_socket(socket_t *socket) {
assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR);
if (!net_close(*socket)) {
close_socket(socket_t socket) {
assert(socket != INVALID_SOCKET);
net_shutdown(socket, SHUT_RDWR);
if (!net_close(socket)) {
LOGW("Could not close socket");
return;
}
*socket = INVALID_SOCKET;
}
void
@ -329,6 +331,22 @@ server_init(struct server *server) {
*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
&& SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) {
// 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
server_start(struct server *server, const char *serial,
const struct server_params *params) {
@ -342,30 +360,47 @@ server_start(struct server *server, const char *serial,
}
if (!push_server(serial)) {
SDL_free(server->serial);
return false;
goto error1;
}
if (!enable_tunnel_any_port(server, params->port_range)) {
SDL_free(server->serial);
return false;
goto error1;
}
// server will connect to our server socket
server->process = execute_server(server, params);
if (server->process == PROCESS_NONE) {
if (!server->tunnel_forward) {
close_socket(&server->server_socket);
}
disable_tunnel(server);
SDL_free(server->serial);
return false;
goto error2;
}
// 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
// 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;
return true;
error2:
if (!server->tunnel_forward) {
// the wait server thread is not started, SDL_AtomicSet() is sufficient
SDL_AtomicSet(&server->server_socket_closed, 1);
close_socket(server->server_socket);
}
disable_tunnel(server);
error1:
SDL_free(server->serial);
return false;
}
bool
@ -383,7 +418,11 @@ server_connect_to(struct server *server) {
}
// we don't need the server socket anymore
close_socket(&server->server_socket);
if (SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) {
// close it from here
close_socket(server->server_socket);
// otherwise, it is closed by run_wait_server()
}
} else {
uint32_t attempts = 100;
uint32_t delay = 100; // ms
@ -410,29 +449,27 @@ server_connect_to(struct server *server) {
void
server_stop(struct server *server) {
if (server->server_socket != INVALID_SOCKET) {
close_socket(&server->server_socket);
if (server->server_socket != INVALID_SOCKET
&& SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) {
close_socket(server->server_socket);
}
if (server->video_socket != INVALID_SOCKET) {
close_socket(&server->video_socket);
close_socket(server->video_socket);
}
if (server->control_socket != INVALID_SOCKET) {
close_socket(&server->control_socket);
close_socket(server->control_socket);
}
assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) {
LOGW("Could not terminate server");
}
cmd_simple_wait(server->process, NULL); // ignore exit code
LOGD("Server terminated");
cmd_terminate(server->process);
if (server->tunnel_enabled) {
// ignore failure
disable_tunnel(server);
}
SDL_WaitThread(server->wait_server_thread, NULL);
}
void

View File

@ -3,6 +3,8 @@
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "command.h"
@ -12,6 +14,8 @@
struct server {
char *serial;
process_t process;
SDL_Thread *wait_server_thread;
SDL_atomic_t server_socket_closed;
socket_t server_socket; // only used if !tunnel_forward
socket_t video_socket;
socket_t control_socket;
@ -24,6 +28,8 @@ struct server {
#define SERVER_INITIALIZER { \
.serial = NULL, \
.process = PROCESS_NONE, \
.wait_server_thread = NULL, \
.server_socket_closed = {0}, \
.server_socket = INVALID_SOCKET, \
.video_socket = INVALID_SOCKET, \
.control_socket = INVALID_SOCKET, \
@ -44,6 +50,7 @@ struct server_params {
uint16_t max_fps;
int8_t lock_video_orientation;
bool control;
uint16_t display_id;
};
// init default values

View File

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

View File

@ -1,5 +1,6 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager;
@ -25,9 +26,35 @@ public final class Device {
private ScreenInfo screenInfo;
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) {
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());
layerStack = displayInfo.getLayerStack();
isPresentationDisplay = (displayInfoFlags & DisplayInfo.FLAG_PRESENTATION) != 0;
registerRotationWatcher(new IRotationWatcher.Stub() {
@Override
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() {
return screenInfo;
}
public int getLayerStack() {
return layerStack;
}
public Point getPhysicalPoint(Position position) {
// it hides the field on purpose, to read it with a lock
@SuppressWarnings("checkstyle:HiddenField")
@ -76,7 +115,22 @@ public final class Device {
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) {
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);
}

View File

@ -1,12 +1,25 @@
package com.genymobile.scrcpy;
public final class DisplayInfo {
private final int displayId;
private final Size size;
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.rotation = rotation;
this.layerStack = layerStack;
this.flags = flags;
}
public int getDisplayId() {
return displayId;
}
public Size getSize() {
@ -16,5 +29,13 @@ public final class DisplayInfo {
public int getRotation() {
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 boolean sendFrameMeta; // send PTS so that the client may record properly
private boolean control;
private int displayId;
public int getMaxSize() {
return maxSize;
@ -75,4 +76,12 @@ public class Options {
public void setControl(boolean 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
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
setSize(format, videoRect.width(), videoRect.height());
configure(codec, format);
Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect);
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
codec.start();
try {
alive = encode(codec, fd);
@ -177,12 +179,12 @@ public class ScreenEncoder implements Device.RotationListener {
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();
try {
SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, 0);
SurfaceControl.setDisplayLayerStack(display, layerStack);
} finally {
SurfaceControl.closeTransaction();
}

View File

@ -80,8 +80,8 @@ public final class Server {
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
if (args.length != 9) {
throw new IllegalArgumentException("Expecting 9 parameters");
if (args.length != 10) {
throw new IllegalArgumentException("Expecting 10 parameters");
}
Options options = new Options();
@ -111,6 +111,9 @@ public final class Server {
boolean control = Boolean.parseBoolean(args[8]);
options.setControl(control);
int displayId = Integer.parseInt(args[9]);
options.setDisplayId(displayId);
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 {

View File

@ -12,15 +12,28 @@ public final class DisplayManager {
this.manager = manager;
}
public DisplayInfo getDisplayInfo() {
public DisplayInfo getDisplayInfo(int displayId) {
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();
// width and height already take the rotation into account
int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
int height = cls.getDeclaredField("logicalHeight").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) {
throw new AssertionError(e);
}

View File

@ -17,6 +17,8 @@ public final class InputManager {
private final IInterface manager;
private Method injectInputEventMethod;
private static Method setDisplayIdMethod;
public InputManager(IInterface manager) {
this.manager = manager;
}
@ -37,4 +39,23 @@ public final class InputManager {
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;
}
}
}