Add option to lock video orientation

PR #1151 <https://github.com/Genymobile/scrcpy/pull/1151>

Signed-off-by: Romain Vimont <rom@rom1v.com>
This commit is contained in:
George Stamoulis 2020-02-16 13:30:36 +02:00 committed by Romain Vimont
parent ef56cc6ff7
commit 1982bc439b
15 changed files with 221 additions and 73 deletions

View file

@ -179,6 +179,21 @@ scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
If `--max-size` is also specified, resizing is applied after cropping. If `--max-size` is also specified, resizing is applied after cropping.
#### Lock video orientation
To lock the orientation of the mirroring:
```bash
scrcpy --lock-video-orientation 0 # natural orientation
scrcpy --lock-video-orientation 1 # 90° counterclockwise
scrcpy --lock-video-orientation 2 # 180°
scrcpy --lock-video-orientation 3 # 90° clockwise
```
This affects recording orientation.
### Recording ### Recording
It is possible to record the screen while mirroring: It is possible to record the screen while mirroring:

View file

@ -105,6 +105,12 @@ conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
# overridden by option --max-size # overridden by option --max-size
conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
# the default video orientation
# natural device orientation is 0 and each increment adds 90 degrees
# counterclockwise
# overridden by option --lock-video-orientation
conf.set('DEFAULT_LOCK_VIDEO_ORIENTATION', '-1') # -1: unlocked
# the default video bitrate, in bits/second # the default video bitrate, in bits/second
# overridden by option --bit-rate # overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps

View file

@ -41,6 +41,12 @@ Start in fullscreen.
.B \-h, \-\-help .B \-h, \-\-help
Print this help. Print this help.
.TP
.BI "\-\-lock\-video\-orientation " value
Lock video orientation to \fIvalue\fR. Values are integers in the range [-1..3]. Natural device orientation is 0 and each increment adds 90 degrees counterclockwise.
Default is -1 (unlocked).
.TP .TP
.BI "\-\-max\-fps " value .BI "\-\-max\-fps " value
Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions).

View file

@ -41,6 +41,12 @@ scrcpy_print_usage(const char *arg0) {
" -h, --help\n" " -h, --help\n"
" Print this help.\n" " Print this help.\n"
"\n" "\n"
" --lock-video-orientation value\n"
" Lock video orientation to value. Values are integers in the\n"
" range [-1..3]. Natural device orientation is 0 and each\n"
" increment adds 90 degrees counterclockwise.\n"
" Default is %d%s.\n"
"\n"
" --max-fps value\n" " --max-fps value\n"
" Limit the frame rate of screen capture (officially supported\n" " Limit the frame rate of screen capture (officially supported\n"
" since Android 10, but may work on earlier versions).\n" " since Android 10, but may work on earlier versions).\n"
@ -192,6 +198,7 @@ scrcpy_print_usage(const char *arg0) {
"\n", "\n",
arg0, arg0,
DEFAULT_BIT_RATE, DEFAULT_BIT_RATE,
DEFAULT_LOCK_VIDEO_ORIENTATION, DEFAULT_LOCK_VIDEO_ORIENTATION >= 0 ? "" : " (unlocked)",
DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)",
DEFAULT_LOCAL_PORT_RANGE_FIRST, DEFAULT_LOCAL_PORT_RANGE_LAST); DEFAULT_LOCAL_PORT_RANGE_FIRST, DEFAULT_LOCAL_PORT_RANGE_LAST);
} }
@ -280,6 +287,19 @@ parse_max_fps(const char *s, uint16_t *max_fps) {
return true; return true;
} }
static bool
parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) {
long value;
bool ok = parse_integer_arg(s, &value, false, -1, 3,
"lock video orientation");
if (!ok) {
return false;
}
*lock_video_orientation = (int8_t) value;
return true;
}
static bool static bool
parse_window_position(const char *s, int16_t *position) { parse_window_position(const char *s, int16_t *position) {
long value; long value;
@ -364,51 +384,54 @@ guess_record_format(const char *filename) {
return 0; return 0;
} }
#define OPT_RENDER_EXPIRED_FRAMES 1000 #define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001 #define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002 #define OPT_PUSH_TARGET 1002
#define OPT_ALWAYS_ON_TOP 1003 #define OPT_ALWAYS_ON_TOP 1003
#define OPT_CROP 1004 #define OPT_CROP 1004
#define OPT_RECORD_FORMAT 1005 #define OPT_RECORD_FORMAT 1005
#define OPT_PREFER_TEXT 1006 #define OPT_PREFER_TEXT 1006
#define OPT_WINDOW_X 1007 #define OPT_WINDOW_X 1007
#define OPT_WINDOW_Y 1008 #define OPT_WINDOW_Y 1008
#define OPT_WINDOW_WIDTH 1009 #define OPT_WINDOW_WIDTH 1009
#define OPT_WINDOW_HEIGHT 1010 #define OPT_WINDOW_HEIGHT 1010
#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
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[]) {
static const struct option long_options[] = { static const struct option long_options[] = {
{"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},
{"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}, {"lock-video-orientation", required_argument, NULL,
{"max-size", required_argument, NULL, 'm'}, OPT_LOCK_VIDEO_ORIENTATION},
{"no-control", no_argument, NULL, 'n'}, {"max-fps", required_argument, NULL, OPT_MAX_FPS},
{"no-display", no_argument, NULL, 'N'}, {"max-size", required_argument, NULL, 'm'},
{"port", required_argument, NULL, 'p'}, {"no-control", no_argument, NULL, 'n'},
{"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"no-display", no_argument, NULL, 'N'},
{"record", required_argument, NULL, 'r'}, {"port", required_argument, NULL, 'p'},
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET},
{"render-expired-frames", no_argument, NULL, {"record", required_argument, NULL, 'r'},
OPT_RENDER_EXPIRED_FRAMES}, {"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
{"serial", required_argument, NULL, 's'}, {"render-expired-frames", no_argument, NULL,
{"show-touches", no_argument, NULL, 't'}, OPT_RENDER_EXPIRED_FRAMES},
{"turn-screen-off", no_argument, NULL, 'S'}, {"serial", required_argument, NULL, 's'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"show-touches", no_argument, NULL, 't'},
{"version", no_argument, NULL, 'v'}, {"turn-screen-off", no_argument, NULL, 'S'},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"window-x", required_argument, NULL, OPT_WINDOW_X}, {"version", no_argument, NULL, 'v'},
{"window-y", required_argument, NULL, OPT_WINDOW_Y}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
{"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, {"window-x", required_argument, NULL, OPT_WINDOW_X},
{"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, {"window-y", required_argument, NULL, OPT_WINDOW_Y},
{"window-borderless", no_argument, NULL, {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH},
OPT_WINDOW_BORDERLESS}, {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT},
{NULL, 0, NULL, 0 }, {"window-borderless", no_argument, NULL,
OPT_WINDOW_BORDERLESS},
{NULL, 0, NULL, 0 },
}; };
struct scrcpy_options *opts = &args->opts; struct scrcpy_options *opts = &args->opts;
@ -454,6 +477,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
return false; return false;
} }
break; break;
case OPT_LOCK_VIDEO_ORIENTATION:
if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) {
return false;
}
break;
case 'n': case 'n':
opts->control = false; opts->control = false;
break; break;

View file

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

View file

@ -20,6 +20,7 @@ struct scrcpy_options {
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;
int8_t lock_video_orientation;
int16_t window_x; int16_t window_x;
int16_t window_y; int16_t window_y;
uint16_t window_width; uint16_t window_width;
@ -49,6 +50,7 @@ struct scrcpy_options {
.max_size = DEFAULT_MAX_SIZE, \ .max_size = DEFAULT_MAX_SIZE, \
.bit_rate = DEFAULT_BIT_RATE, \ .bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \ .max_fps = 0, \
.lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \
.window_x = -1, \ .window_x = -1, \
.window_y = -1, \ .window_y = -1, \
.window_width = 0, \ .window_width = 0, \

View file

@ -233,9 +233,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 lock_video_orientation_string[3];
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);
const char *const cmd[] = { const char *const cmd[] = {
"shell", "shell",
"CLASSPATH=" DEVICE_SERVER_PATH, "CLASSPATH=" DEVICE_SERVER_PATH,
@ -251,6 +253,7 @@ execute_server(struct server *server, const struct server_params *params) {
max_size_string, max_size_string,
bit_rate_string, bit_rate_string,
max_fps_string, max_fps_string,
lock_video_orientation_string,
server->tunnel_forward ? "true" : "false", server->tunnel_forward ? "true" : "false",
params->crop ? params->crop : "-", params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp) "true", // always send frame meta (packet boundaries + timestamp)

View file

@ -42,6 +42,7 @@ struct server_params {
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;
int8_t lock_video_orientation;
bool control; bool control;
}; };

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",
"--lock-video-orientation", "2",
// "--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:1236", "--port", "1234:1236",
@ -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->lock_video_orientation == 2);
assert(opts->port_range.first == 1234); assert(opts->port_range.first == 1234);
assert(opts->port_range.last == 1236); assert(opts->port_range.last == 1236);
assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->push_target, "/sdcard/Movies"));

View file

@ -22,10 +22,12 @@ public final class Device {
private final ServiceManager serviceManager = new ServiceManager(); private final ServiceManager serviceManager = new ServiceManager();
private final int lockedVideoOrientation;
private ScreenInfo screenInfo; private ScreenInfo screenInfo;
private RotationListener rotationListener; private RotationListener rotationListener;
public Device(Options options) { public Device(Options options) {
lockedVideoOrientation = options.getLockedVideoOrientation();
screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize());
registerRotationWatcher(new IRotationWatcher.Stub() { registerRotationWatcher(new IRotationWatcher.Stub() {
@Override @Override
@ -48,11 +50,11 @@ public final class Device {
private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { private ScreenInfo computeScreenInfo(Rect crop, int maxSize) {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
boolean rotated = (displayInfo.getRotation() & 1) != 0; int rotation = displayInfo.getRotation();
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());
if (crop != null) { if (crop != null) {
if (rotated) { if (rotation % 2 != 0) { // 180s preserve dimensions
// the crop (provided by the user) is expressed in the natural orientation // the crop (provided by the user) is expressed in the natural orientation
crop = flipRect(crop); crop = flipRect(crop);
} }
@ -64,7 +66,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(contentRect, videoSize, rotation);
} }
private static String formatCrop(Rect rect) { private static String formatCrop(Rect rect) {
@ -99,22 +101,56 @@ public final class Device {
return new Size(w, h); return new Size(w, h);
} }
/**
* Return the rotation to apply to the device rotation to get the requested locked video orientation
*
* @param deviceRotation the device rotation
* @return the rotation offset
*/
public int getVideoRotation(int deviceRotation) {
if (lockedVideoOrientation == -1) {
// no offset
return 0;
}
return (deviceRotation + 4 - lockedVideoOrientation) % 4;
}
/**
* Return the rotation to apply to the requested locked video orientation to get the device rotation
*
* @param deviceRotation the device rotation
* @return the (reverse) rotation offset
*/
private int getReverseVideoRotation(int deviceRotation) {
if (lockedVideoOrientation == -1) {
// no offset
return 0;
}
return (lockedVideoOrientation + 4 - deviceRotation) % 4;
}
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")
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
Size videoSize = screenInfo.getVideoSize(); Size videoSize = screenInfo.getVideoSize();
Size clientVideoSize = position.getScreenSize();
int deviceRotation = screenInfo.getRotation();
int reverseVideoRotation = getReverseVideoRotation(deviceRotation);
// reverse the video rotation to apply the events
Position devicePosition = position.rotate(reverseVideoRotation);
Size clientVideoSize = devicePosition.getScreenSize();
if (!videoSize.equals(clientVideoSize)) { if (!videoSize.equals(clientVideoSize)) {
// The client sends a click relative to a video with wrong dimensions, // The client sends a click relative to a video with wrong dimensions,
// the device may have been rotated since the event was generated, so ignore the event // the device may have been rotated since the event was generated, so ignore the event
return null; return null;
} }
Rect contentRect = screenInfo.getContentRect(); Rect contentRect = screenInfo.getContentRect();
Point point = position.getPoint(); Point point = devicePosition.getPoint();
int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); int convertedX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth();
int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); int convertedY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight();
return new Point(scaledX, scaledY); return new Point(convertedX, convertedY);
} }
public static String getDeviceName() { public static String getDeviceName() {

View file

@ -6,6 +6,7 @@ public class Options {
private int maxSize; private int maxSize;
private int bitRate; private int bitRate;
private int maxFps; private int maxFps;
private int lockedVideoOrientation;
private boolean tunnelForward; private boolean tunnelForward;
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
@ -35,6 +36,14 @@ public class Options {
this.maxFps = maxFps; this.maxFps = maxFps;
} }
public int getLockedVideoOrientation() {
return lockedVideoOrientation;
}
public void setLockedVideoOrientation(int lockedVideoOrientation) {
this.lockedVideoOrientation = lockedVideoOrientation;
}
public boolean isTunnelForward() { public boolean isTunnelForward() {
return tunnelForward; return tunnelForward;
} }

View file

@ -23,6 +23,19 @@ public class Position {
return screenSize; return screenSize;
} }
public Position rotate(int rotation) {
switch (rotation) {
case 1:
return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate());
case 2:
return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize);
case 3:
return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate());
default:
return this;
}
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) { if (this == o) {

View file

@ -28,19 +28,21 @@ public class ScreenEncoder implements Device.RotationListener {
private int bitRate; private int bitRate;
private int maxFps; private int maxFps;
private int lockedVideoOrientation;
private int iFrameInterval; private int iFrameInterval;
private boolean sendFrameMeta; private boolean sendFrameMeta;
private long ptsOrigin; private long ptsOrigin;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, int iFrameInterval) {
this.sendFrameMeta = sendFrameMeta; this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate; this.bitRate = bitRate;
this.maxFps = maxFps; this.maxFps = maxFps;
this.lockedVideoOrientation = lockedVideoOrientation;
this.iFrameInterval = iFrameInterval; this.iFrameInterval = iFrameInterval;
} }
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation) {
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); this(sendFrameMeta, bitRate, maxFps, lockedVideoOrientation, DEFAULT_I_FRAME_INTERVAL);
} }
@Override @Override
@ -66,10 +68,11 @@ public class ScreenEncoder implements Device.RotationListener {
ScreenInfo screenInfo = device.getScreenInfo(); ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect(); Rect contentRect = screenInfo.getContentRect();
Rect videoRect = screenInfo.getVideoSize().toRect(); Rect videoRect = screenInfo.getVideoSize().toRect();
setSize(format, videoRect.width(), videoRect.height()); int videoRotation = device.getVideoRotation(screenInfo.getRotation());
setSize(format, videoRotation, 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, videoRotation, contentRect, videoRect);
codec.start(); codec.start();
try { try {
alive = encode(codec, fd); alive = encode(codec, fd);
@ -168,16 +171,21 @@ public class ScreenEncoder implements Device.RotationListener {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} }
private static void setSize(MediaFormat format, int width, int height) { private static void setSize(MediaFormat format, int orientation, int width, int height) {
format.setInteger(MediaFormat.KEY_WIDTH, width); if (orientation % 2 == 0) {
format.setInteger(MediaFormat.KEY_HEIGHT, height); format.setInteger(MediaFormat.KEY_WIDTH, width);
format.setInteger(MediaFormat.KEY_HEIGHT, height);
return;
}
format.setInteger(MediaFormat.KEY_WIDTH, height);
format.setInteger(MediaFormat.KEY_HEIGHT, width);
} }
private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) { private static void setDisplaySurface(IBinder display, Surface surface, int orientation, 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, orientation, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, 0); SurfaceControl.setDisplayLayerStack(display, 0);
} finally { } finally {
SurfaceControl.closeTransaction(); SurfaceControl.closeTransaction();

View file

@ -5,12 +5,12 @@ import android.graphics.Rect;
public final class ScreenInfo { 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 int rotation;
public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { public ScreenInfo(Rect contentRect, Size videoSize, int rotation) {
this.contentRect = contentRect; this.contentRect = contentRect;
this.videoSize = videoSize; this.videoSize = videoSize;
this.rotated = rotated; this.rotation = rotation;
} }
public Rect getContentRect() { public Rect getContentRect() {
@ -21,11 +21,25 @@ public final class ScreenInfo {
return videoSize; return videoSize;
} }
public ScreenInfo withRotation(int rotation) { public int getRotation() {
boolean newRotated = (rotation & 1) != 0; return rotation;
if (rotated == newRotated) { }
public ScreenInfo withRotation(int newRotation) {
if (newRotation == rotation) {
return this; return this;
} }
return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated); // true if changed between portrait and landscape
boolean orientationChanged = (rotation + newRotation) % 2 != 0;
Rect newContentRect;
Size newVideoSize;
if (orientationChanged) {
newContentRect = Device.flipRect(contentRect);
newVideoSize = videoSize.rotate();
} else {
newContentRect = contentRect;
newVideoSize = videoSize;
}
return new ScreenInfo(newContentRect, newVideoSize, newRotation);
} }
} }

View file

@ -19,7 +19,8 @@ public final class Server {
final Device device = new Device(options); final Device device = new Device(options);
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(),
options.getLockedVideoOrientation());
if (options.getControl()) { if (options.getControl()) {
Controller controller = new Controller(device, connection); Controller controller = new Controller(device, connection);
@ -79,8 +80,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();
@ -94,17 +95,20 @@ public final class Server {
int maxFps = Integer.parseInt(args[3]); int maxFps = Integer.parseInt(args[3]);
options.setMaxFps(maxFps); options.setMaxFps(maxFps);
int lockedVideoOrientation = Integer.parseInt(args[4]);
options.setLockedVideoOrientation(lockedVideoOrientation);
// use "adb forward" instead of "adb tunnel"? (so the server must listen) // use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[4]); boolean tunnelForward = Boolean.parseBoolean(args[5]);
options.setTunnelForward(tunnelForward); options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[5]); Rect crop = parseCrop(args[6]);
options.setCrop(crop); options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[6]); boolean sendFrameMeta = Boolean.parseBoolean(args[7]);
options.setSendFrameMeta(sendFrameMeta); options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[7]); boolean control = Boolean.parseBoolean(args[8]);
options.setControl(control); options.setControl(control);
return options; return options;