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 { struct args {
const char *serial; const char *serial;
Uint16 port; Uint16 port;
Uint16 maximum_size;
}; };
int parse_args(struct args *args, int argc, char *argv[]) { int parse_args(struct args *args, int argc, char *argv[]) {
int c; int c;
while ((c = getopt(argc, argv, "p:")) != -1) { while ((c = getopt(argc, argv, "p:m:")) != -1) {
switch (c) { switch (c) {
case 'p': { case 'p': {
char *endptr; char *endptr;
long int value = strtol(optarg, &endptr, 0); long value = strtol(optarg, &endptr, 0);
if (*optarg == '\0' || *endptr != '\0') { if (*optarg == '\0' || *endptr != '\0') {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid port: %s\n", optarg); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid port: %s\n", optarg);
return -1; return -1;
@ -29,6 +30,20 @@ int parse_args(struct args *args, int argc, char *argv[]) {
args->port = (Uint16) value; args->port = (Uint16) value;
break; 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: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return -1; return -1;
@ -65,7 +80,7 @@ int main(int argc, char *argv[]) {
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); 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 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; SDL_bool ret = 0;
process_t push_proc = push_server(serial); 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 // server will connect to our socket
process_t server = start_server(serial); process_t server = start_server(serial, maximum_size);
if (server == PROCESS_NONE) { if (server == PROCESS_NONE) {
ret = SDL_FALSE; ret = SDL_FALSE;
SDLNet_TCP_Close(server_socket); SDLNet_TCP_Close(server_socket);

View file

@ -3,6 +3,6 @@
#include <SDL2/SDL_stdinc.h> #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 #endif

View file

@ -21,13 +21,16 @@ process_t disable_tunnel(const char *serial) {
return adb_reverse_remove(serial, SOCKET_NAME); 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[] = { const char *const cmd[] = {
"shell", "shell",
"CLASSPATH=/data/local/tmp/scrcpy-server.jar", "CLASSPATH=/data/local/tmp/scrcpy-server.jar",
"app_process", "app_process",
"/system/bin", "/system/bin",
"com.genymobile.scrcpy.ScrCpyServer" "com.genymobile.scrcpy.ScrCpyServer",
maximum_size_string,
}; };
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0])); 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 enable_tunnel(const char *serial, Uint16 local_port);
process_t disable_tunnel(const char *serial); 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); 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/ControlEventReader.java \
com/genymobile/scrcpy/DesktopConnection.java \ com/genymobile/scrcpy/DesktopConnection.java \
com/genymobile/scrcpy/Device.java \ com/genymobile/scrcpy/Device.java \
com/genymobile/scrcpy/DisplayInfo.java \
com/genymobile/scrcpy/EventController.java \ com/genymobile/scrcpy/EventController.java \
com/genymobile/scrcpy/Ln.java \ com/genymobile/scrcpy/Ln.java \
com/genymobile/scrcpy/Options.java \
com/genymobile/scrcpy/Point.java \ com/genymobile/scrcpy/Point.java \
com/genymobile/scrcpy/Position.java \ com/genymobile/scrcpy/Position.java \
com/genymobile/scrcpy/ScreenInfo.java \ com/genymobile/scrcpy/ScreenInfo.java \
com/genymobile/scrcpy/ScreenStreamer.java \ com/genymobile/scrcpy/ScreenStreamer.java \
com/genymobile/scrcpy/ScreenStreamerSession.java \ com/genymobile/scrcpy/ScreenStreamerSession.java \
com/genymobile/scrcpy/Size.java \
com/genymobile/scrcpy/wrappers/DisplayManager.java \ com/genymobile/scrcpy/wrappers/DisplayManager.java \
com/genymobile/scrcpy/wrappers/InputManager.java \ com/genymobile/scrcpy/wrappers/InputManager.java \
com/genymobile/scrcpy/wrappers/ServiceManager.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 { public static DesktopConnection open(Device device) throws IOException {
LocalSocket socket = connect(SOCKET_NAME); LocalSocket socket = connect(SOCKET_NAME);
ScreenInfo initialScreenInfo = device.getScreenInfo();
int width = initialScreenInfo.getLogicalWidth();
int height = initialScreenInfo.getLogicalHeight();
DesktopConnection connection = new DesktopConnection(socket); 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; return connection;
} }
@ -81,4 +78,3 @@ public class DesktopConnection implements Closeable {
return event; return event;
} }
} }

View file

@ -7,7 +7,7 @@ import android.view.IRotationWatcher;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
public class Device { public final class Device {
public interface RotationListener { public interface RotationListener {
void onRotationChanged(int rotation); void onRotationChanged(int rotation);
@ -18,13 +18,12 @@ public class Device {
private ScreenInfo screenInfo; private ScreenInfo screenInfo;
private RotationListener rotationListener; private RotationListener rotationListener;
public Device() { public Device(Options options) {
screenInfo = readScreenInfo(); screenInfo = computeScreenInfo(options.getMaximumSize());
registerRotationWatcher(new IRotationWatcher.Stub() { registerRotationWatcher(new IRotationWatcher.Stub() {
@Override @Override
public void onRotationChanged(int rotation) throws RemoteException { public void onRotationChanged(int rotation) throws RemoteException {
synchronized (Device.this) { synchronized (Device.this) {
// update screenInfo cache
screenInfo = screenInfo.withRotation(rotation); screenInfo = screenInfo.withRotation(rotation);
// notify // notify
@ -37,23 +36,59 @@ public class Device {
} }
public synchronized ScreenInfo getScreenInfo() { public synchronized ScreenInfo getScreenInfo() {
if (screenInfo == null) {
screenInfo = readScreenInfo();
}
return screenInfo; return screenInfo;
} }
public Point getPhysicalPoint(Position position) { private ScreenInfo computeScreenInfo(int maximumSize) {
ScreenInfo screenInfo = getScreenInfo(); DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
int deviceWidth = screenInfo.getLogicalWidth(); boolean rotated = (displayInfo.getRotation() & 1) != 0;
int deviceHeight = screenInfo.getLogicalHeight(); Size deviceSize = displayInfo.getSize();
int scaledX = position.getX() * deviceWidth / position.getScreenWidth(); int w = deviceSize.getWidth();
int scaledY = position.getY() * deviceHeight / position.getScreenHeight(); int h = deviceSize.getHeight();
return new Point(scaledX, scaledY); 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() { public Point getPhysicalPoint(Position position) {
return serviceManager.getDisplayManager().getScreenInfo(); 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() { 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; package com.genymobile.scrcpy;
import java.util.Objects;
public class Point { public class Point {
private int x; private int x;
private int y; private int y;
@ -17,6 +19,20 @@ public class Point {
return y; 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 @Override
public String toString() { public String toString() {
return "Point{" + return "Point{" +

View file

@ -1,42 +1,48 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
public class Position { import java.util.Objects;
private int x; public class Position {
private int y; private Point point;
private int screenWidth; private Size screenSize;
private int screenHeight;
public Position(Point point, Size screenSize) {
this.point = point;
this.screenSize = screenSize;
}
public Position(int x, int y, int screenWidth, int screenHeight) { public Position(int x, int y, int screenWidth, int screenHeight) {
this.x = x; this(new Point(x, y), new Size(screenWidth, screenHeight));
this.y = y;
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
} }
public int getX() { public Point getPoint() {
return x; return point;
} }
public int getY() { public Size getScreenSize() {
return y; return screenSize;
} }
public int getScreenWidth() { @Override
return screenWidth; 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() { @Override
return screenHeight; public int hashCode() {
return Objects.hash(point, screenSize);
} }
@Override @Override
public String toString() { public String toString() {
return "Point{" + return "Position{" +
"x=" + x + "point=" + point +
", y=" + y + ", screenSize=" + screenSize +
", screenWidth=" + screenWidth +
", screenHeight=" + screenHeight +
'}'; '}';
} }
} }

View file

@ -6,10 +6,10 @@ public class ScrCpyServer {
private static final String TAG = "scrcpy"; private static final String TAG = "scrcpy";
private static void scrcpy() throws IOException { private static void scrcpy(Options options) throws IOException {
final Device device = new Device(); final Device device = new Device(options);
try (DesktopConnection connection = DesktopConnection.open(device)) { try (DesktopConnection connection = DesktopConnection.open(device)) {
final ScreenStreamer streamer = new ScreenStreamer(connection); final ScreenStreamer streamer = new ScreenStreamer(device, connection);
device.setRotationListener(new Device.RotationListener() { device.setRotationListener(new Device.RotationListener() {
@Override @Override
public void onRotationChanged(int rotation) { public void onRotationChanged(int rotation) {
@ -43,8 +43,13 @@ public class ScrCpyServer {
} }
public static void main(String... args) throws Exception { 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 { try {
scrcpy(); scrcpy(options);
} catch (Throwable t) { } catch (Throwable t) {
t.printStackTrace(); t.printStackTrace();
throw t; throw t;

View file

@ -1,26 +1,39 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
public class ScreenInfo { public final class ScreenInfo {
private final int width; private final Size deviceSize;
private final int height; private final Size videoSize;
private int rotation; 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) { public ScreenInfo(Size deviceSize, Size videoSize, int padding, boolean rotated) {
this.width = width; this.deviceSize = deviceSize;
this.height = height; this.videoSize = videoSize;
this.rotation = rotation; 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) { public ScreenInfo withRotation(int rotation) {
return new ScreenInfo(width, height, rotation); boolean newRotated = (rotation & 1) != 0;
} if (rotated == newRotated) {
return this;
public int getLogicalWidth() { }
return (rotation & 1) == 0 ? width : height; return new ScreenInfo(deviceSize.rotate(), videoSize.rotate(), padding, newRotated);
}
public int getLogicalHeight() {
return (rotation & 1) == 0 ? height : width;
} }
} }

View file

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

View file

@ -2,16 +2,20 @@ package com.genymobile.scrcpy;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenStreamerSession { public class ScreenStreamerSession {
private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private Process screenRecordProcess; // protected by 'this' private Process screenRecordProcess; // protected by 'this'
private final AtomicBoolean stopped = new AtomicBoolean(); private final AtomicBoolean stopped = new AtomicBoolean();
private final byte[] buffer = new byte[0x10000]; private final byte[] buffer = new byte[0x10000];
public ScreenStreamerSession(DesktopConnection connection) { public ScreenStreamerSession(Device device, DesktopConnection connection) {
this.device = device;
this.connection = connection; this.connection = connection;
} }
@ -28,7 +32,8 @@ public class ScreenStreamerSession {
*/ */
private boolean streamScreenOnce() throws IOException { private boolean streamScreenOnce() throws IOException {
Ln.d("Recording..."); Ln.d("Recording...");
Process process = startScreenRecord(); Size videoSize = device.getScreenInfo().getVideoSize();
Process process = startScreenRecord(videoSize);
setCurrentProcess(process); setCurrentProcess(process);
InputStream inputStream = process.getInputStream(); InputStream inputStream = process.getInputStream();
int r; int r;
@ -44,8 +49,15 @@ public class ScreenStreamerSession {
killCurrentProcess(); killCurrentProcess();
} }
private static Process startScreenRecord() throws IOException { private static Process startScreenRecord(Size videoSize) throws IOException {
Process process = new ProcessBuilder("screenrecord", "--output-format=h264", "-").start(); 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(); process.getOutputStream().close();
return process; 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 android.os.IInterface;
import com.genymobile.scrcpy.ScreenInfo; import com.genymobile.scrcpy.DisplayInfo;
import com.genymobile.scrcpy.Size;
public class DisplayManager { public class DisplayManager {
private final IInterface manager; private final IInterface manager;
@ -11,15 +12,15 @@ public class DisplayManager {
this.manager = manager; this.manager = manager;
} }
public ScreenInfo getScreenInfo() { public DisplayInfo getDisplayInfo() {
try { try {
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0);
Class<?> cls = displayInfo.getClass(); Class<?> cls = displayInfo.getClass();
// width and height do not depend on the rotation // width and height already take the rotation into account
int width = (Integer) cls.getMethod("getNaturalWidth").invoke(displayInfo); int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
int height = (Integer) cls.getMethod("getNaturalHeight").invoke(displayInfo); int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
int rotation = cls.getDeclaredField("rotation").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) { } catch (Exception e) {
throw new AssertionError(e); throw new AssertionError(e);
} }