Add support for multi-display Android system

Add -d, --display option to work with non-primary display in multi-
display Android configurations

Signed-off-by: Kostiantyn Luzan <vblack2006@gmail.com>
This commit is contained in:
Kostiantyn Luzan 2020-04-07 00:18:58 +03:00 committed by Kostiantyn Luzan
commit 3ecb89b469
16 changed files with 111 additions and 23 deletions

View file

@ -110,6 +110,10 @@ conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
# overridden by option --bit-rate # overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
# the default display number
# overridden by option --display
conf.set('DEFAULT_DISPLAY_ID', '0') # Primary display
# enable High DPI support # enable High DPI support
conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))

View file

@ -35,6 +35,10 @@ 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"
" -d, --display value\n"
" Use specified display in multi-display Android systems.\n"
" Default is %d.\n"
"\n"
" -f, --fullscreen\n" " -f, --fullscreen\n"
" Start in fullscreen.\n" " Start in fullscreen.\n"
"\n" "\n"
@ -192,6 +196,7 @@ scrcpy_print_usage(const char *arg0) {
"\n", "\n",
arg0, arg0,
DEFAULT_BIT_RATE, DEFAULT_BIT_RATE,
DEFAULT_DISPLAY_ID,
DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)",
DEFAULT_LOCAL_PORT); DEFAULT_LOCAL_PORT);
} }
@ -235,6 +240,19 @@ parse_bit_rate(const char *s, uint32_t *bit_rate) {
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");
if (!ok) {
return false;
}
*display_id = (uint16_t) value;
return true;
}
static bool static bool
parse_max_size(const char *s, uint16_t *max_size) { parse_max_size(const char *s, uint16_t *max_size) {
long value; long value;
@ -347,6 +365,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, 'd'},
{"fullscreen", no_argument, NULL, 'f'}, {"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"max-fps", required_argument, NULL, OPT_MAX_FPS}, {"max-fps", required_argument, NULL, OPT_MAX_FPS},
@ -379,7 +398,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
optind = 0; // reset to start from the first argument in tests optind = 0; // reset to start from the first argument in tests
int c; int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, while ((c = getopt_long(argc, argv, "b:c:d:fF:hm:nNp:r:s:StTv", long_options,
NULL)) != -1) { NULL)) != -1) {
switch (c) { switch (c) {
case 'b': case 'b':
@ -390,6 +409,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case 'c': case 'c':
LOGW("Deprecated option -c. Use --crop instead."); LOGW("Deprecated option -c. Use --crop instead.");
// fall through // fall through
case 'd':
if (!parse_display_id(optarg, &opts->display_id)) {
return false;
}
break;
case OPT_CROP: case OPT_CROP:
opts->crop = optarg; opts->crop = optarg;
break; break;

View file

@ -285,6 +285,7 @@ scrcpy(const struct scrcpy_options *options) {
.bit_rate = options->bit_rate, .bit_rate = options->bit_rate,
.max_fps = options->max_fps, .max_fps = options->max_fps,
.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

@ -18,6 +18,7 @@ struct scrcpy_options {
uint16_t port; uint16_t port;
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t display_id;
uint16_t max_fps; uint16_t max_fps;
int16_t window_x; int16_t window_x;
int16_t window_y; int16_t window_y;
@ -44,6 +45,7 @@ struct scrcpy_options {
.port = DEFAULT_LOCAL_PORT, \ .port = DEFAULT_LOCAL_PORT, \
.max_size = DEFAULT_MAX_SIZE, \ .max_size = DEFAULT_MAX_SIZE, \
.bit_rate = DEFAULT_BIT_RATE, \ .bit_rate = DEFAULT_BIT_RATE, \
.display_id = DEFAULT_DISPLAY_ID, \
.max_fps = 0, \ .max_fps = 0, \
.window_x = -1, \ .window_x = -1, \
.window_y = -1, \ .window_y = -1, \

View file

@ -124,9 +124,11 @@ execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6]; char max_size_string[6];
char bit_rate_string[11]; char bit_rate_string[11];
char max_fps_string[6]; char max_fps_string[6];
char 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(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,
@ -146,6 +148,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 "

View file

@ -37,6 +37,7 @@ struct server_params {
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;
bool control; bool control;
uint16_t display_id;
}; };
// init default values // init default values

View file

@ -48,6 +48,7 @@ static void test_options(void) {
"--fullscreen", "--fullscreen",
"--max-fps", "30", "--max-fps", "30",
"--max-size", "1024", "--max-size", "1024",
"--display", "1",
// "--no-control" is not compatible with "--turn-screen-off" // "--no-control" is not compatible with "--turn-screen-off"
// "--no-display" is not compatible with "--fulscreen" // "--no-display" is not compatible with "--fulscreen"
"--port", "1234", "--port", "1234",
@ -78,6 +79,7 @@ static void test_options(void) {
assert(opts->fullscreen); assert(opts->fullscreen);
assert(opts->max_fps == 30); assert(opts->max_fps == 30);
assert(opts->max_size == 1024); assert(opts->max_size == 1024);
assert(opts->display_id == 1);
assert(opts->port == 1234); assert(opts->port == 1234);
assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->push_target, "/sdcard/Movies"));
assert(!strcmp(opts->record_filename, "file")); assert(!strcmp(opts->record_filename, "file"));

View file

@ -1,6 +1,7 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.MotionEventWrapper;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.InputDevice; import android.view.InputDevice;
@ -179,9 +180,10 @@ public class Controller {
} }
} }
MotionEvent event = MotionEvent MotionEvent event = MotionEventWrapper
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0); InputDevice.SOURCE_TOUCHSCREEN, device.getScreenInfo().getDisplayId(), 0);
return injectEvent(event); return injectEvent(event);
} }
@ -202,9 +204,9 @@ public class Controller {
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
MotionEvent event = MotionEvent MotionEvent event = MotionEventWrapper
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_MOUSE, 0); InputDevice.SOURCE_MOUSE, device.getScreenInfo().getDisplayId(), 0);
return injectEvent(event); return injectEvent(event);
} }

View file

@ -26,7 +26,8 @@ public final class Device {
private RotationListener rotationListener; private RotationListener rotationListener;
public Device(Options options) { public Device(Options options) {
screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); int displayId = options.getDisplayId();
screenInfo = computeScreenInfo(displayId, options.getCrop(), options.getMaxSize());
registerRotationWatcher(new IRotationWatcher.Stub() { registerRotationWatcher(new IRotationWatcher.Stub() {
@Override @Override
public void onRotationChanged(int rotation) throws RemoteException { public void onRotationChanged(int rotation) throws RemoteException {
@ -39,15 +40,15 @@ public final class Device {
} }
} }
} }
}); }, displayId);
} }
public synchronized ScreenInfo getScreenInfo() { public synchronized ScreenInfo getScreenInfo() {
return screenInfo; return screenInfo;
} }
private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { private ScreenInfo computeScreenInfo(int displayId, Rect crop, int maxSize) {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId);
boolean rotated = (displayInfo.getRotation() & 1) != 0; boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize(); Size deviceSize = displayInfo.getSize();
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
@ -64,7 +65,7 @@ public final class Device {
} }
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
return new ScreenInfo(contentRect, videoSize, rotated); return new ScreenInfo(displayId, contentRect, videoSize, rotated);
} }
private static String formatCrop(Rect rect) { private static String formatCrop(Rect rect) {
@ -129,8 +130,8 @@ public final class Device {
return serviceManager.getPowerManager().isScreenOn(); return serviceManager.getPowerManager().isScreenOn();
} }
public void registerRotationWatcher(IRotationWatcher rotationWatcher) { public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher); serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
} }
public synchronized void setRotationListener(RotationListener rotationListener) { public synchronized void setRotationListener(RotationListener rotationListener) {

View file

@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
import android.graphics.Rect; import android.graphics.Rect;
public class Options { public class Options {
private int displayId;
private int maxSize; private int maxSize;
private int bitRate; private int bitRate;
private int maxFps; private int maxFps;
@ -11,6 +12,14 @@ public class Options {
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;
public int getDisplayId() {
return displayId;
}
public void setDisplayId(int displayId) {
this.displayId = displayId;
}
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
} }

View file

@ -64,10 +64,11 @@ public class ScreenEncoder implements Device.RotationListener {
IBinder display = createDisplay(); IBinder display = createDisplay();
Rect contentRect = device.getScreenInfo().getContentRect(); Rect contentRect = device.getScreenInfo().getContentRect();
Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
int displayId = device.getScreenInfo().getDisplayId();
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, contentRect, videoRect); setDisplaySurface(display, surface, displayId, contentRect, videoRect);
codec.start(); codec.start();
try { try {
alive = encode(codec, fd); alive = encode(codec, fd);
@ -172,12 +173,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, Rect deviceRect, Rect displayRect) { private static void setDisplaySurface(IBinder display, Surface surface, int displayId, Rect deviceRect, Rect displayRect) {
SurfaceControl.openTransaction(); SurfaceControl.openTransaction();
try { try {
SurfaceControl.setDisplaySurface(display, surface); SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, 0); SurfaceControl.setDisplayLayerStack(display, displayId);
} finally { } finally {
SurfaceControl.closeTransaction(); SurfaceControl.closeTransaction();
} }

View file

@ -6,13 +6,19 @@ public final class ScreenInfo {
private final Rect contentRect; // device size, possibly cropped private final Rect contentRect; // device size, possibly cropped
private final Size videoSize; private final Size videoSize;
private final boolean rotated; private final boolean rotated;
private final int displayId;
public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { public ScreenInfo(int displayId, Rect contentRect, Size videoSize, boolean rotated) {
this.displayId = displayId;
this.contentRect = contentRect; this.contentRect = contentRect;
this.videoSize = videoSize; this.videoSize = videoSize;
this.rotated = rotated; this.rotated = rotated;
} }
public int getDisplayId() {
return displayId;
}
public Rect getContentRect() { public Rect getContentRect() {
return contentRect; return contentRect;
} }
@ -26,6 +32,6 @@ public final class ScreenInfo {
if (rotated == newRotated) { if (rotated == newRotated) {
return this; return this;
} }
return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated); return new ScreenInfo(displayId, Device.flipRect(contentRect), videoSize.rotate(), newRotated);
} }
} }

View file

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

View file

@ -12,9 +12,9 @@ public final class DisplayManager {
this.manager = manager; this.manager = manager;
} }
public DisplayInfo getDisplayInfo() { public DisplayInfo getDisplayInfo(int display) {
try { try {
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, display);
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);

View file

@ -0,0 +1,29 @@
package com.genymobile.scrcpy.wrappers;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import android.view.MotionEvent;
public final class MotionEventWrapper {
static public MotionEvent obtain(long downTime, long eventTime,
int action, int pointerCount, MotionEvent.PointerProperties[] pointerProperties,
MotionEvent.PointerCoords[] pointerCoords, int metaState, int buttonState,
float xPrecision, float yPrecision, int deviceId,
int edgeFlags, int source, int displayId, int flags) {
MotionEvent motionEvent;
try {
// Modern Android 10 (29+ API), Android R has new displayId parameter, to support multi-display touch
motionEvent = (MotionEvent)MotionEvent.class.getMethod("obtain",
long.class, long.class,
int.class, int.class, MotionEvent.PointerProperties[].class,
MotionEvent.PointerCoords[].class, int.class, int.class,
float.class, float.class, int.class,
int.class, int.class, int.class, int.class)
.invoke(null, downTime, eventTime, action, pointerCount, pointerProperties, pointerCoords, metaState, buttonState, xPrecision, yPrecision, deviceId, edgeFlags, source, displayId, flags);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
motionEvent = MotionEvent.obtain(downTime, eventTime, action, pointerCount, pointerProperties, pointerCoords, metaState, buttonState, xPrecision, yPrecision, deviceId, edgeFlags, source, flags);
}
return motionEvent;
}
}

View file

@ -93,13 +93,13 @@ public final class WindowManager {
} }
} }
public void registerRotationWatcher(IRotationWatcher rotationWatcher) { public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
try { try {
Class<?> cls = manager.getClass(); Class<?> cls = manager.getClass();
try { try {
// display parameter added since this commit: // display parameter added since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0); cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId);
} catch (NoSuchMethodException e) { } catch (NoSuchMethodException e) {
// old version // old version
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);