Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multi-display Android system #1272

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
# overridden by option --bit-rate
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
conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))

Expand Down
26 changes: 25 additions & 1 deletion app/src/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ 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"
" -d, --display value\n"
" Use specified display in multi-display Android systems.\n"
" Default is %d.\n"
"\n"
" -f, --fullscreen\n"
" Start in fullscreen.\n"
"\n"
Expand Down Expand Up @@ -192,6 +196,7 @@ scrcpy_print_usage(const char *arg0) {
"\n",
arg0,
DEFAULT_BIT_RATE,
DEFAULT_DISPLAY_ID,
DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)",
DEFAULT_LOCAL_PORT);
}
Expand Down Expand Up @@ -235,6 +240,19 @@ parse_bit_rate(const char *s, uint32_t *bit_rate) {
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
parse_max_size(const char *s, uint16_t *max_size) {
long value;
Expand Down Expand Up @@ -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},
{"bit-rate", required_argument, NULL, 'b'},
{"crop", required_argument, NULL, OPT_CROP},
{"display", required_argument, NULL, 'd'},
{"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'},
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
Expand Down Expand Up @@ -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

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) {
switch (c) {
case 'b':
Expand All @@ -390,6 +409,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case 'c':
LOGW("Deprecated option -c. Use --crop instead.");
// fall through
case 'd':
if (!parse_display_id(optarg, &opts->display_id)) {
return false;
}
break;
case OPT_CROP:
opts->crop = optarg;
break;
Expand Down
1 change: 1 addition & 0 deletions app/src/scrcpy.c
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ scrcpy(const struct scrcpy_options *options) {
.bit_rate = options->bit_rate,
.max_fps = options->max_fps,
.control = options->control,
.display_id = options->display_id,
};
if (!server_start(&server, options->serial, &params)) {
return false;
Expand Down
2 changes: 2 additions & 0 deletions app/src/scrcpy.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct scrcpy_options {
uint16_t port;
uint16_t max_size;
uint32_t bit_rate;
uint16_t display_id;
uint16_t max_fps;
int16_t window_x;
int16_t window_y;
Expand All @@ -44,6 +45,7 @@ struct scrcpy_options {
.port = DEFAULT_LOCAL_PORT, \
.max_size = DEFAULT_MAX_SIZE, \
.bit_rate = DEFAULT_BIT_RATE, \
.display_id = DEFAULT_DISPLAY_ID, \
.max_fps = 0, \
.window_x = -1, \
.window_y = -1, \
Expand Down
3 changes: 3 additions & 0 deletions app/src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,11 @@ execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6];
char bit_rate_string[11];
char max_fps_string[6];
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(display_id_string, "%"PRIu16, params->display_id);
const char *const cmd[] = {
"shell",
"CLASSPATH=" DEVICE_SERVER_PATH,
Expand All @@ -146,6 +148,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 "
Expand Down
1 change: 1 addition & 0 deletions app/src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct server_params {
uint32_t bit_rate;
uint16_t max_fps;
bool control;
uint16_t display_id;
};

// init default values
Expand Down
2 changes: 2 additions & 0 deletions app/tests/test_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ static void test_options(void) {
"--fullscreen",
"--max-fps", "30",
"--max-size", "1024",
"--display", "1",
// "--no-control" is not compatible with "--turn-screen-off"
// "--no-display" is not compatible with "--fulscreen"
"--port", "1234",
Expand Down Expand Up @@ -78,6 +79,7 @@ static void test_options(void) {
assert(opts->fullscreen);
assert(opts->max_fps == 30);
assert(opts->max_size == 1024);
assert(opts->display_id == 1);
assert(opts->port == 1234);
assert(!strcmp(opts->push_target, "/sdcard/Movies"));
assert(!strcmp(opts->record_filename, "file"));
Expand Down
10 changes: 6 additions & 4 deletions server/src/main/java/com/genymobile/scrcpy/Controller.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.genymobile.scrcpy;

import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.MotionEventWrapper;

import android.os.SystemClock;
import android.view.InputDevice;
Expand Down Expand Up @@ -179,9 +180,10 @@ private boolean injectTouch(int action, long pointerId, Position position, float
}
}

MotionEvent event = MotionEvent
MotionEvent event = MotionEventWrapper
.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);
}

Expand All @@ -202,9 +204,9 @@ private boolean injectScroll(Position position, int hScroll, int vScroll) {
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
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,
InputDevice.SOURCE_MOUSE, 0);
InputDevice.SOURCE_MOUSE, device.getScreenInfo().getDisplayId(), 0);
return injectEvent(event);
}

Expand Down
15 changes: 8 additions & 7 deletions server/src/main/java/com/genymobile/scrcpy/Device.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public interface RotationListener {
private RotationListener rotationListener;

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() {
@Override
public void onRotationChanged(int rotation) throws RemoteException {
Expand All @@ -39,15 +40,15 @@ public void onRotationChanged(int rotation) throws RemoteException {
}
}
}
});
}, displayId);
}

public synchronized ScreenInfo getScreenInfo() {
return screenInfo;
}

private ScreenInfo computeScreenInfo(Rect crop, int maxSize) {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
private ScreenInfo computeScreenInfo(int displayId, Rect crop, int maxSize) {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId);
boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize();
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
Expand All @@ -64,7 +65,7 @@ private ScreenInfo computeScreenInfo(Rect crop, int 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) {
Expand Down Expand Up @@ -129,8 +130,8 @@ public boolean isScreenOn() {
return serviceManager.getPowerManager().isScreenOn();
}

public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher);
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
}

public synchronized void setRotationListener(RotationListener rotationListener) {
Expand Down
9 changes: 9 additions & 0 deletions server/src/main/java/com/genymobile/scrcpy/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.graphics.Rect;

public class Options {
private int displayId;
private int maxSize;
private int bitRate;
private int maxFps;
Expand All @@ -11,6 +12,14 @@ public class Options {
private boolean sendFrameMeta; // send PTS so that the client may record properly
private boolean control;

public int getDisplayId() {
return displayId;
}

public void setDisplayId(int displayId) {
this.displayId = displayId;
}

public int getMaxSize() {
return maxSize;
}
Expand Down
7 changes: 4 additions & 3 deletions server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ public void streamScreen(Device device, FileDescriptor fd) throws IOException {
IBinder display = createDisplay();
Rect contentRect = device.getScreenInfo().getContentRect();
Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
int displayId = device.getScreenInfo().getDisplayId();
setSize(format, videoRect.width(), videoRect.height());
configure(codec, format);
Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, contentRect, videoRect);
setDisplaySurface(display, surface, displayId, contentRect, videoRect);
codec.start();
try {
alive = encode(codec, fd);
Expand Down Expand Up @@ -172,12 +173,12 @@ private static void setSize(MediaFormat format, int width, int 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();
try {
SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, 0);
SurfaceControl.setDisplayLayerStack(display, displayId);
} finally {
SurfaceControl.closeTransaction();
}
Expand Down
10 changes: 8 additions & 2 deletions server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ public final class ScreenInfo {
private final Rect contentRect; // device size, possibly cropped
private final Size videoSize;
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.videoSize = videoSize;
this.rotated = rotated;
}

public int getDisplayId() {
return displayId;
}

public Rect getContentRect() {
return contentRect;
}
Expand All @@ -26,6 +32,6 @@ public ScreenInfo withRotation(int rotation) {
if (rotated == newRotated) {
return this;
}
return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated);
return new ScreenInfo(displayId, Device.flipRect(contentRect), videoSize.rotate(), newRotated);
}
}
7 changes: 5 additions & 2 deletions server/src/main/java/com/genymobile/scrcpy/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ private static Options createOptions(String... args) {
"The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")");
}

if (args.length != 8) {
throw new IllegalArgumentException("Expecting 8 parameters");
if (args.length != 9) {
throw new IllegalArgumentException("Expecting 9 parameters");
}

Options options = new Options();
Expand All @@ -107,6 +107,9 @@ private static Options createOptions(String... args) {
boolean control = Boolean.parseBoolean(args[7]);
options.setControl(control);

int displayId = Integer.parseInt(args[8]);
options.setDisplayId(displayId);

return options;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public DisplayManager(IInterface manager) {
this.manager = manager;
}

public DisplayInfo getDisplayInfo() {
public DisplayInfo getDisplayInfo(int display) {
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();
// width and height already take the rotation into account
int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,13 @@ public void thawRotation() {
}
}

public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
try {
Class<?> cls = manager.getClass();
try {
// display parameter added since this commit:
// 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) {
// old version
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
Expand Down