Handle resized video stream

Accept a parameter to limit the video size.

For instance, with "-m 960", the great side of the video will be scaled
down to 960 (if necessary), while the other side will be scaled down so
that the aspect ratio is preserved. Both dimensions must be a multiple
of 8, so black bands might be added, and the mouse positions must be
computed accordingly.
This commit is contained in:
Romain Vimont 2018-01-29 15:40:33 +01:00
parent 2c4ea6869e
commit 89f6a3cfe7
18 changed files with 274 additions and 87 deletions

View file

@ -9,15 +9,16 @@
struct args {
const char *serial;
Uint16 port;
Uint16 maximum_size;
};
int parse_args(struct args *args, int argc, char *argv[]) {
int c;
while ((c = getopt(argc, argv, "p:")) != -1) {
while ((c = getopt(argc, argv, "p:m:")) != -1) {
switch (c) {
case 'p': {
char *endptr;
long int value = strtol(optarg, &endptr, 0);
long value = strtol(optarg, &endptr, 0);
if (*optarg == '\0' || *endptr != '\0') {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid port: %s\n", optarg);
return -1;
@ -29,6 +30,20 @@ int parse_args(struct args *args, int argc, char *argv[]) {
args->port = (Uint16) value;
break;
}
case 'm': {
char *endptr;
long value = strtol(optarg, &endptr, 0);
if (*optarg == '\0' || *endptr != '\0') {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid maximum size: %s\n", optarg);
return -1;
}
if (value & ~0xffff) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Maximum size must be between 0 and 65535: %ld\n", value);
return -1;
}
args->maximum_size = (Uint16) value;
break;
}
default:
// getopt prints the error message on stderr
return -1;
@ -65,7 +80,7 @@ int main(int argc, char *argv[]) {
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
res = scrcpy(args.serial, args.port) ? 0 : 1;
res = scrcpy(args.serial, args.port, args.maximum_size) ? 0 : 1;
avformat_network_deinit(); // ignore failure

View file

@ -382,7 +382,7 @@ void event_loop(void) {
}
}
SDL_bool scrcpy(const char *serial, Uint16 local_port) {
SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 maximum_size) {
SDL_bool ret = 0;
process_t push_proc = push_server(serial);
@ -402,7 +402,7 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port) {
}
// server will connect to our socket
process_t server = start_server(serial);
process_t server = start_server(serial, maximum_size);
if (server == PROCESS_NONE) {
ret = SDL_FALSE;
SDLNet_TCP_Close(server_socket);

View file

@ -3,6 +3,6 @@
#include <SDL2/SDL_stdinc.h>
SDL_bool scrcpy(const char *serial, Uint16 local_port);
SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 maximum_size);
#endif

View file

@ -21,13 +21,16 @@ process_t disable_tunnel(const char *serial) {
return adb_reverse_remove(serial, SOCKET_NAME);
}
process_t start_server(const char *serial) {
process_t start_server(const char *serial, Uint16 maximum_size) {
char maximum_size_string[6];
sprintf(maximum_size_string, "%d", maximum_size);
const char *const cmd[] = {
"shell",
"CLASSPATH=/data/local/tmp/scrcpy-server.jar",
"app_process",
"/system/bin",
"com.genymobile.scrcpy.ScrCpyServer"
"com.genymobile.scrcpy.ScrCpyServer",
maximum_size_string,
};
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
}

View file

@ -4,5 +4,5 @@ process_t push_server(const char *serial);
process_t enable_tunnel(const char *serial, Uint16 local_port);
process_t disable_tunnel(const char *serial);
process_t start_server(const char *serial);
process_t start_server(const char *serial, Uint16 maximum_size);
void stop_server(process_t server);

View file

@ -24,13 +24,16 @@ SRC := com/genymobile/scrcpy/ScrCpyServer.java \
com/genymobile/scrcpy/ControlEventReader.java \
com/genymobile/scrcpy/DesktopConnection.java \
com/genymobile/scrcpy/Device.java \
com/genymobile/scrcpy/DisplayInfo.java \
com/genymobile/scrcpy/EventController.java \
com/genymobile/scrcpy/Ln.java \
com/genymobile/scrcpy/Options.java \
com/genymobile/scrcpy/Point.java \
com/genymobile/scrcpy/Position.java \
com/genymobile/scrcpy/ScreenInfo.java \
com/genymobile/scrcpy/ScreenStreamer.java \
com/genymobile/scrcpy/ScreenStreamerSession.java \
com/genymobile/scrcpy/Size.java \
com/genymobile/scrcpy/wrappers/DisplayManager.java \
com/genymobile/scrcpy/wrappers/InputManager.java \
com/genymobile/scrcpy/wrappers/ServiceManager.java \

View file

@ -36,12 +36,9 @@ public class DesktopConnection implements Closeable {
public static DesktopConnection open(Device device) throws IOException {
LocalSocket socket = connect(SOCKET_NAME);
ScreenInfo initialScreenInfo = device.getScreenInfo();
int width = initialScreenInfo.getLogicalWidth();
int height = initialScreenInfo.getLogicalHeight();
DesktopConnection connection = new DesktopConnection(socket);
connection.send(Device.getDeviceName(), width, height);
Size videoSize = device.getScreenInfo().getVideoSize();
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
return connection;
}
@ -81,4 +78,3 @@ public class DesktopConnection implements Closeable {
return event;
}
}

View file

@ -7,7 +7,7 @@ import android.view.IRotationWatcher;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
public class Device {
public final class Device {
public interface RotationListener {
void onRotationChanged(int rotation);
@ -18,13 +18,12 @@ public class Device {
private ScreenInfo screenInfo;
private RotationListener rotationListener;
public Device() {
screenInfo = readScreenInfo();
public Device(Options options) {
screenInfo = computeScreenInfo(options.getMaximumSize());
registerRotationWatcher(new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) throws RemoteException {
synchronized (Device.this) {
// update screenInfo cache
screenInfo = screenInfo.withRotation(rotation);
// notify
@ -37,23 +36,59 @@ public class Device {
}
public synchronized ScreenInfo getScreenInfo() {
if (screenInfo == null) {
screenInfo = readScreenInfo();
}
return screenInfo;
}
public Point getPhysicalPoint(Position position) {
ScreenInfo screenInfo = getScreenInfo();
int deviceWidth = screenInfo.getLogicalWidth();
int deviceHeight = screenInfo.getLogicalHeight();
int scaledX = position.getX() * deviceWidth / position.getScreenWidth();
int scaledY = position.getY() * deviceHeight / position.getScreenHeight();
return new Point(scaledX, scaledY);
private ScreenInfo computeScreenInfo(int maximumSize) {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize();
int w = deviceSize.getWidth();
int h = deviceSize.getHeight();
int padding = 0;
if (maximumSize > 0) {
assert maximumSize % 8 == 0;
boolean portrait = h > w;
int major = portrait ? h : w;
int minor = portrait ? w : h;
if (major > maximumSize) {
int minorExact = minor * maximumSize / major;
// +7 to ceil the value on rounding to the next multiple of 8,
// so that any necessary black bands to keep the aspect ratio are added to the smallest dimension
minor = (minorExact + 7) & ~7;
major = maximumSize;
padding = minor - minorExact;
}
w = portrait ? minor : major;
h = portrait ? major : minor;
}
return new ScreenInfo(deviceSize, new Size(w, h), padding, rotated);
}
private ScreenInfo readScreenInfo() {
return serviceManager.getDisplayManager().getScreenInfo();
public Point getPhysicalPoint(Position position) {
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
Size videoSize = screenInfo.getVideoSize();
Size clientVideoSize = position.getScreenSize();
if (!videoSize.equals(clientVideoSize)) {
// 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
return null;
}
Size deviceSize = screenInfo.getDeviceSize();
int xPadding = screenInfo.getXPadding();
int yPadding = screenInfo.getYPadding();
int contentWidth = videoSize.getWidth() - xPadding;
int contentHeight = videoSize.getHeight() - yPadding;
Point point = position.getPoint();
int x = point.getX() - xPadding / 2;
int y = point.getY() - yPadding / 2;
if (x < 0 || x >= contentWidth || y < 0 || y >= contentHeight) {
// out of screen
return null;
}
int scaledX = x * deviceSize.getWidth() / videoSize.getWidth();
int scaledY = y * deviceSize.getHeight() / videoSize.getHeight();
return new Point(scaledX, scaledY);
}
public static String getDeviceName() {

View file

@ -0,0 +1,20 @@
package com.genymobile.scrcpy;
public final class DisplayInfo {
private final Size size;
private final int rotation;
public DisplayInfo(Size size, int rotation) {
this.size = size;
this.rotation = rotation;
}
public Size getSize() {
return size;
}
public int getRotation() {
return rotation;
}
}

View file

@ -0,0 +1,13 @@
package com.genymobile.scrcpy;
public class Options {
private int maximumSize;
public int getMaximumSize() {
return maximumSize;
}
public void setMaximumSize(int maximumSize) {
this.maximumSize = maximumSize;
}
}

View file

@ -1,5 +1,7 @@
package com.genymobile.scrcpy;
import java.util.Objects;
public class Point {
private int x;
private int y;
@ -17,6 +19,20 @@ public class Point {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x &&
y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{" +

View file

@ -1,42 +1,48 @@
package com.genymobile.scrcpy;
public class Position {
import java.util.Objects;
private int x;
private int y;
private int screenWidth;
private int screenHeight;
public class Position {
private Point point;
private Size screenSize;
public Position(Point point, Size screenSize) {
this.point = point;
this.screenSize = screenSize;
}
public Position(int x, int y, int screenWidth, int screenHeight) {
this.x = x;
this.y = y;
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
this(new Point(x, y), new Size(screenWidth, screenHeight));
}
public int getX() {
return x;
public Point getPoint() {
return point;
}
public int getY() {
return y;
public Size getScreenSize() {
return screenSize;
}
public int getScreenWidth() {
return screenWidth;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Position position = (Position) o;
return Objects.equals(point, position.point) &&
Objects.equals(screenSize, position.screenSize);
}
public int getScreenHeight() {
return screenHeight;
@Override
public int hashCode() {
return Objects.hash(point, screenSize);
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
", screenWidth=" + screenWidth +
", screenHeight=" + screenHeight +
return "Position{" +
"point=" + point +
", screenSize=" + screenSize +
'}';
}
}

View file

@ -6,10 +6,10 @@ public class ScrCpyServer {
private static final String TAG = "scrcpy";
private static void scrcpy() throws IOException {
final Device device = new Device();
private static void scrcpy(Options options) throws IOException {
final Device device = new Device(options);
try (DesktopConnection connection = DesktopConnection.open(device)) {
final ScreenStreamer streamer = new ScreenStreamer(connection);
final ScreenStreamer streamer = new ScreenStreamer(device, connection);
device.setRotationListener(new Device.RotationListener() {
@Override
public void onRotationChanged(int rotation) {
@ -43,8 +43,13 @@ public class ScrCpyServer {
}
public static void main(String... args) throws Exception {
Options options = new Options();
if (args.length > 0) {
int maximumSize = Integer.parseInt(args[0]) & ~7; // multiple of 8
options.setMaximumSize(maximumSize);
}
try {
scrcpy();
scrcpy(options);
} catch (Throwable t) {
t.printStackTrace();
throw t;

View file

@ -1,26 +1,39 @@
package com.genymobile.scrcpy;
public class ScreenInfo {
private final int width;
private final int height;
private int rotation;
public final class ScreenInfo {
private final Size deviceSize;
private final Size videoSize;
private final int padding; // padding inside the video stream, along the smallest dimension
private final boolean rotated;
public ScreenInfo(int width, int height, int rotation) {
this.width = width;
this.height = height;
this.rotation = rotation;
public ScreenInfo(Size deviceSize, Size videoSize, int padding, boolean rotated) {
this.deviceSize = deviceSize;
this.videoSize = videoSize;
this.padding = padding;
this.rotated = rotated;
}
public Size getDeviceSize() {
return deviceSize;
}
public Size getVideoSize() {
return videoSize;
}
public int getXPadding() {
return videoSize.getWidth() < videoSize.getHeight() ? padding : 0;
}
public int getYPadding() {
return videoSize.getHeight() < videoSize.getWidth() ? padding : 0;
}
public ScreenInfo withRotation(int rotation) {
return new ScreenInfo(width, height, rotation);
}
public int getLogicalWidth() {
return (rotation & 1) == 0 ? width : height;
}
public int getLogicalHeight() {
return (rotation & 1) == 0 ? height : width;
boolean newRotated = (rotation & 1) != 0;
if (rotated == newRotated) {
return this;
}
return new ScreenInfo(deviceSize.rotate(), videoSize.rotate(), padding, newRotated);
}
}

View file

@ -5,15 +5,17 @@ import java.io.InterruptedIOException;
public class ScreenStreamer {
private final Device device;
private final DesktopConnection connection;
private ScreenStreamerSession currentStreamer; // protected by 'this'
public ScreenStreamer(DesktopConnection connection) {
public ScreenStreamer(Device device, DesktopConnection connection) {
this.device = device;
this.connection = connection;
}
private synchronized ScreenStreamerSession newScreenStreamerSession() {
currentStreamer = new ScreenStreamerSession(connection);
currentStreamer = new ScreenStreamerSession(device, connection);
return currentStreamer;
}

View file

@ -2,16 +2,20 @@ package com.genymobile.scrcpy;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenStreamerSession {
private final Device device;
private final DesktopConnection connection;
private Process screenRecordProcess; // protected by 'this'
private final AtomicBoolean stopped = new AtomicBoolean();
private final byte[] buffer = new byte[0x10000];
public ScreenStreamerSession(DesktopConnection connection) {
public ScreenStreamerSession(Device device, DesktopConnection connection) {
this.device = device;
this.connection = connection;
}
@ -28,7 +32,8 @@ public class ScreenStreamerSession {
*/
private boolean streamScreenOnce() throws IOException {
Ln.d("Recording...");
Process process = startScreenRecord();
Size videoSize = device.getScreenInfo().getVideoSize();
Process process = startScreenRecord(videoSize);
setCurrentProcess(process);
InputStream inputStream = process.getInputStream();
int r;
@ -44,8 +49,15 @@ public class ScreenStreamerSession {
killCurrentProcess();
}
private static Process startScreenRecord() throws IOException {
Process process = new ProcessBuilder("screenrecord", "--output-format=h264", "-").start();
private static Process startScreenRecord(Size videoSize) throws IOException {
List<String> command = new ArrayList<>();
command.add("screenrecord");
command.add("--output-format=h264");
if (videoSize != null) {
command.add("--size=" + videoSize.getWidth() + "x" + videoSize.getHeight());
}
command.add("-");
Process process = new ProcessBuilder(command).start();
process.getOutputStream().close();
return process;
}

View file

@ -0,0 +1,47 @@
package com.genymobile.scrcpy;
import java.util.Objects;
public final class Size {
private final int width;
private final int height;
public Size(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Size rotate() {
return new Size(height, width);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Size size = (Size) o;
return width == size.width &&
height == size.height;
}
@Override
public int hashCode() {
return Objects.hash(width, height);
}
@Override
public String toString() {
return "Size{" +
"width=" + width +
", height=" + height +
'}';
}
}

View file

@ -2,7 +2,8 @@ package com.genymobile.scrcpy.wrappers;
import android.os.IInterface;
import com.genymobile.scrcpy.ScreenInfo;
import com.genymobile.scrcpy.DisplayInfo;
import com.genymobile.scrcpy.Size;
public class DisplayManager {
private final IInterface manager;
@ -11,15 +12,15 @@ public class DisplayManager {
this.manager = manager;
}
public ScreenInfo getScreenInfo() {
public DisplayInfo getDisplayInfo() {
try {
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0);
Class<?> cls = displayInfo.getClass();
// width and height do not depend on the rotation
int width = (Integer) cls.getMethod("getNaturalWidth").invoke(displayInfo);
int height = (Integer) cls.getMethod("getNaturalHeight").invoke(displayInfo);
// 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 ScreenInfo(width, height, rotation);
return new DisplayInfo(new Size(width, height), rotation);
} catch (Exception e) {
throw new AssertionError(e);
}