From 810ff80ba7c425f021bcfda972345595a85b803c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2019 16:58:14 +0200 Subject: [PATCH 01/60] Add buffer_write64be() Add a function to write 64 bits in big-endian from a uint64_t. --- app/src/buffer_util.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/buffer_util.h b/app/src/buffer_util.h index 681421f3..262df1dc 100644 --- a/app/src/buffer_util.h +++ b/app/src/buffer_util.h @@ -20,6 +20,12 @@ buffer_write32be(uint8_t *buf, uint32_t value) { buf[3] = value; } +static inline void +buffer_write64be(uint8_t *buf, uint64_t value) { + buffer_write32be(buf, value >> 32); + buffer_write32be(&buf[4], (uint32_t) value); +} + static inline uint16_t buffer_read16be(const uint8_t *buf) { return (buf[0] << 8) | buf[1]; From d90549d1e67e018285fedb8f1d820c04b7d5b3c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 22 Sep 2019 14:45:56 +0200 Subject: [PATCH 02/60] Rename "pointer" to "mouse pointer" This will help to distinguish them from "touch pointers". --- .../com/genymobile/scrcpy/Controller.java | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 263fc2fc..cbc4aec4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -20,35 +20,35 @@ public class Controller { private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); private long lastMouseDown; - private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()}; - private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()}; + private final MotionEvent.PointerProperties[] mousePointerProperties = {new MotionEvent.PointerProperties()}; + private final MotionEvent.PointerCoords[] mousePointerCoords = {new MotionEvent.PointerCoords()}; public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; - initPointer(); + initMousePointer(); sender = new DeviceMessageSender(connection); } - private void initPointer() { - MotionEvent.PointerProperties props = pointerProperties[0]; + private void initMousePointer() { + MotionEvent.PointerProperties props = mousePointerProperties[0]; props.id = 0; props.toolType = MotionEvent.TOOL_TYPE_FINGER; - MotionEvent.PointerCoords coords = pointerCoords[0]; + MotionEvent.PointerCoords coords = mousePointerCoords[0]; coords.orientation = 0; coords.pressure = 1; coords.size = 1; } - private void setPointerCoords(Point point) { - MotionEvent.PointerCoords coords = pointerCoords[0]; + private void setMousePointerCoords(Point point) { + MotionEvent.PointerCoords coords = mousePointerCoords[0]; coords.x = point.getX(); coords.y = point.getY(); } private void setScroll(int hScroll, int vScroll) { - MotionEvent.PointerCoords coords = pointerCoords[0]; + MotionEvent.PointerCoords coords = mousePointerCoords[0]; coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); } @@ -158,9 +158,9 @@ public class Controller { // ignore event return false; } - setPointerCoords(point); - MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, 0, 0, - InputDevice.SOURCE_TOUCHSCREEN, 0); + setMousePointerCoords(point); + MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, mousePointerProperties, + mousePointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); return injectEvent(event); } @@ -171,23 +171,22 @@ public class Controller { // ignore event return false; } - setPointerCoords(point); + setMousePointerCoords(point); setScroll(hScroll, vScroll); - MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0, - 0, InputDevice.SOURCE_MOUSE, 0); + MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, + mousePointerProperties, mousePointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); return injectEvent(event); } private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { long now = SystemClock.uptimeMillis(); - KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, - InputDevice.SOURCE_KEYBOARD); + KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, + 0, 0, InputDevice.SOURCE_KEYBOARD); return injectEvent(event); } private boolean injectKeycode(int keyCode) { - return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) - && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); } private boolean injectEvent(InputEvent event) { From 77f876e29cdd6f78dd9de4be6511b6228d58a1e7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2019 16:16:17 +0200 Subject: [PATCH 03/60] Add "inject touch" control message Add a control message type in the protocol to forward touch events to the device. --- app/src/control_msg.c | 19 ++++++++++ app/src/control_msg.h | 7 ++++ app/tests/test_control_msg_serialize.c | 36 +++++++++++++++++++ .../com/genymobile/scrcpy/ControlMessage.java | 35 ++++++++++++++---- .../scrcpy/ControlMessageReader.java | 19 ++++++++++ .../scrcpy/ControlMessageReaderTest.java | 31 ++++++++++++++++ 6 files changed, 140 insertions(+), 7 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index fff93592..11e87e40 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -1,6 +1,7 @@ #include "control_msg.h" #include +#include #include "config.h" #include "buffer_util.h" @@ -24,6 +25,16 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) { return 2 + len; } +static uint16_t +to_fixed_point_16(float f) { + SDL_assert(f >= 0.0f && f <= 1.0f); + uint32_t u = f * 0x1p16f; // 2^16 + if (u >= 0xffff) { + u = 0xffff; + } + return (uint16_t) u; +} + size_t control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buf[0] = msg->type; @@ -43,6 +54,14 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buffer_write32be(&buf[2], msg->inject_mouse_event.buttons); write_position(&buf[6], &msg->inject_mouse_event.position); return 18; + case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: + buf[1] = msg->inject_touch_event.action; + buffer_write64be(&buf[2], msg->inject_touch_event.pointer_id); + write_position(&buf[10], &msg->inject_touch_event.position); + uint16_t pressure = + to_fixed_point_16(msg->inject_touch_event.pressure); + buffer_write16be(&buf[22], pressure); + return 24; case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); buffer_write32be(&buf[13], diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 308d54a3..546564cf 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -19,6 +19,7 @@ enum control_msg_type { CONTROL_MSG_TYPE_INJECT_KEYCODE, CONTROL_MSG_TYPE_INJECT_TEXT, CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, @@ -50,6 +51,12 @@ struct control_msg { enum android_motionevent_buttons buttons; struct position position; } inject_mouse_event; + struct { + enum android_motionevent_action action; + uint64_t pointer_id; + struct position position; + float pressure; + } inject_touch_event; struct { struct position position; int32_t hscroll; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index c0c501f2..ea06211a 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -100,6 +100,41 @@ static void test_serialize_inject_mouse_event(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_inject_touch_event(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .inject_touch_event = { + .action = AMOTION_EVENT_ACTION_DOWN, + .pointer_id = 0x1234567887654321L, + .position = { + .point = { + .x = 100, + .y = 200, + }, + .screen_size = { + .width = 1080, + .height = 1920, + }, + }, + .pressure = 1.0f, + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 24); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + 0x00, // AKEY_EVENT_ACTION_DOWN + 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id + 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200 + 0x04, 0x38, 0x07, 0x80, // 1080 1920 + 0xff, 0xff, // pressure + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + static void test_serialize_inject_scroll_event(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, @@ -237,6 +272,7 @@ int main(void) { test_serialize_inject_text(); test_serialize_inject_text_long(); test_serialize_inject_mouse_event(); + test_serialize_inject_touch_event(); test_serialize_inject_scroll_event(); test_serialize_back_or_screen_on(); test_serialize_expand_notification_panel(); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index a1cd873a..34da7741 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -8,13 +8,14 @@ public final class ControlMessage { public static final int TYPE_INJECT_KEYCODE = 0; public static final int TYPE_INJECT_TEXT = 1; public static final int TYPE_INJECT_MOUSE_EVENT = 2; - public static final int TYPE_INJECT_SCROLL_EVENT = 3; - public static final int TYPE_BACK_OR_SCREEN_ON = 4; - public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; - public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; - public static final int TYPE_GET_CLIPBOARD = 7; - public static final int TYPE_SET_CLIPBOARD = 8; - public static final int TYPE_SET_SCREEN_POWER_MODE = 9; + public static final int TYPE_INJECT_TOUCH_EVENT = 3; + public static final int TYPE_INJECT_SCROLL_EVENT = 4; + public static final int TYPE_BACK_OR_SCREEN_ON = 5; + public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 6; + public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 7; + public static final int TYPE_GET_CLIPBOARD = 8; + public static final int TYPE_SET_CLIPBOARD = 9; + public static final int TYPE_SET_SCREEN_POWER_MODE = 10; private int type; private String text; @@ -22,6 +23,8 @@ public final class ControlMessage { private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* private int keycode; // KeyEvent.KEYCODE_* private int buttons; // MotionEvent.BUTTON_* + private long pointerId; + private float pressure; private Position position; private int hScroll; private int vScroll; @@ -54,6 +57,16 @@ public final class ControlMessage { return msg; } + public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_INJECT_TOUCH_EVENT; + msg.action = action; + msg.pointerId = pointerId; + msg.pressure = pressure; + msg.position = position; + return msg; + } + public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_SCROLL_EVENT; @@ -110,6 +123,14 @@ public final class ControlMessage { return buttons; } + public long getPointerId() { + return pointerId; + } + + public float getPressure() { + return pressure; + } + public Position getPosition() { return position; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 8ced049d..e6a6c905 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -10,6 +10,7 @@ public class ControlMessageReader { private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; + private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21; private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; @@ -62,6 +63,9 @@ public class ControlMessageReader { case ControlMessage.TYPE_INJECT_MOUSE_EVENT: msg = parseInjectMouseEvent(); break; + case ControlMessage.TYPE_INJECT_TOUCH_EVENT: + msg = parseInjectTouchEvent(); + break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: msg = parseInjectScrollEvent(); break; @@ -130,6 +134,21 @@ public class ControlMessageReader { return ControlMessage.createInjectMouseEvent(action, buttons, position); } + @SuppressWarnings("checkstyle:MagicNumber") + private ControlMessage parseInjectTouchEvent() { + if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { + return null; + } + int action = toUnsigned(buffer.get()); + long pointerId = buffer.getLong(); + Position position = readPosition(buffer); + // 16 bits fixed-point + int pressureInt = toUnsigned(buffer.getShort()); + // convert it to a float between 0 and 1 (0x1p16f is 2^16 as float) + float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f); + return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure); + } + private ControlMessage parseInjectScrollEvent() { if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) { return null; diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index f0c643d4..33380295 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -105,6 +105,37 @@ public class ControlMessageReaderTest { Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); } + @Test + @SuppressWarnings("checkstyle:MagicNumber") + public void testParseTouchEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT); + dos.writeByte(MotionEvent.ACTION_DOWN); + dos.writeLong(-42); // pointerId + dos.writeInt(100); + dos.writeInt(200); + dos.writeShort(1080); + dos.writeShort(1920); + dos.writeShort(0xffff); // pressure + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType()); + Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(-42, event.getPointerId()); + Assert.assertEquals(100, event.getPosition().getPoint().getX()); + Assert.assertEquals(200, event.getPosition().getPoint().getY()); + Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); + Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); + Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact + } + @Test @SuppressWarnings("checkstyle:MagicNumber") public void testParseScrollEvent() throws IOException { From f765aae352ecc0404ab483ec3255e7e1608ee128 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2019 22:28:59 +0200 Subject: [PATCH 04/60] Inject touch events on the server On receiving an "inject touch" control message, update the local pointers state and inject touches. --- .../com/genymobile/scrcpy/Controller.java | 64 +++++++++++ .../java/com/genymobile/scrcpy/Pointer.java | 55 ++++++++++ .../com/genymobile/scrcpy/PointersState.java | 103 ++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 server/src/main/java/com/genymobile/scrcpy/Pointer.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/PointersState.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index cbc4aec4..5ea712d4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -23,10 +23,18 @@ public class Controller { private final MotionEvent.PointerProperties[] mousePointerProperties = {new MotionEvent.PointerProperties()}; private final MotionEvent.PointerCoords[] mousePointerCoords = {new MotionEvent.PointerCoords()}; + private long lastTouchDown; + private final PointersState pointersState = new PointersState(); + private final MotionEvent.PointerProperties[] touchPointerProperties = + new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; + private final MotionEvent.PointerCoords[] touchPointerCoords = + new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; + public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; initMousePointer(); + initTouchPointers(); sender = new DeviceMessageSender(connection); } @@ -41,6 +49,20 @@ public class Controller { coords.size = 1; } + private void initTouchPointers() { + for (int i = 0; i < PointersState.MAX_POINTERS; ++i) { + MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); + props.toolType = MotionEvent.TOOL_TYPE_FINGER; + + MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); + coords.orientation = 0; + coords.size = 1; + + touchPointerProperties[i] = props; + touchPointerCoords[i] = coords; + } + } + private void setMousePointerCoords(Point point) { MotionEvent.PointerCoords coords = mousePointerCoords[0]; coords.x = point.getX(); @@ -90,6 +112,9 @@ public class Controller { case ControlMessage.TYPE_INJECT_MOUSE_EVENT: injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition()); break; + case ControlMessage.TYPE_INJECT_TOUCH_EVENT: + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure()); + break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); break; @@ -164,6 +189,45 @@ public class Controller { return injectEvent(event); } + private boolean injectTouch(int action, long pointerId, Position position, float pressure) { + long now = SystemClock.uptimeMillis(); + + Point point = device.getPhysicalPoint(position); + if (point == null) { + // ignore event + return false; + } + + int pointerIndex = pointersState.getPointerIndex(pointerId); + if (pointerIndex == -1) { + Ln.w("Too many pointers for touch event"); + return false; + } + Pointer pointer = pointersState.get(pointerIndex); + pointer.setPoint(point); + pointer.setPressure(pressure); + pointer.setUp(action == MotionEvent.ACTION_UP); + + int pointerCount = pointersState.update(touchPointerProperties, touchPointerCoords); + + if (pointerCount == 1) { + if (action == MotionEvent.ACTION_DOWN) { + lastTouchDown = now; + } + } else { + // secondary pointers must use ACTION_POINTER_* ORed with the pointerIndex + if (action == MotionEvent.ACTION_UP) { + action = MotionEvent.ACTION_POINTER_UP | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); + } else if (action == MotionEvent.ACTION_DOWN) { + action = MotionEvent.ACTION_POINTER_DOWN | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); + } + } + + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, touchPointerProperties, + touchPointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + return injectEvent(event); + } + private boolean injectScroll(Position position, int hScroll, int vScroll) { long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); diff --git a/server/src/main/java/com/genymobile/scrcpy/Pointer.java b/server/src/main/java/com/genymobile/scrcpy/Pointer.java new file mode 100644 index 00000000..b89cc256 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Pointer.java @@ -0,0 +1,55 @@ +package com.genymobile.scrcpy; + +public class Pointer { + + /** + * Pointer id as received from the client. + */ + private final long id; + + /** + * Local pointer id, using the lowest possible values to fill the {@link android.view.MotionEvent.PointerProperties PointerProperties}. + */ + private final int localId; + + private Point point; + private float pressure; + private boolean up; + + public Pointer(long id, int localId) { + this.id = id; + this.localId = localId; + } + + public long getId() { + return id; + } + + public int getLocalId() { + return localId; + } + + public Point getPoint() { + return point; + } + + public void setPoint(Point point) { + this.point = point; + } + + public float getPressure() { + return pressure; + } + + public void setPressure(float pressure) { + this.pressure = pressure; + } + + public boolean isUp() { + return up; + } + + public void setUp(boolean up) { + this.up = up; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/PointersState.java new file mode 100644 index 00000000..d8daaff2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/PointersState.java @@ -0,0 +1,103 @@ +package com.genymobile.scrcpy; + +import android.view.MotionEvent; + +import java.util.ArrayList; +import java.util.List; + +public class PointersState { + + public static final int MAX_POINTERS = 10; + + private final List pointers = new ArrayList<>(); + + private int indexOf(long id) { + for (int i = 0; i < pointers.size(); ++i) { + Pointer pointer = pointers.get(i); + if (pointer.getId() == id) { + return i; + } + } + return -1; + } + + private boolean isLocalIdAvailable(int localId) { + for (int i = 0; i < pointers.size(); ++i) { + Pointer pointer = pointers.get(i); + if (pointer.getLocalId() == localId) { + return false; + } + } + return true; + } + + private int nextUnusedLocalId() { + for (int localId = 0; localId < MAX_POINTERS; ++localId) { + if (isLocalIdAvailable(localId)) { + return localId; + } + } + return -1; + } + + public Pointer get(int index) { + return pointers.get(index); + } + + public int getPointerIndex(long id) { + int index = indexOf(id); + if (index != -1) { + // already exists, return it + return index; + } + if (pointers.size() >= MAX_POINTERS) { + // it's full + return -1; + } + // id 0 is reserved for mouse events + int localId = nextUnusedLocalId(); + if (localId == -1) { + throw new AssertionError("pointers.size() < maxFingers implies that a local id is available"); + } + Pointer pointer = new Pointer(id, localId); + pointers.add(pointer); + // return the index of the pointer + return pointers.size() - 1; + } + + /** + * Initialize the motion event parameters. + * + * @param props the pointer properties + * @param coords the pointer coordinates + * @return The number of items initialized (the number of pointers). + */ + public int update(MotionEvent.PointerProperties[] props, MotionEvent.PointerCoords[] coords) { + int count = pointers.size(); + for (int i = 0; i < count; ++i) { + Pointer pointer = pointers.get(i); + + // id 0 is reserved for mouse events + props[i].id = pointer.getLocalId(); + + Point point = pointer.getPoint(); + coords[i].x = point.getX(); + coords[i].y = point.getY(); + coords[i].pressure = pointer.getPressure(); + } + cleanUp(); + return count; + } + + /** + * Remove all pointers which are UP. + */ + private void cleanUp() { + for (int i = pointers.size() - 1; i >= 0; --i) { + Pointer pointer = pointers.get(i); + if (pointer.isUp()) { + pointers.remove(i); + } + } + } +} From b5a2d99bc24b6ae1cd040bef7086e4847cae4d07 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 22 Sep 2019 21:30:05 +0200 Subject: [PATCH 05/60] Send touch events from the client On SDL touch events, send control messages to the server. --- app/src/event_converter.c | 28 ++++++++++++++++++++++++++++ app/src/event_converter.h | 4 ++++ app/src/input_manager.c | 11 +++++++++++ app/src/input_manager.h | 4 ++++ app/src/scrcpy.c | 5 +++++ 5 files changed, 52 insertions(+) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index da4b2e30..e9fbe13b 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -207,6 +207,34 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, return true; } +static bool +convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { + switch (from) { + MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); + MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN); + MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP); + FAIL; + } +} + +bool +convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + to->inject_touch_event.pointer_id = from->fingerId; + to->inject_touch_event.position.screen_size = screen_size; + // SDL touch event coordinates are normalized in the range [0; 1] + to->inject_touch_event.position.point.x = from->x * screen_size.width; + to->inject_touch_event.position.point.y = from->y * screen_size.height; + to->inject_touch_event.pressure = from->pressure; + return true; +} + bool convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, struct control_msg *to) { diff --git a/app/src/event_converter.h b/app/src/event_converter.h index e0b24c15..f6f136a3 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -30,6 +30,10 @@ bool convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, struct control_msg *to); +bool +convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, + struct control_msg *to); + // on Android, a scroll event requires the current mouse position bool convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index cf2a7519..2123f241 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -397,6 +397,17 @@ input_manager_process_mouse_motion(struct input_manager *input_manager, } } +void +input_manager_process_touch(struct input_manager *input_manager, + const SDL_TouchFingerEvent *event) { + struct control_msg msg; + if (convert_touch(event, input_manager->screen->frame_size, &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Could not request 'inject touch event'"); + } + } +} + static bool is_outside_device_screen(struct input_manager *input_manager, int x, int y) { diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 61a0447f..0009cb81 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -29,6 +29,10 @@ void input_manager_process_mouse_motion(struct input_manager *input_manager, const SDL_MouseMotionEvent *event); +void +input_manager_process_touch(struct input_manager *input_manager, + const SDL_TouchFingerEvent *event); + void input_manager_process_mouse_button(struct input_manager *input_manager, const SDL_MouseButtonEvent *event, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index defcb751..c219c9e5 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -181,6 +181,11 @@ handle_event(SDL_Event *event, bool control) { input_manager_process_mouse_button(&input_manager, &event->button, control); break; + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: + input_manager_process_touch(&input_manager, &event->tfinger); + break; case SDL_DROPFILE: { if (!control) { break; From 30168f042890f01071d15a7d7c88050b1825aa73 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 22 Sep 2019 21:33:16 +0200 Subject: [PATCH 06/60] Ignore duplicate mouse events In SDL, a touch event may simulate an identical mouse event. Since we already handle touch event, ignore these duplicates. --- app/src/input_manager.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 2123f241..db15da75 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -389,6 +389,10 @@ input_manager_process_mouse_motion(struct input_manager *input_manager, // do not send motion events when no button is pressed return; } + if (event->which == SDL_TOUCH_MOUSEID) { + // simulated from touch events, so it's a duplicate + return; + } struct control_msg msg; if (convert_mouse_motion(event, input_manager->screen->frame_size, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) { @@ -419,6 +423,10 @@ void input_manager_process_mouse_button(struct input_manager *input_manager, const SDL_MouseButtonEvent *event, bool control) { + if (event->which == SDL_TOUCH_MOUSEID) { + // simulated from touch events, so it's a duplicate + return; + } if (event->type == SDL_MOUSEBUTTONDOWN) { if (control && event->button == SDL_BUTTON_RIGHT) { press_back_or_turn_screen_on(input_manager->controller); From 280d5b718cb0416adb164d363ffe077060ea87b1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 3 Oct 2019 19:59:01 +0200 Subject: [PATCH 07/60] Use common pointers for mouse and touch The mouse is a pointer like any other. --- .../com/genymobile/scrcpy/Controller.java | 66 +++++-------------- .../com/genymobile/scrcpy/PointersState.java | 1 + 2 files changed, 16 insertions(+), 51 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 5ea712d4..479f82cd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -19,10 +19,6 @@ public class Controller { private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); - private long lastMouseDown; - private final MotionEvent.PointerProperties[] mousePointerProperties = {new MotionEvent.PointerProperties()}; - private final MotionEvent.PointerCoords[] mousePointerCoords = {new MotionEvent.PointerCoords()}; - private long lastTouchDown; private final PointersState pointersState = new PointersState(); private final MotionEvent.PointerProperties[] touchPointerProperties = @@ -33,22 +29,10 @@ public class Controller { public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; - initMousePointer(); initTouchPointers(); sender = new DeviceMessageSender(connection); } - private void initMousePointer() { - MotionEvent.PointerProperties props = mousePointerProperties[0]; - props.id = 0; - props.toolType = MotionEvent.TOOL_TYPE_FINGER; - - MotionEvent.PointerCoords coords = mousePointerCoords[0]; - coords.orientation = 0; - coords.pressure = 1; - coords.size = 1; - } - private void initTouchPointers() { for (int i = 0; i < PointersState.MAX_POINTERS; ++i) { MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); @@ -63,18 +47,6 @@ public class Controller { } } - private void setMousePointerCoords(Point point) { - MotionEvent.PointerCoords coords = mousePointerCoords[0]; - coords.x = point.getX(); - coords.y = point.getY(); - } - - private void setScroll(int hScroll, int vScroll) { - MotionEvent.PointerCoords coords = mousePointerCoords[0]; - coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); - coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); - } - @SuppressWarnings("checkstyle:MagicNumber") public void control() throws IOException { // on start, power on the device @@ -110,10 +82,10 @@ public class Controller { injectText(msg.getText()); break; case ControlMessage.TYPE_INJECT_MOUSE_EVENT: - injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition()); + injectTouch(msg.getAction(), PointersState.POINTER_ID_MOUSE, msg.getPosition(), 1, msg.getButtons()); break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure()); + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), 0); break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); @@ -173,23 +145,7 @@ public class Controller { return successCount; } - private boolean injectMouse(int action, int buttons, Position position) { - long now = SystemClock.uptimeMillis(); - if (action == MotionEvent.ACTION_DOWN) { - lastMouseDown = now; - } - Point point = device.getPhysicalPoint(position); - if (point == null) { - // ignore event - return false; - } - setMousePointerCoords(point); - MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, mousePointerProperties, - mousePointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); - return injectEvent(event); - } - - private boolean injectTouch(int action, long pointerId, Position position, float pressure) { + private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) { long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); @@ -235,10 +191,18 @@ public class Controller { // ignore event return false; } - setMousePointerCoords(point); - setScroll(hScroll, vScroll); - MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, - mousePointerProperties, mousePointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); + + MotionEvent.PointerProperties props = touchPointerProperties[0]; + props.id = 0; + + MotionEvent.PointerCoords coords = touchPointerCoords[0]; + coords.x = point.getX(); + coords.y = point.getY(); + coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); + coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); + + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, + touchPointerProperties, touchPointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); return injectEvent(event); } diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/PointersState.java index d8daaff2..eab258b1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/PointersState.java +++ b/server/src/main/java/com/genymobile/scrcpy/PointersState.java @@ -8,6 +8,7 @@ import java.util.List; public class PointersState { public static final int MAX_POINTERS = 10; + public static final long POINTER_ID_MOUSE = -1; private final List pointers = new ArrayList<>(); From 7e1d52c1194ae262bd34489419a26e148d899138 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 3 Oct 2019 20:00:50 +0200 Subject: [PATCH 08/60] Rename "touch pointer" to "pointer" There are only touch pointers now, mouse pointers have been removed. --- .../com/genymobile/scrcpy/Controller.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 479f82cd..8c5e645a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -21,19 +21,19 @@ public class Controller { private long lastTouchDown; private final PointersState pointersState = new PointersState(); - private final MotionEvent.PointerProperties[] touchPointerProperties = + private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; - private final MotionEvent.PointerCoords[] touchPointerCoords = + private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; - initTouchPointers(); + initPointers(); sender = new DeviceMessageSender(connection); } - private void initTouchPointers() { + private void initPointers() { for (int i = 0; i < PointersState.MAX_POINTERS; ++i) { MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); props.toolType = MotionEvent.TOOL_TYPE_FINGER; @@ -42,8 +42,8 @@ public class Controller { coords.orientation = 0; coords.size = 1; - touchPointerProperties[i] = props; - touchPointerCoords[i] = coords; + pointerProperties[i] = props; + pointerCoords[i] = coords; } } @@ -164,7 +164,7 @@ public class Controller { pointer.setPressure(pressure); pointer.setUp(action == MotionEvent.ACTION_UP); - int pointerCount = pointersState.update(touchPointerProperties, touchPointerCoords); + int pointerCount = pointersState.update(pointerProperties, pointerCoords); if (pointerCount == 1) { if (action == MotionEvent.ACTION_DOWN) { @@ -179,8 +179,8 @@ public class Controller { } } - MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, touchPointerProperties, - touchPointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, + pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); return injectEvent(event); } @@ -192,17 +192,17 @@ public class Controller { return false; } - MotionEvent.PointerProperties props = touchPointerProperties[0]; + MotionEvent.PointerProperties props = pointerProperties[0]; props.id = 0; - MotionEvent.PointerCoords coords = touchPointerCoords[0]; + MotionEvent.PointerCoords coords = pointerCoords[0]; coords.x = point.getX(); coords.y = point.getY(); coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); - MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, - touchPointerProperties, touchPointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, + pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); return injectEvent(event); } From 6220456def65e00696a268ac654756a8b22a96a7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 3 Oct 2019 20:14:12 +0200 Subject: [PATCH 09/60] Merge mouse and touch events Both are handled the very same way on the device. --- app/src/control_msg.c | 8 +--- app/src/control_msg.h | 7 +-- app/src/event_converter.c | 47 ++++++++++--------- app/tests/test_control_msg_serialize.c | 38 ++------------- .../com/genymobile/scrcpy/ControlMessage.java | 30 +++++------- .../scrcpy/ControlMessageReader.java | 16 +------ .../com/genymobile/scrcpy/Controller.java | 3 -- .../com/genymobile/scrcpy/PointersState.java | 1 - .../scrcpy/ControlMessageReaderTest.java | 31 +----------- 9 files changed, 48 insertions(+), 133 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 11e87e40..e042dc5a 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -49,11 +49,6 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; } - case CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT: - buf[1] = msg->inject_mouse_event.action; - buffer_write32be(&buf[2], msg->inject_mouse_event.buttons); - write_position(&buf[6], &msg->inject_mouse_event.position); - return 18; case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: buf[1] = msg->inject_touch_event.action; buffer_write64be(&buf[2], msg->inject_touch_event.pointer_id); @@ -61,7 +56,8 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { uint16_t pressure = to_fixed_point_16(msg->inject_touch_event.pressure); buffer_write16be(&buf[22], pressure); - return 24; + buffer_write32be(&buf[24], msg->inject_touch_event.buttons); + return 28; case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); buffer_write32be(&buf[13], diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 546564cf..2f319d9d 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -15,10 +15,11 @@ #define CONTROL_MSG_SERIALIZED_MAX_SIZE \ (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) +#define POINTER_ID_MOUSE UINT64_C(-1); + enum control_msg_type { CONTROL_MSG_TYPE_INJECT_KEYCODE, CONTROL_MSG_TYPE_INJECT_TEXT, - CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, @@ -49,10 +50,6 @@ struct control_msg { struct { enum android_motionevent_action action; enum android_motionevent_buttons buttons; - struct position position; - } inject_mouse_event; - struct { - enum android_motionevent_action action; uint64_t pointer_id; struct position position; float pressure; diff --git a/app/src/event_converter.c b/app/src/event_converter.c index e9fbe13b..13abfab2 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -128,15 +128,6 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { } } -static bool -convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { - switch (from) { - MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); - MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); - FAIL; - } -} - static enum android_motionevent_buttons convert_mouse_buttons(uint32_t state) { enum android_motionevent_buttons buttons = 0; @@ -176,20 +167,31 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { return true; } +static bool +convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { + switch (from) { + MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); + MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); + FAIL; + } +} + bool convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) { + if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { return false; } - to->inject_mouse_event.buttons = + to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + to->inject_touch_event.position.screen_size = screen_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); - to->inject_mouse_event.position.screen_size = screen_size; - to->inject_mouse_event.position.point.x = from->x; - to->inject_mouse_event.position.point.y = from->y; return true; } @@ -197,12 +199,14 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, bool convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; - to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE; - to->inject_mouse_event.buttons = convert_mouse_buttons(from->state); - to->inject_mouse_event.position.screen_size = screen_size; - to->inject_mouse_event.position.point.x = from->x; - to->inject_mouse_event.position.point.y = from->y; + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; + to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + to->inject_touch_event.position.screen_size = screen_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = convert_mouse_buttons(from->state); return true; } @@ -232,6 +236,7 @@ convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, to->inject_touch_event.position.point.x = from->x * screen_size.width; to->inject_touch_event.position.point.y = from->y * screen_size.height; to->inject_touch_event.pressure = from->pressure; + to->inject_touch_event.buttons = 0; return true; } diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index ea06211a..83ab011f 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -67,39 +67,6 @@ static void test_serialize_inject_text_long(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_inject_mouse_event(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, - .inject_mouse_event = { - .action = AMOTION_EVENT_ACTION_DOWN, - .buttons = AMOTION_EVENT_BUTTON_PRIMARY, - .position = { - .point = { - .x = 260, - .y = 1026, - }, - .screen_size = { - .width = 1080, - .height = 1920, - }, - }, - }, - }; - - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); - assert(size == 18); - - const unsigned char expected[] = { - CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, - 0x00, // AKEY_EVENT_ACTION_DOWN - 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY - 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 - 0x04, 0x38, 0x07, 0x80, // 1080 1920 - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - static void test_serialize_inject_touch_event(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, @@ -117,12 +84,13 @@ static void test_serialize_inject_touch_event(void) { }, }, .pressure = 1.0f, + .buttons = AMOTION_EVENT_BUTTON_PRIMARY, }, }; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 24); + assert(size == 28); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, @@ -131,6 +99,7 @@ static void test_serialize_inject_touch_event(void) { 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200 0x04, 0x38, 0x07, 0x80, // 1080 1920 0xff, 0xff, // pressure + 0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY }; assert(!memcmp(buf, expected, sizeof(expected))); } @@ -271,7 +240,6 @@ int main(void) { test_serialize_inject_keycode(); test_serialize_inject_text(); test_serialize_inject_text_long(); - test_serialize_inject_mouse_event(); test_serialize_inject_touch_event(); test_serialize_inject_scroll_event(); test_serialize_back_or_screen_on(); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 34da7741..30c05a3b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -7,15 +7,14 @@ public final class ControlMessage { public static final int TYPE_INJECT_KEYCODE = 0; public static final int TYPE_INJECT_TEXT = 1; - public static final int TYPE_INJECT_MOUSE_EVENT = 2; - public static final int TYPE_INJECT_TOUCH_EVENT = 3; - public static final int TYPE_INJECT_SCROLL_EVENT = 4; - public static final int TYPE_BACK_OR_SCREEN_ON = 5; - public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 6; - public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 7; - public static final int TYPE_GET_CLIPBOARD = 8; - public static final int TYPE_SET_CLIPBOARD = 9; - public static final int TYPE_SET_SCREEN_POWER_MODE = 10; + public static final int TYPE_INJECT_TOUCH_EVENT = 2; + public static final int TYPE_INJECT_SCROLL_EVENT = 3; + public static final int TYPE_BACK_OR_SCREEN_ON = 4; + public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; + public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; + public static final int TYPE_GET_CLIPBOARD = 7; + public static final int TYPE_SET_CLIPBOARD = 8; + public static final int TYPE_SET_SCREEN_POWER_MODE = 9; private int type; private String text; @@ -48,22 +47,15 @@ public final class ControlMessage { return msg; } - public static ControlMessage createInjectMouseEvent(int action, int buttons, Position position) { - ControlMessage msg = new ControlMessage(); - msg.type = TYPE_INJECT_MOUSE_EVENT; - msg.action = action; - msg.buttons = buttons; - msg.position = position; - return msg; - } - - public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure) { + public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, + int buttons) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_TOUCH_EVENT; msg.action = action; msg.pointerId = pointerId; msg.pressure = pressure; msg.position = position; + msg.buttons = buttons; return msg; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index e6a6c905..2f8b5177 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -60,9 +60,6 @@ public class ControlMessageReader { case ControlMessage.TYPE_INJECT_TEXT: msg = parseInjectText(); break; - case ControlMessage.TYPE_INJECT_MOUSE_EVENT: - msg = parseInjectMouseEvent(); - break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: msg = parseInjectTouchEvent(); break; @@ -124,16 +121,6 @@ public class ControlMessageReader { return ControlMessage.createInjectText(text); } - private ControlMessage parseInjectMouseEvent() { - if (buffer.remaining() < INJECT_MOUSE_EVENT_PAYLOAD_LENGTH) { - return null; - } - int action = toUnsigned(buffer.get()); - int buttons = buffer.getInt(); - Position position = readPosition(buffer); - return ControlMessage.createInjectMouseEvent(action, buttons, position); - } - @SuppressWarnings("checkstyle:MagicNumber") private ControlMessage parseInjectTouchEvent() { if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { @@ -146,7 +133,8 @@ public class ControlMessageReader { int pressureInt = toUnsigned(buffer.getShort()); // convert it to a float between 0 and 1 (0x1p16f is 2^16 as float) float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f); - return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure); + int buttons = buffer.getInt(); + return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons); } private ControlMessage parseInjectScrollEvent() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 8c5e645a..19bf9d94 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -81,9 +81,6 @@ public class Controller { case ControlMessage.TYPE_INJECT_TEXT: injectText(msg.getText()); break; - case ControlMessage.TYPE_INJECT_MOUSE_EVENT: - injectTouch(msg.getAction(), PointersState.POINTER_ID_MOUSE, msg.getPosition(), 1, msg.getButtons()); - break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), 0); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/PointersState.java index eab258b1..d8daaff2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/PointersState.java +++ b/server/src/main/java/com/genymobile/scrcpy/PointersState.java @@ -8,7 +8,6 @@ import java.util.List; public class PointersState { public static final int MAX_POINTERS = 10; - public static final long POINTER_ID_MOUSE = -1; private final List pointers = new ArrayList<>(); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 33380295..ede759dc 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -76,35 +76,6 @@ public class ControlMessageReaderTest { Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); } - @Test - @SuppressWarnings("checkstyle:MagicNumber") - public void testParseMouseEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_INJECT_MOUSE_EVENT); - dos.writeByte(MotionEvent.ACTION_DOWN); - dos.writeInt(MotionEvent.BUTTON_PRIMARY); - dos.writeInt(100); - dos.writeInt(200); - dos.writeShort(1080); - dos.writeShort(1920); - - byte[] packet = bos.toByteArray(); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); - - Assert.assertEquals(ControlMessage.TYPE_INJECT_MOUSE_EVENT, event.getType()); - Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); - Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); - Assert.assertEquals(100, event.getPosition().getPoint().getX()); - Assert.assertEquals(200, event.getPosition().getPoint().getY()); - Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); - Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); - } - @Test @SuppressWarnings("checkstyle:MagicNumber") public void testParseTouchEvent() throws IOException { @@ -120,6 +91,7 @@ public class ControlMessageReaderTest { dos.writeShort(1080); dos.writeShort(1920); dos.writeShort(0xffff); // pressure + dos.writeInt(MotionEvent.BUTTON_PRIMARY); byte[] packet = bos.toByteArray(); @@ -134,6 +106,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); } @Test From bab936194858bdf21f8505e30c38c25c26d72434 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Oct 2019 21:45:52 +0200 Subject: [PATCH 10/60] Do not crash on control error Some devices do not have some methods that we invoke via reflection, or their call do not return the expected value. In that case, do not crash the whole controller. --- .../java/com/genymobile/scrcpy/Device.java | 4 ++ .../scrcpy/wrappers/ClipboardManager.java | 49 ++++++++++---- .../scrcpy/wrappers/InputManager.java | 27 ++++++-- .../scrcpy/wrappers/PowerManager.java | 31 ++++++--- .../scrcpy/wrappers/StatusBarManager.java | 40 ++++++++---- .../scrcpy/wrappers/SurfaceControl.java | 64 +++++++++++++++---- 6 files changed, 163 insertions(+), 52 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 538135d4..0246b216 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -162,6 +162,10 @@ public final class Device { */ public void setScreenPowerMode(int mode) { IBinder d = SurfaceControl.getBuiltInDisplay(0); + if (d == null) { + Ln.e("Could not get built-in display"); + return; + } SurfaceControl.setDisplayPowerMode(d, mode); Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index a058a8bb..7dc2e75e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.content.ClipData; import android.os.IInterface; @@ -8,37 +10,62 @@ import java.lang.reflect.Method; public class ClipboardManager { private final IInterface manager; - private final Method getPrimaryClipMethod; - private final Method setPrimaryClipMethod; + private Method getPrimaryClipMethod; + private Method setPrimaryClipMethod; public ClipboardManager(IInterface manager) { this.manager = manager; - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); + } + + private Method getGetPrimaryClipMethod() { + if (getPrimaryClipMethod == null) { + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } } + return getPrimaryClipMethod; + } + + private Method getSetPrimaryClipMethod() { + if (setPrimaryClipMethod == null) { + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } + } + return setPrimaryClipMethod; } public CharSequence getText() { + Method method = getGetPrimaryClipMethod(); + if (method == null) { + return null; + } try { - ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell"); + ClipData clipData = (ClipData) method.invoke(manager, "com.android.shell"); if (clipData == null || clipData.getItemCount() == 0) { return null; } return clipData.getItemAt(0).getText(); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), e); + return null; } } public void setText(CharSequence text) { + Method method = getSetPrimaryClipMethod(); + if (method == null) { + return; + } ClipData clipData = ClipData.newPlainText(null, text); try { - setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell"); + method.invoke(manager, clipData, "com.android.shell"); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), e); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 1fc78c27..788a04c7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.os.IInterface; import android.view.InputEvent; @@ -13,22 +15,33 @@ public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; private final IInterface manager; - private final Method injectInputEventMethod; + private Method injectInputEventMethod; public InputManager(IInterface manager) { this.manager = manager; - try { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); + } + + private Method getInjectInputEventMethod() { + if (injectInputEventMethod == null) { + try { + injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } } + return injectInputEventMethod; } public boolean injectInputEvent(InputEvent inputEvent, int mode) { + Method method = getInjectInputEventMethod(); + if (method == null) { + return false; + } try { - return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode); + return (Boolean) method.invoke(manager, inputEvent, mode); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), e); + return false; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index a730d1b1..66acdba8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.annotation.SuppressLint; import android.os.Build; import android.os.IInterface; @@ -9,24 +11,35 @@ import java.lang.reflect.Method; public final class PowerManager { private final IInterface manager; - private final Method isScreenOnMethod; + private Method isScreenOnMethod; public PowerManager(IInterface manager) { this.manager = manager; - try { - @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future - String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; - isScreenOnMethod = manager.getClass().getMethod(methodName); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); + } + + private Method getIsScreenOnMethod() { + if (isScreenOnMethod == null) { + try { + @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future + String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; + isScreenOnMethod = manager.getClass().getMethod(methodName); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } } + return isScreenOnMethod; } public boolean isScreenOn() { + Method method = getIsScreenOnMethod(); + if (method == null) { + return false; + } try { - return (Boolean) isScreenOnMethod.invoke(manager); + return (Boolean) method.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), e); + return false; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 7cd28da6..670de952 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -17,35 +17,49 @@ public class StatusBarManager { this.manager = manager; } - public void expandNotificationsPanel() { + private Method getExpandNotificationsPanelMethod() { if (expandNotificationsPanelMethod == null) { try { expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); } catch (NoSuchMethodException e) { - Ln.e("ServiceBarManager.expandNotificationsPanel() is not available on this device"); - return; + Ln.e("Could not find method", e); } } - try { - expandNotificationsPanelMethod.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke ServiceBarManager.expandNotificationsPanel()", e); - } + return expandNotificationsPanelMethod; } - public void collapsePanels() { + private Method getCollapsePanelsMethod() { if (collapsePanelsMethod == null) { try { collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); } catch (NoSuchMethodException e) { - Ln.e("ServiceBarManager.collapsePanels() is not available on this device"); - return; + Ln.e("Could not find method", e); } } + return collapsePanelsMethod; + } + + public void expandNotificationsPanel() { + Method method = getExpandNotificationsPanelMethod(); + if (method == null) { + return; + } try { - collapsePanelsMethod.invoke(manager); + method.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke ServiceBarManager.collapsePanels()", e); + Ln.e("Could not invoke " + method.getName(), e); + } + } + + public void collapsePanels() { + Method method = getCollapsePanelsMethod(); + if (method == null) { + return; + } + try { + method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException e) { + Ln.e("Could not invoke " + method.getName(), e); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 5b5586ff..ba37da0d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -1,11 +1,16 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.annotation.SuppressLint; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.view.Surface; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + @SuppressLint("PrivateApi") public final class SurfaceControl { @@ -23,6 +28,9 @@ public final class SurfaceControl { } } + private static Method getBuiltInDisplayMethod; + private static Method setDisplayPowerModeMethod; + private SurfaceControl() { // only static methods } @@ -76,24 +84,56 @@ public final class SurfaceControl { } } - public static IBinder getBuiltInDisplay(int builtInDisplayId) { - try { - // the method signature has changed in Android Q - // - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); + private static Method getGetBuiltInDisplayMethod() { + if (getBuiltInDisplayMethod == null) { + try { + // the method signature has changed in Android Q + // + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); + } else { + getBuiltInDisplayMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); + } + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); } - return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId); - } catch (Exception e) { - throw new AssertionError(e); + } + return getBuiltInDisplayMethod; + } + + public static IBinder getBuiltInDisplay(int builtInDisplayId) { + Method method = getGetBuiltInDisplayMethod(); + if (method == null) { + return null; + } + try { + return (IBinder) method.invoke(null, builtInDisplayId); + } catch (InvocationTargetException | IllegalAccessException e) { + Ln.e("Could not invoke " + method.getName(), e); + return null; } } + private static Method getSetDisplayPowerModeMethod() { + if (setDisplayPowerModeMethod == null) { + try { + setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } + } + return setDisplayPowerModeMethod; + } + public static void setDisplayPowerMode(IBinder displayToken, int mode) { + Method method = getSetDisplayPowerModeMethod(); + if (method == null) { + return; + } try { - CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode); - } catch (Exception e) { - throw new AssertionError(e); + method.invoke(null, displayToken, mode); + } catch (InvocationTargetException | IllegalAccessException e) { + Ln.e("Could not invoke " + method.getName(), e); } } From 5b7a0cd8e958946a51409767630cb43504807b0f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Oct 2019 22:11:39 +0200 Subject: [PATCH 11/60] Extract String literal to static constant --- .../com/genymobile/scrcpy/wrappers/ClipboardManager.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 7dc2e75e..5cc71cd4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -9,6 +9,9 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ClipboardManager { + + private static final String PACKAGE_NAME = "com.android.shell"; + private final IInterface manager; private Method getPrimaryClipMethod; private Method setPrimaryClipMethod; @@ -45,7 +48,7 @@ public class ClipboardManager { return null; } try { - ClipData clipData = (ClipData) method.invoke(manager, "com.android.shell"); + ClipData clipData = (ClipData) method.invoke(manager, PACKAGE_NAME); if (clipData == null || clipData.getItemCount() == 0) { return null; } @@ -63,7 +66,7 @@ public class ClipboardManager { } ClipData clipData = ClipData.newPlainText(null, text); try { - method.invoke(manager, clipData, "com.android.shell"); + method.invoke(manager, clipData, PACKAGE_NAME); } catch (InvocationTargetException | IllegalAccessException e) { Ln.e("Could not invoke " + method.getName(), e); } From 8b33c6c1087a183c7e8a7d45445eb0673b50e458 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Oct 2019 22:21:47 +0200 Subject: [PATCH 12/60] Adapt copy-paste methods for Android 10 The methods getPrimaryClip() and setPrimaryClip() expect an additional parameter since Android 10. Fixes . --- .../scrcpy/wrappers/ClipboardManager.java | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 5cc71cd4..27dcb443 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import android.content.ClipData; +import android.os.Build; import android.os.IInterface; import java.lang.reflect.InvocationTargetException; @@ -11,6 +12,7 @@ import java.lang.reflect.Method; public class ClipboardManager { private static final String PACKAGE_NAME = "com.android.shell"; + private static final int USER_ID = 0; private final IInterface manager; private Method getPrimaryClipMethod; @@ -23,7 +25,11 @@ public class ClipboardManager { private Method getGetPrimaryClipMethod() { if (getPrimaryClipMethod == null) { try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + } else { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); + } } catch (NoSuchMethodException e) { Ln.e("Could not find method", e); } @@ -34,7 +40,12 @@ public class ClipboardManager { private Method getSetPrimaryClipMethod() { if (setPrimaryClipMethod == null) { try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + } else { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, + String.class, int.class); + } } catch (NoSuchMethodException e) { Ln.e("Could not find method", e); } @@ -42,13 +53,30 @@ public class ClipboardManager { return setPrimaryClipMethod; } + private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, + IllegalAccessException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return (ClipData) method.invoke(manager, PACKAGE_NAME); + } + return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID); + } + + private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) throws InvocationTargetException, + IllegalAccessException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + method.invoke(manager, clipData, PACKAGE_NAME); + } else { + method.invoke(manager, clipData, PACKAGE_NAME, USER_ID); + } + } + public CharSequence getText() { Method method = getGetPrimaryClipMethod(); if (method == null) { return null; } try { - ClipData clipData = (ClipData) method.invoke(manager, PACKAGE_NAME); + ClipData clipData = getPrimaryClip(method, manager); if (clipData == null || clipData.getItemCount() == 0) { return null; } @@ -66,7 +94,7 @@ public class ClipboardManager { } ClipData clipData = ClipData.newPlainText(null, text); try { - method.invoke(manager, clipData, PACKAGE_NAME); + setPrimaryClip(method, manager, clipData); } catch (InvocationTargetException | IllegalAccessException e) { Ln.e("Could not invoke " + method.getName(), e); } From c33a147fd0e9fbd3a5add6f0190eea909d74c26d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Oct 2019 23:14:18 +0200 Subject: [PATCH 13/60] Fix "turn screen off" on Android Q Call getInternalDisplayToken(), which retrieve the id of the first physical display (which is not necessarily 0 anymore). Fixes --- .../src/main/java/com/genymobile/scrcpy/Device.java | 2 +- .../genymobile/scrcpy/wrappers/SurfaceControl.java | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 0246b216..708b9516 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -161,7 +161,7 @@ public final class Device { * @param mode one of the {@code SCREEN_POWER_MODE_*} constants */ public void setScreenPowerMode(int mode) { - IBinder d = SurfaceControl.getBuiltInDisplay(0); + IBinder d = SurfaceControl.getBuiltInDisplay(); if (d == null) { Ln.e("Could not get built-in display"); return; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index ba37da0d..bef6e5d9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -92,7 +92,7 @@ public final class SurfaceControl { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); } else { - getBuiltInDisplayMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); + getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); } } catch (NoSuchMethodException e) { Ln.e("Could not find method", e); @@ -101,13 +101,19 @@ public final class SurfaceControl { return getBuiltInDisplayMethod; } - public static IBinder getBuiltInDisplay(int builtInDisplayId) { + public static IBinder getBuiltInDisplay() { Method method = getGetBuiltInDisplayMethod(); if (method == null) { return null; } try { - return (IBinder) method.invoke(null, builtInDisplayId); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + // call getBuiltInDisplay(0) + return (IBinder) method.invoke(null, 0); + } + + // call getInternalDisplayToken() + return (IBinder) method.invoke(null); } catch (InvocationTargetException | IllegalAccessException e) { Ln.e("Could not invoke " + method.getName(), e); return null; From f6c8460ebb2e96aa4f1630296893807adfb84a3c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2019 16:06:28 +0200 Subject: [PATCH 14/60] Rename window size functions for clarity Now, get_window_size() returns the current window size (fullscreen or not), while get_windowed_window_size() always returned the windowed size (the size when fullscreen is disabled). --- app/src/screen.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index e34bcf46..4bc4c5c5 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -16,7 +16,7 @@ // get the window size in a struct size static struct size -get_native_window_size(SDL_Window *window) { +get_window_size(SDL_Window *window) { int width; int height; SDL_GetWindowSize(window, &width, &height); @@ -29,11 +29,11 @@ get_native_window_size(SDL_Window *window) { // get the windowed window size static struct size -get_window_size(const struct screen *screen) { +get_windowed_window_size(const struct screen *screen) { if (screen->fullscreen) { return screen->windowed_window_size; } - return get_native_window_size(screen->window); + return get_window_size(screen->window); } // set the window size to be applied when fullscreen is disabled @@ -112,8 +112,8 @@ get_optimal_size(struct size current_size, struct size frame_size) { // same as get_optimal_size(), but read the current size from the window static inline struct size get_optimal_window_size(const struct screen *screen, struct size frame_size) { - struct size current_size = get_window_size(screen); - return get_optimal_size(current_size, frame_size); + struct size windowed_size = get_windowed_window_size(screen); + return get_optimal_size(windowed_size, frame_size); } // initially, there is no current size, so use the frame size as current size @@ -229,11 +229,11 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) { // frame dimension changed, destroy texture SDL_DestroyTexture(screen->texture); - struct size current_size = get_window_size(screen); + struct size windowed_size = get_windowed_window_size(screen); struct size target_size = { - (uint32_t) current_size.width * new_frame_size.width + (uint32_t) windowed_size.width * new_frame_size.width / screen->frame_size.width, - (uint32_t) current_size.height * new_frame_size.height + (uint32_t) windowed_size.height * new_frame_size.height / screen->frame_size.height, }; target_size = get_optimal_size(target_size, new_frame_size); @@ -289,7 +289,7 @@ void screen_switch_fullscreen(struct screen *screen) { if (!screen->fullscreen) { // going to fullscreen, store the current windowed window size - screen->windowed_window_size = get_native_window_size(screen->window); + screen->windowed_window_size = get_window_size(screen->window); } uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; if (SDL_SetWindowFullscreen(screen->window, new_mode)) { From f9938dbf88fbe35415db65824a3345a0117790f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 25 Oct 2019 11:04:04 +0200 Subject: [PATCH 15/60] Inject button state for touch/mouse events The buttons state was forwarded, but ignored by the server. --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 19bf9d94..ce02e333 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -82,7 +82,7 @@ public class Controller { injectText(msg.getText()); break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), 0); + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); @@ -177,7 +177,7 @@ public class Controller { } MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, - pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + pointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); return injectEvent(event); } From 3da95b52bd21ae0f4d81ee933caea63b78191deb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 30 Oct 2019 23:40:10 +0100 Subject: [PATCH 16/60] Rename scrcpy-server.jar to scrcpy-server The server name ending with .jar has several drawbacks: - meson requires the jar executable to attempt to modify it: - meson warns during "ninja install" - some users try to execute it on the computer as a java executable Removing the extension solves all these problems. --- BUILD.md | 4 ++-- DEVELOP.md | 4 ++-- Makefile.CrossWindows | 6 +++--- README.md | 2 +- app/meson.build | 2 +- app/src/server.c | 4 ++-- meson_options.txt | 2 +- release.sh | 8 ++++---- run | 2 +- scripts/run-scrcpy.sh | 2 +- server/build_without_gradle.sh | 4 ++-- server/meson.build | 4 ++-- server/src/main/java/com/genymobile/scrcpy/Server.java | 2 +- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/BUILD.md b/BUILD.md index 475580f8..161f8f92 100644 --- a/BUILD.md +++ b/BUILD.md @@ -225,7 +225,7 @@ sudo ninja install # without sudo on Windows This installs two files: - `/usr/local/bin/scrcpy` - - `/usr/local/share/scrcpy/scrcpy-server.jar` + - `/usr/local/share/scrcpy/scrcpy-server` Just remove them to "uninstall" the application. @@ -244,7 +244,7 @@ configuration: ```bash meson x --buildtype release --strip -Db_lto=true \ - -Dprebuilt_server=/path/to/scrcpy-server.jar + -Dprebuilt_server=/path/to/scrcpy-server cd x ninja sudo ninja install diff --git a/DEVELOP.md b/DEVELOP.md index dea8137d..fb8ab91d 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -3,7 +3,7 @@ ## Overview This application is composed of two parts: - - the server (`scrcpy-server.jar`), to be executed on the device, + - the server (`scrcpy-server`), to be executed on the device, - the client (the `scrcpy` binary), executed on the host computer. The client is responsible to push the server to the device and start its @@ -49,7 +49,7 @@ application may not replace the server just before the client executes it._ Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing `classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle build system, the server is built to an (unsigned) APK (renamed to -`scrcpy-server.jar`). +`scrcpy-server`). [dex]: https://en.wikipedia.org/wiki/Dalvik_(software) [apk]: https://en.wikipedia.org/wiki/Android_application_package diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index c07cb24f..59f5a302 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -3,7 +3,7 @@ # # Here, "portable" means that the client and server binaries are expected to be # anywhere, but in the same directory, instead of well-defined separate -# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar). +# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server). # # In particular, this implies to change the location from where the client push # the server to the device. @@ -97,7 +97,7 @@ build-win64-noconsole: prepare-deps-win64 dist-win32: build-server build-win32 build-win32-noconsole mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -112,7 +112,7 @@ dist-win32: build-server build-win32 build-win32-noconsole dist-win64: build-server build-win64 build-win64-noconsole mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" diff --git a/README.md b/README.md index f698cb4c..ebf593a1 100644 --- a/README.md +++ b/README.md @@ -358,7 +358,7 @@ To use a specific _adb_ binary, configure its path in the environment variable ADB=/path/to/adb scrcpy -To override the path of the `scrcpy-server.jar` file, configure its path in +To override the path of the `scrcpy-server` file, configure its path in `SCRCPY_SERVER_PATH`. [useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 diff --git a/app/meson.build b/app/meson.build index ccd05fee..5d24fb3a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -93,7 +93,7 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version()) # the prefix used during configuration (meson --prefix=PREFIX) conf.set_quoted('PREFIX', get_option('prefix')) -# build a "portable" version (with scrcpy-server.jar accessible from the same +# build a "portable" version (with scrcpy-server accessible from the same # directory as the executable) conf.set('PORTABLE', get_option('portable')) diff --git a/app/src/server.c b/app/src/server.c index 85b1b6b8..4fe65402 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -13,7 +13,7 @@ #include "net.h" #define SOCKET_NAME "scrcpy" -#define SERVER_FILENAME "scrcpy-server.jar" +#define SERVER_FILENAME "scrcpy-server" #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME @@ -32,7 +32,7 @@ get_server_path(void) { // the absolute path is hardcoded return DEFAULT_SERVER_PATH; #else - // use scrcpy-server.jar in the same directory as the executable + // use scrcpy-server in the same directory as the executable char *executable_path = get_executable_path(); if (!executable_path) { LOGE("Could not get executable path, " diff --git a/meson_options.txt b/meson_options.txt index d93161e3..84889597 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,5 +3,5 @@ option('compile_server', type: 'boolean', value: true, description: 'Build the s option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') -option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') +option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') diff --git a/release.sh b/release.sh index fbd1eb54..4c5afbf1 100755 --- a/release.sh +++ b/release.sh @@ -23,21 +23,21 @@ cd - make -f Makefile.CrossWindows # the generated server must be the same everywhere -cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar -cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar +cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win32/scrcpy-server +cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server # get version name TAG=$(git describe --tags --always) # create release directory mkdir -p "release-$TAG" -cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar" +cp "$BUILDDIR/server/scrcpy-server" "release-$TAG/scrcpy-server-$TAG" cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/" cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/" # generate checksums cd "release-$TAG" -sha256sum "scrcpy-server-$TAG.jar" \ +sha256sum "scrcpy-server-$TAG" \ "scrcpy-win32-$TAG.zip" \ "scrcpy-win64-$TAG.zip" > SHA256SUMS.txt diff --git a/run b/run index 7abeca05..bfb499ae 100755 --- a/run +++ b/run @@ -20,4 +20,4 @@ then exit 1 fi -SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server.jar" "$BUILDDIR/app/scrcpy" "$@" +SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@" diff --git a/scripts/run-scrcpy.sh b/scripts/run-scrcpy.sh index fa6d7c8f..f3130ee9 100755 --- a/scripts/run-scrcpy.sh +++ b/scripts/run-scrcpy.sh @@ -1,2 +1,2 @@ #!/bin/bash -SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server.jar" "$MESON_BUILD_ROOT/app/scrcpy" +SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 5f2eff22..daf85008 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -17,7 +17,7 @@ BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" SERVER_DIR=$(dirname "$0") -SERVER_BINARY=scrcpy-server.jar +SERVER_BINARY=scrcpy-server echo "Platform: android-$PLATFORM" echo "Build-tools: $BUILD_TOOLS" @@ -59,4 +59,4 @@ cd "$BUILD_DIR" jar cvf "$SERVER_BINARY" classes.dex rm -rf classes.dex classes -echo "Server generated in $BUILD_DIR/scrcpy-server.jar" +echo "Server generated in $BUILD_DIR/$SERVER_BINARY" diff --git a/server/meson.build b/server/meson.build index 43901246..4ba481d5 100644 --- a/server/meson.build +++ b/server/meson.build @@ -4,7 +4,7 @@ prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', build_always: true, # gradle is responsible for tracking source changes - output: 'scrcpy-server.jar', + output: 'scrcpy-server', command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], console: true, install: true, @@ -16,7 +16,7 @@ else endif custom_target('scrcpy-server-prebuilt', input: prebuilt_server, - output: 'scrcpy-server.jar', + output: 'scrcpy-server', command: ['cp', '@INPUT@', '@OUTPUT@'], install: true, install_dir: 'share/scrcpy') diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 3bd2fcdc..eba89bdb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -7,7 +7,7 @@ import java.io.IOException; public final class Server { - private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; + private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server"; private Server() { // not instantiable From 95fd64b5dea57c4b94f2ab0a08f6421a134a48bf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2019 20:57:57 +0100 Subject: [PATCH 17/60] Add scrcpy version in recorded video metadata It might help to understand problems in recorded videos. --- app/src/recorder.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/recorder.c b/app/src/recorder.c index 77186350..f96bcd26 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -135,6 +135,9 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { // recorder->ctx->oformat = (AVOutputFormat *) format; + av_dict_set(&recorder->ctx->metadata, "comment", + "Recorded by scrcpy " SCRCPY_VERSION, 0); + AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); if (!ostream) { avformat_free_context(recorder->ctx); From 120f08ee96d29464bed9f4b1390244d8e2ccb81c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2019 16:51:47 +0100 Subject: [PATCH 18/60] Fix manpage option parameter format The parameter for --window-title was not underlined the same way as others. --- app/scrcpy.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 67db3569..1dafbc6a 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -103,7 +103,7 @@ Make scrcpy window always on top (above other windows). Print the version of scrcpy. .TP -.B \-\-window\-title text +.BI \-\-window\-title " text Set a custom window title. From 683f7ca848ad4785557d116dcea466f1b5654ef9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2019 19:31:56 +0100 Subject: [PATCH 19/60] Document how to attach a debugger to the server --- DEVELOP.md | 30 ++++++++++++++++++++++++++++++ app/meson.build | 3 +++ app/src/server.c | 16 ++++++++++++++++ meson_options.txt | 1 + 4 files changed, 50 insertions(+) diff --git a/DEVELOP.md b/DEVELOP.md index fb8ab91d..92c3ce87 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -268,3 +268,33 @@ For more details, go read the code! If you find a bug, or have an awesome idea to implement, please discuss and contribute ;-) + + +### Debug the server + +The server is pushed to the device by the client on startup. + +To debug it, enable the server debugger during configuration: + +```bash +meson x -Dserver_debugger=true +# or, if x is already configured +meson configure x -Dserver_debugger=true +``` + +Then recompile. + +When you start scrcpy, it will start a debugger on port 5005 on the device. +Redirect that port to the computer: + +```bash +adb forward tcp:5005 tcp:5005 +``` + +In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on +`+`, _Remote_, and fill the form: + + - Host: `localhost` + - Port: `5005` + +Then click on _Debug_. diff --git a/app/meson.build b/app/meson.build index 95587980..145e0ef6 100644 --- a/app/meson.build +++ b/app/meson.build @@ -115,6 +115,9 @@ conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) # disable console on Windows conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole')) +# run a server debugger and wait for a client to be attached +conf.set('SERVER_DEBUGGER', get_option('server_debugger')) + configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') diff --git a/app/src/server.c b/app/src/server.c index 4fe65402..de61001f 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -124,6 +124,11 @@ execute_server(struct server *server, const struct server_params *params) { "shell", "CLASSPATH=/data/local/tmp/" SERVER_FILENAME, "app_process", +#ifdef SERVER_DEBUGGER +# define SERVER_DEBUGGER_PORT "5005" + "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" + SERVER_DEBUGGER_PORT, +#endif "/", // unused "com.genymobile.scrcpy.Server", max_size_string, @@ -133,6 +138,17 @@ execute_server(struct server *server, const struct server_params *params) { "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", }; +#ifdef SERVER_DEBUGGER + LOGI("Server debugger waiting for a client on device port " + SERVER_DEBUGGER_PORT "..."); + // From the computer, run + // adb forward tcp:5005 tcp:5005 + // Then, from Android Studio: Run > Debug > Edit configurations... + // On the left, click on '+', "Remote", with: + // Host: localhost + // Port: 5005 + // Then click on "Debug" +#endif return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } diff --git a/meson_options.txt b/meson_options.txt index 84889597..4cf4a8bf 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -5,3 +5,4 @@ option('windows_noconsole', type: 'boolean', value: false, description: 'Disable option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') +option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') From 8d601d3210c0714c947a3029a1299182a849e348 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2019 11:51:54 +0200 Subject: [PATCH 20/60] Rename "input_manager" variables to "im" It is used a lot, a short name improves readability. --- app/src/input_manager.c | 56 ++++++++++++++++++++--------------------- app/src/input_manager.h | 12 ++++----- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index db15da75..8dfc712d 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -212,7 +212,7 @@ clipboard_paste(struct controller *controller) { } void -input_manager_process_text_input(struct input_manager *input_manager, +input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { char c = event->text[0]; if (isalpha(c) || c == ' ') { @@ -227,14 +227,14 @@ input_manager_process_text_input(struct input_manager *input_manager, LOGW("Could not strdup input text"); return; } - if (!controller_push_msg(input_manager->controller, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { SDL_free(msg.inject_text.text); LOGW("Could not request 'inject text'"); } } void -input_manager_process_key(struct input_manager *input_manager, +input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event, bool control) { // control: indicates the state of the command-line option --no-control @@ -261,7 +261,7 @@ input_manager_process_key(struct input_manager *input_manager, return; } - struct controller *controller = input_manager->controller; + struct controller *controller = im->controller; // capture all Ctrl events if (ctrl || cmd) { @@ -336,23 +336,23 @@ input_manager_process_key(struct input_manager *input_manager, return; case SDLK_f: if (!shift && cmd && !repeat && down) { - screen_switch_fullscreen(input_manager->screen); + screen_switch_fullscreen(im->screen); } return; case SDLK_x: if (!shift && cmd && !repeat && down) { - screen_resize_to_fit(input_manager->screen); + screen_resize_to_fit(im->screen); } return; case SDLK_g: if (!shift && cmd && !repeat && down) { - screen_resize_to_pixel_perfect(input_manager->screen); + screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: if (!shift && cmd && !repeat && down) { struct fps_counter *fps_counter = - input_manager->video_buffer->fps_counter; + im->video_buffer->fps_counter; switch_fps_counter_state(fps_counter); } return; @@ -383,7 +383,7 @@ input_manager_process_key(struct input_manager *input_manager, } void -input_manager_process_mouse_motion(struct input_manager *input_manager, +input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { if (!event->state) { // do not send motion events when no button is pressed @@ -394,33 +394,33 @@ input_manager_process_mouse_motion(struct input_manager *input_manager, return; } struct control_msg msg; - if (convert_mouse_motion(event, input_manager->screen->frame_size, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_mouse_motion(event, im->screen->frame_size, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); } } } void -input_manager_process_touch(struct input_manager *input_manager, +input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { struct control_msg msg; - if (convert_touch(event, input_manager->screen->frame_size, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_touch(event, im->screen->frame_size, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject touch event'"); } } } static bool -is_outside_device_screen(struct input_manager *input_manager, int x, int y) +is_outside_device_screen(struct input_manager *im, int x, int y) { - return x < 0 || x >= input_manager->screen->frame_size.width || - y < 0 || y >= input_manager->screen->frame_size.height; + return x < 0 || x >= im->screen->frame_size.width || + y < 0 || y >= im->screen->frame_size.height; } void -input_manager_process_mouse_button(struct input_manager *input_manager, +input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event, bool control) { if (event->which == SDL_TOUCH_MOUSEID) { @@ -429,19 +429,19 @@ input_manager_process_mouse_button(struct input_manager *input_manager, } if (event->type == SDL_MOUSEBUTTONDOWN) { if (control && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(input_manager->controller); + press_back_or_turn_screen_on(im->controller); return; } if (control && event->button == SDL_BUTTON_MIDDLE) { - action_home(input_manager->controller, ACTION_DOWN | ACTION_UP); + action_home(im->controller, ACTION_DOWN | ACTION_UP); return; } // double-click on black borders resize to fit the device screen if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { bool outside = - is_outside_device_screen(input_manager, event->x, event->y); + is_outside_device_screen(im, event->x, event->y); if (outside) { - screen_resize_to_fit(input_manager->screen); + screen_resize_to_fit(im->screen); return; } } @@ -453,23 +453,23 @@ input_manager_process_mouse_button(struct input_manager *input_manager, } struct control_msg msg; - if (convert_mouse_button(event, input_manager->screen->frame_size, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_mouse_button(event, im->screen->frame_size, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse button event'"); } } } void -input_manager_process_mouse_wheel(struct input_manager *input_manager, +input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { struct position position = { - .screen_size = input_manager->screen->frame_size, - .point = get_mouse_point(input_manager->screen), + .screen_size = im->screen->frame_size, + .point = get_mouse_point(im->screen), }; struct control_msg msg; if (convert_mouse_wheel(event, position, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse wheel event'"); } } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 0009cb81..934c529e 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -17,29 +17,29 @@ struct input_manager { }; void -input_manager_process_text_input(struct input_manager *input_manager, +input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event); void -input_manager_process_key(struct input_manager *input_manager, +input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event, bool control); void -input_manager_process_mouse_motion(struct input_manager *input_manager, +input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event); void -input_manager_process_touch(struct input_manager *input_manager, +input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event); void -input_manager_process_mouse_button(struct input_manager *input_manager, +input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event, bool control); void -input_manager_process_mouse_wheel(struct input_manager *input_manager, +input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event); #endif From b0db1178d1f61e9cef42189e60c4ce68cc1c0aee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2019 12:49:47 +0200 Subject: [PATCH 21/60] Move event conversion to input_manager Only keep helper functions separated. This will help to convert coordinates internally when necessary. --- app/src/event_converter.c | 97 ++------------------------------------- app/src/event_converter.h | 34 ++++---------- app/src/input_manager.c | 89 +++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 117 deletions(-) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index a634614e..00e989f7 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -5,7 +5,7 @@ #define MAP(FROM, TO) case FROM: *to = TO; return true #define FAIL default: return false -static bool +bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { switch (from) { MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); @@ -33,7 +33,7 @@ autocomplete_metastate(enum android_metastate metastate) { return metastate; } -static enum android_metastate +enum android_metastate convert_meta_state(SDL_Keymod mod) { enum android_metastate metastate = 0; if (mod & KMOD_LSHIFT) { @@ -74,7 +74,7 @@ convert_meta_state(SDL_Keymod mod) { return autocomplete_metastate(metastate); } -static bool +bool convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { switch (from) { MAP(SDLK_RETURN, AKEYCODE_ENTER); @@ -128,7 +128,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { } } -static enum android_motionevent_buttons +enum android_motionevent_buttons convert_mouse_buttons(uint32_t state) { enum android_motionevent_buttons buttons = 0; if (state & SDL_BUTTON_LMASK) { @@ -150,24 +150,6 @@ convert_mouse_buttons(uint32_t state) { } bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; - - if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { - return false; - } - - uint16_t mod = from->keysym.mod; - if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { - return false; - } - - to->inject_keycode.metastate = convert_meta_state(mod); - - return true; -} - -static bool convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { switch (from) { MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); @@ -177,41 +159,6 @@ convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { } bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { - return false; - } - - to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - to->inject_touch_event.position.screen_size = screen_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; - to->inject_touch_event.pressure = 1.f; - to->inject_touch_event.buttons = - convert_mouse_buttons(SDL_BUTTON(from->button)); - - return true; -} - -bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; - to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - to->inject_touch_event.position.screen_size = screen_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; - to->inject_touch_event.pressure = 1.f; - to->inject_touch_event.buttons = convert_mouse_buttons(from->state); - - return true; -} - -static bool convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { switch (from) { MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); @@ -220,39 +167,3 @@ convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { FAIL; } } - -bool -convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { - return false; - } - - to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = screen_size; - // SDL touch event coordinates are normalized in the range [0; 1] - to->inject_touch_event.position.point.x = from->x * screen_size.width; - to->inject_touch_event.position.point.y = from->y * screen_size.height; - to->inject_touch_event.pressure = from->pressure; - to->inject_touch_event.buttons = 0; - return true; -} - -bool -convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; - - to->inject_scroll_event.position = position; - - int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; - // SDL behavior seems inconsistent between horizontal and vertical scrolling - // so reverse the horizontal - // - to->inject_scroll_event.hscroll = -mul * from->x; - to->inject_scroll_event.vscroll = mul * from->y; - - return true; -} diff --git a/app/src/event_converter.h b/app/src/event_converter.h index f6f136a3..8bad7358 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -7,36 +7,22 @@ #include "config.h" #include "control_msg.h" -struct complete_mouse_motion_event { - SDL_MouseMotionEvent *mouse_motion_event; - struct size screen_size; -}; +bool +convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to); -struct complete_mouse_wheel_event { - SDL_MouseWheelEvent *mouse_wheel_event; - struct point position; -}; +enum android_metastate +convert_meta_state(SDL_Keymod mod); bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to); +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod); + +enum android_motionevent_buttons +convert_mouse_buttons(uint32_t state); bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_msg *to); - -// the video size may be different from the real device size, so we need the -// size to which the absolute position apply, to scale it accordingly -bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_msg *to); +convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to); bool -convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, - struct control_msg *to); - -// on Android, a scroll event requires the current mouse position -bool -convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, - struct control_msg *to); +convert_touch_action(SDL_EventType from, enum android_motionevent_action *to); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 8dfc712d..0fce979b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -233,6 +233,24 @@ input_manager_process_text_input(struct input_manager *im, } } +static bool +convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + + if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { + return false; + } + + uint16_t mod = from->keysym.mod; + if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { + return false; + } + + to->inject_keycode.metastate = convert_meta_state(mod); + + return true; +} + void input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event, @@ -382,6 +400,21 @@ input_manager_process_key(struct input_manager *im, } } +static bool +convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; + to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + to->inject_touch_event.position.screen_size = screen_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = convert_mouse_buttons(from->state); + + return true; +} + void input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { @@ -401,6 +434,25 @@ input_manager_process_mouse_motion(struct input_manager *im, } } +static bool +convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + to->inject_touch_event.pointer_id = from->fingerId; + to->inject_touch_event.position.screen_size = screen_size; + // SDL touch event coordinates are normalized in the range [0; 1] + to->inject_touch_event.position.point.x = from->x * screen_size.width; + to->inject_touch_event.position.point.y = from->y * screen_size.height; + to->inject_touch_event.pressure = from->pressure; + to->inject_touch_event.buttons = 0; + return true; +} + void input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { @@ -419,6 +471,26 @@ is_outside_device_screen(struct input_manager *im, int x, int y) y < 0 || y >= im->screen->frame_size.height; } +static bool +convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + to->inject_touch_event.position.screen_size = screen_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = + convert_mouse_buttons(SDL_BUTTON(from->button)); + + return true; +} + void input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event, @@ -460,6 +532,23 @@ input_manager_process_mouse_button(struct input_manager *im, } } +static bool +convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; + + to->inject_scroll_event.position = position; + + int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; + // SDL behavior seems inconsistent between horizontal and vertical scrolling + // so reverse the horizontal + // + to->inject_scroll_event.hscroll = -mul * from->x; + to->inject_scroll_event.vscroll = mul * from->y; + + return true; +} + void input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { From c42ff75b74a617d20a24e86d15da26a13a9bb828 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2019 14:09:07 +0200 Subject: [PATCH 22/60] Pass screen to mouse event converters Mouse events coordinates depend on the screen size and location, so the converter need to access the screen. The fact that it needs the position or the size is an internal detail, so pass a pointer to the whole screen structure. --- app/src/input_manager.c | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 0fce979b..fe891990 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -401,12 +401,12 @@ input_manager_process_key(struct input_manager *im, } static bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, +convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - to->inject_touch_event.position.screen_size = screen_size; + to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.point.x = from->x; to->inject_touch_event.position.point.y = from->y; to->inject_touch_event.pressure = 1.f; @@ -427,7 +427,7 @@ input_manager_process_mouse_motion(struct input_manager *im, return; } struct control_msg msg; - if (convert_mouse_motion(event, im->screen->frame_size, &msg)) { + if (convert_mouse_motion(event, im->screen, &msg)) { if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); } @@ -435,7 +435,7 @@ input_manager_process_mouse_motion(struct input_manager *im, } static bool -convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, +convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; @@ -443,11 +443,13 @@ convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, return false; } + struct size frame_size = screen->frame_size; + to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = screen_size; + to->inject_touch_event.position.screen_size = frame_size; // SDL touch event coordinates are normalized in the range [0; 1] - to->inject_touch_event.position.point.x = from->x * screen_size.width; - to->inject_touch_event.position.point.y = from->y * screen_size.height; + to->inject_touch_event.position.point.x = from->x * frame_size.width; + to->inject_touch_event.position.point.y = from->y * frame_size.height; to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.buttons = 0; return true; @@ -457,7 +459,7 @@ void input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { struct control_msg msg; - if (convert_touch(event, im->screen->frame_size, &msg)) { + if (convert_touch(event, im->screen, &msg)) { if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject touch event'"); } @@ -472,7 +474,7 @@ is_outside_device_screen(struct input_manager *im, int x, int y) } static bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, +convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; @@ -481,7 +483,7 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, } to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - to->inject_touch_event.position.screen_size = screen_size; + to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.point.x = from->x; to->inject_touch_event.position.point.y = from->y; to->inject_touch_event.pressure = 1.f; @@ -525,7 +527,7 @@ input_manager_process_mouse_button(struct input_manager *im, } struct control_msg msg; - if (convert_mouse_button(event, im->screen->frame_size, &msg)) { + if (convert_mouse_button(event, im->screen, &msg)) { if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse button event'"); } @@ -533,8 +535,13 @@ input_manager_process_mouse_button(struct input_manager *im, } static bool -convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, +convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, struct control_msg *to) { + struct position position = { + .screen_size = screen->frame_size, + .point = get_mouse_point(screen), + }; + to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; to->inject_scroll_event.position = position; @@ -552,12 +559,8 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, void input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { - struct position position = { - .screen_size = im->screen->frame_size, - .point = get_mouse_point(im->screen), - }; struct control_msg msg; - if (convert_mouse_wheel(event, position, &msg)) { + if (convert_mouse_wheel(event, im->screen, &msg)) { if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse wheel event'"); } From 0e301ddf19c45de50ed728419adff9e6792332bf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 6 Nov 2019 22:06:54 +0100 Subject: [PATCH 23/60] Factorize scrcpy options and command-line args Do not duplicate all scrcpy options fields in the structure storing the parsed command-line arguments. --- app/src/main.c | 105 +++++++++++++-------------------------------- app/src/recorder.h | 3 +- app/src/scrcpy.h | 19 ++++++++ 3 files changed, 52 insertions(+), 75 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 41383ed9..d2a13237 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -14,24 +14,9 @@ #include "recorder.h" struct args { - const char *serial; - const char *crop; - const char *record_filename; - const char *window_title; - const char *push_target; - enum recorder_format record_format; - bool fullscreen; - bool no_control; - bool no_display; + struct scrcpy_options opts; bool help; bool version; - bool show_touches; - uint16_t port; - uint16_t max_size; - uint32_t bit_rate; - bool always_on_top; - bool turn_screen_off; - bool render_expired_frames; }; static void usage(const char *arg0) { @@ -339,23 +324,26 @@ parse_args(struct args *args, int argc, char *argv[]) { OPT_WINDOW_TITLE}, {NULL, 0, NULL, 0 }, }; + + struct scrcpy_options *opts = &args->opts; + int c; while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, NULL)) != -1) { switch (c) { case 'b': - if (!parse_bit_rate(optarg, &args->bit_rate)) { + if (!parse_bit_rate(optarg, &opts->bit_rate)) { return false; } break; case 'c': - args->crop = optarg; + opts->crop = optarg; break; case 'f': - args->fullscreen = true; + opts->fullscreen = true; break; case 'F': - if (!parse_record_format(optarg, &args->record_format)) { + if (!parse_record_format(optarg, &opts->record_format)) { return false; } break; @@ -363,47 +351,47 @@ parse_args(struct args *args, int argc, char *argv[]) { args->help = true; break; case 'm': - if (!parse_max_size(optarg, &args->max_size)) { + if (!parse_max_size(optarg, &opts->max_size)) { return false; } break; case 'n': - args->no_control = true; + opts->control = false; break; case 'N': - args->no_display = true; + opts->display = false; break; case 'p': - if (!parse_port(optarg, &args->port)) { + if (!parse_port(optarg, &opts->port)) { return false; } break; case 'r': - args->record_filename = optarg; + opts->record_filename = optarg; break; case 's': - args->serial = optarg; + opts->serial = optarg; break; case 'S': - args->turn_screen_off = true; + opts->turn_screen_off = true; break; case 't': - args->show_touches = true; + opts->show_touches = true; break; case 'T': - args->always_on_top = true; + opts->always_on_top = true; break; case 'v': args->version = true; break; case OPT_RENDER_EXPIRED_FRAMES: - args->render_expired_frames = true; + opts->render_expired_frames = true; break; case OPT_WINDOW_TITLE: - args->window_title = optarg; + opts->window_title = optarg; break; case OPT_PUSH_TARGET: - args->push_target = optarg; + opts->push_target = optarg; break; default: // getopt prints the error message on stderr @@ -411,12 +399,12 @@ parse_args(struct args *args, int argc, char *argv[]) { } } - if (args->no_display && !args->record_filename) { + if (!opts->display && !opts->record_filename) { LOGE("-N/--no-display requires screen recording (-r/--record)"); return false; } - if (args->no_display && args->fullscreen) { + if (!opts->display && opts->fullscreen) { LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); return false; } @@ -427,21 +415,21 @@ parse_args(struct args *args, int argc, char *argv[]) { return false; } - if (args->record_format && !args->record_filename) { + if (opts->record_format && !opts->record_filename) { LOGE("Record format specified without recording"); return false; } - if (args->record_filename && !args->record_format) { - args->record_format = guess_record_format(args->record_filename); - if (!args->record_format) { + if (opts->record_filename && !opts->record_format) { + opts->record_format = guess_record_format(opts->record_filename); + if (!opts->record_format) { LOGE("No format specified for \"%s\" (try with -F mkv)", - args->record_filename); + opts->record_filename); return false; } } - if (args->no_control && args->turn_screen_off) { + if (!opts->control && opts->turn_screen_off) { LOGE("Could not request to turn screen off if control is disabled"); return false; } @@ -458,24 +446,11 @@ main(int argc, char *argv[]) { setbuf(stderr, NULL); #endif struct args args = { - .serial = NULL, - .crop = NULL, - .record_filename = NULL, - .window_title = NULL, - .push_target = NULL, - .record_format = 0, + .opts = SCRCPY_OPTIONS_DEFAULT, .help = false, .version = false, - .show_touches = false, - .port = DEFAULT_LOCAL_PORT, - .max_size = DEFAULT_MAX_SIZE, - .bit_rate = DEFAULT_BIT_RATE, - .always_on_top = false, - .no_control = false, - .no_display = false, - .turn_screen_off = false, - .render_expired_frames = false, }; + if (!parse_args(&args, argc, argv)) { return 1; } @@ -504,25 +479,7 @@ main(int argc, char *argv[]) { SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); #endif - struct scrcpy_options options = { - .serial = args.serial, - .crop = args.crop, - .port = args.port, - .record_filename = args.record_filename, - .window_title = args.window_title, - .push_target = args.push_target, - .record_format = args.record_format, - .max_size = args.max_size, - .bit_rate = args.bit_rate, - .show_touches = args.show_touches, - .fullscreen = args.fullscreen, - .always_on_top = args.always_on_top, - .control = !args.no_control, - .display = !args.no_display, - .turn_screen_off = args.turn_screen_off, - .render_expired_frames = args.render_expired_frames, - }; - int res = scrcpy(&options) ? 0 : 1; + int res = scrcpy(&args.opts) ? 0 : 1; avformat_network_deinit(); // ignore failure diff --git a/app/src/recorder.h b/app/src/recorder.h index b1953fcb..4ad77197 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -11,7 +11,8 @@ #include "queue.h" enum recorder_format { - RECORDER_FORMAT_MP4 = 1, + RECORDER_FORMAT_AUTO, + RECORDER_FORMAT_MP4, RECORDER_FORMAT_MKV, }; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 1593fb1e..62430e79 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -26,6 +26,25 @@ struct scrcpy_options { bool render_expired_frames; }; +#define SCRCPY_OPTIONS_DEFAULT { \ + .serial = NULL, \ + .crop = NULL, \ + .record_filename = NULL, \ + .window_title = NULL, \ + .push_target = NULL, \ + .record_format = RECORDER_FORMAT_AUTO, \ + .port = DEFAULT_LOCAL_PORT, \ + .max_size = DEFAULT_LOCAL_PORT, \ + .bit_rate = DEFAULT_BIT_RATE, \ + .show_touches = false, \ + .fullscreen = false, \ + .always_on_top = false, \ + .control = true, \ + .display = true, \ + .turn_screen_off = false, \ + .render_expired_frames = false, \ +} + bool scrcpy(const struct scrcpy_options *options); From 2d90e1befdd8e751a1d710ab4824cc8fa28cce90 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 6 Nov 2019 22:22:23 +0100 Subject: [PATCH 24/60] Fix include recorder.h --- app/src/scrcpy.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 62430e79..4bc24742 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -3,9 +3,9 @@ #include #include -#include #include "config.h" +#include "recorder.h" struct scrcpy_options { const char *serial; From 157c60feb4de2010de2b4d357076f1645b816cba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 7 Nov 2019 09:48:48 +0100 Subject: [PATCH 25/60] Fix indentation --- app/src/main.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index d2a13237..cd03f195 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -310,18 +310,17 @@ parse_args(struct args *args, int argc, char *argv[]) { {"no-control", no_argument, NULL, 'n'}, {"no-display", no_argument, NULL, 'N'}, {"port", required_argument, NULL, 'p'}, - {"push-target", required_argument, NULL, - OPT_PUSH_TARGET}, + {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, {"record-format", required_argument, NULL, 'F'}, {"render-expired-frames", no_argument, NULL, - OPT_RENDER_EXPIRED_FRAMES}, + OPT_RENDER_EXPIRED_FRAMES}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, {"version", no_argument, NULL, 'v'}, {"window-title", required_argument, NULL, - OPT_WINDOW_TITLE}, + OPT_WINDOW_TITLE}, {NULL, 0, NULL, 0 }, }; From ff061b4f30c54dedc5073a588c6c697477b805db Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 7 Nov 2019 09:58:06 +0100 Subject: [PATCH 26/60] Deprecate short options for advanced features The short options will be removed in the future (and may be reused for other features). --- README.md | 2 -- app/src/main.c | 18 +++++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ebf593a1..f0717c2a 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,6 @@ This is useful for example to mirror only one eye of the Oculus Go: ```bash scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) -scrcpy -c 1224:1440:0:0 # short version ``` If `--max-size` is also specified, resizing is applied after cropping. @@ -226,7 +225,6 @@ The window of app can always be above others by: ```bash scrcpy --always-on-top -scrcpy -T # short version ``` diff --git a/app/src/main.c b/app/src/main.c index cd03f195..1d5beb64 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -297,13 +297,16 @@ guess_record_format(const char *filename) { #define OPT_RENDER_EXPIRED_FRAMES 1000 #define OPT_WINDOW_TITLE 1001 #define OPT_PUSH_TARGET 1002 +#define OPT_ALWAYS_ON_TOP 1003 +#define OPT_CROP 1004 +#define OPT_RECORD_FORMAT 1005 static bool parse_args(struct args *args, int argc, char *argv[]) { static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, 'T'}, + {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"bit-rate", required_argument, NULL, 'b'}, - {"crop", required_argument, NULL, 'c'}, + {"crop", required_argument, NULL, OPT_CROP}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"max-size", required_argument, NULL, 'm'}, @@ -312,7 +315,7 @@ parse_args(struct args *args, int argc, char *argv[]) { {"port", required_argument, NULL, 'p'}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, 'F'}, + {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, {"render-expired-frames", no_argument, NULL, OPT_RENDER_EXPIRED_FRAMES}, {"serial", required_argument, NULL, 's'}, @@ -336,12 +339,18 @@ parse_args(struct args *args, int argc, char *argv[]) { } break; case 'c': + LOGW("Deprecated option -c. Use --crop instead."); + // fall through + case OPT_CROP: opts->crop = optarg; break; case 'f': opts->fullscreen = true; break; case 'F': + LOGW("Deprecated option -F. Use --record-format instead."); + // fall through + case OPT_RECORD_FORMAT: if (!parse_record_format(optarg, &opts->record_format)) { return false; } @@ -378,6 +387,9 @@ parse_args(struct args *args, int argc, char *argv[]) { opts->show_touches = true; break; case 'T': + LOGW("Deprecated option -T. Use --always-on-top instead."); + // fall through + case OPT_ALWAYS_ON_TOP: opts->always_on_top = true; break; case 'v': From c916af0984f72a60301d13fa8ef9a85112f54202 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 7 Nov 2019 19:01:35 +0100 Subject: [PATCH 27/60] Add --prefer-text option Expose an option to configure how key/text events are forwarded to the Android device. Enabling the option avoids issues when combining multiple keys to enter special characters, but breaks the expected behavior of alpha keys in games (typically WASD). Fixes --- app/scrcpy.1 | 7 +++++++ app/src/event_converter.c | 9 ++++++++- app/src/event_converter.h | 3 ++- app/src/input_manager.c | 21 +++++++++++++-------- app/src/input_manager.h | 1 + app/src/main.c | 12 ++++++++++++ app/src/scrcpy.c | 3 +++ app/src/scrcpy.h | 3 +++ 8 files changed, 49 insertions(+), 10 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1dafbc6a..203395a4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -61,6 +61,13 @@ Set the TCP port the client listens on. Default is 27183. +.TP +.B \-\-prefer\-text +Inject alpha characters and space as text events instead of key events. + +This avoids issues when combining multiple keys to enter special characters, +but breaks the expected behavior of alpha keys in games (typically WASD). + .TP .BI "\-\-push\-target " path Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". diff --git a/app/src/event_converter.c b/app/src/event_converter.c index 00e989f7..80ead615 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -75,7 +75,8 @@ convert_meta_state(SDL_Keymod mod) { } bool -convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, + bool prefer_text) { switch (from) { MAP(SDLK_RETURN, AKEYCODE_ENTER); MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); @@ -92,6 +93,12 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_UP, AKEYCODE_DPAD_UP); } + + if (prefer_text) { + // do not forward alpha and space key events + return false; + } + if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { return false; } diff --git a/app/src/event_converter.h b/app/src/event_converter.h index 8bad7358..c41887e1 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -14,7 +14,8 @@ enum android_metastate convert_meta_state(SDL_Keymod mod); bool -convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod); +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, + bool prefer_text); enum android_motionevent_buttons convert_mouse_buttons(uint32_t state); diff --git a/app/src/input_manager.c b/app/src/input_manager.c index fe891990..7d333c1b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -214,12 +214,15 @@ clipboard_paste(struct controller *controller) { void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { - char c = event->text[0]; - if (isalpha(c) || c == ' ') { - SDL_assert(event->text[1] == '\0'); - // letters and space are handled as raw key event - return; + if (!im->prefer_text) { + char c = event->text[0]; + if (isalpha(c) || c == ' ') { + SDL_assert(event->text[1] == '\0'); + // letters and space are handled as raw key event + return; + } } + struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = SDL_strdup(event->text); @@ -234,7 +237,8 @@ input_manager_process_text_input(struct input_manager *im, } static bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { +convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, + bool prefer_text) { to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { @@ -242,7 +246,8 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { } uint16_t mod = from->keysym.mod; - if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { + if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod, + prefer_text)) { return false; } @@ -393,7 +398,7 @@ input_manager_process_key(struct input_manager *im, } struct control_msg msg; - if (convert_input_key(event, &msg)) { + if (convert_input_key(event, &msg, im->prefer_text)) { if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'inject keycode'"); } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 934c529e..43fc0eeb 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -14,6 +14,7 @@ struct input_manager { struct controller *controller; struct video_buffer *video_buffer; struct screen *screen; + bool prefer_text; }; void diff --git a/app/src/main.c b/app/src/main.c index 1d5beb64..12c65ed4 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -67,6 +67,13 @@ static void usage(const char *arg0) { " Set the TCP port the client listens on.\n" " Default is %d.\n" "\n" + " --prefer-text\n" + " Inject alpha characters and space as text events instead of\n" + " key events.\n" + " This avoids issues when combining multiple keys to enter a\n" + " special character, but breaks the expected behavior of alpha\n" + " keys in games (typically WASD).\n" + "\n" " --push-target path\n" " Set the target directory for pushing files to the device by\n" " drag & drop. It is passed as-is to \"adb push\".\n" @@ -300,6 +307,7 @@ guess_record_format(const char *filename) { #define OPT_ALWAYS_ON_TOP 1003 #define OPT_CROP 1004 #define OPT_RECORD_FORMAT 1005 +#define OPT_PREFER_TEXT 1006 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -321,6 +329,7 @@ parse_args(struct args *args, int argc, char *argv[]) { {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, + {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"version", no_argument, NULL, 'v'}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, @@ -404,6 +413,9 @@ parse_args(struct args *args, int argc, char *argv[]) { case OPT_PUSH_TARGET: opts->push_target = optarg; break; + case OPT_PREFER_TEXT: + opts->prefer_text = true; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c219c9e5..16f1b4f7 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -42,6 +42,7 @@ static struct input_manager input_manager = { .controller = &controller, .video_buffer = &video_buffer, .screen = &screen, + .prefer_text = false, // initialized later }; // init SDL and set appropriate hints @@ -414,6 +415,8 @@ scrcpy(const struct scrcpy_options *options) { show_touches_waited = true; } + input_manager.prefer_text = options->prefer_text; + ret = event_loop(options->display, options->control); LOGD("quit..."); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 4bc24742..70a41ec1 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -5,6 +5,7 @@ #include #include "config.h" +#include "input_manager.h" #include "recorder.h" struct scrcpy_options { @@ -24,6 +25,7 @@ struct scrcpy_options { bool display; bool turn_screen_off; bool render_expired_frames; + bool prefer_text; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -43,6 +45,7 @@ struct scrcpy_options { .display = true, \ .turn_screen_off = false, \ .render_expired_frames = false, \ + .prefer_text = false, \ } bool From b08a98324d35298903098ec8ea9023ecf9515a2f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 7 Nov 2019 21:58:57 +0100 Subject: [PATCH 28/60] Fix segfault on empty file recorded Write the file trailer only if the file header have been written, to avoid a segfault in libav. Fixes . --- app/src/recorder.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index f96bcd26..c09e21ae 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -174,9 +174,14 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { void recorder_close(struct recorder *recorder) { - int ret = av_write_trailer(recorder->ctx); - if (ret < 0) { - LOGE("Failed to write trailer to %s", recorder->filename); + if (recorder->header_written) { + int ret = av_write_trailer(recorder->ctx); + if (ret < 0) { + LOGE("Failed to write trailer to %s", recorder->filename); + recorder->failed = true; + } + } else { + // the recorded file is empty recorder->failed = true; } avio_close(recorder->ctx->pb); From e282100d0b796b579b2a9b9656cf962cca88aca0 Mon Sep 17 00:00:00 2001 From: olbb Date: Tue, 19 Feb 2019 10:33:59 +0800 Subject: [PATCH 29/60] Call Looper.prepareMainLooper() to avoid exception Some devices internally create a Handler when creating an input Surface, causing an exception: > Surface: java.lang.RuntimeException: Can't create handler inside > thread that has not called Looper.prepare() As a workaround, call Looper.prepareMainLooper() beforehand. Fixes: - - Signed-off-by: Romain Vimont --- .../java/com/genymobile/scrcpy/ScreenEncoder.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 8357b061..52f6f26b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -7,6 +7,7 @@ import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.IBinder; +import android.os.Looper; import android.view.Surface; import java.io.FileDescriptor; @@ -54,6 +55,16 @@ public class ScreenEncoder implements Device.RotationListener { } public void streamScreen(Device device, FileDescriptor fd) throws IOException { + // Some devices internally create a Handler when creating an input Surface, causing an exception: + // "Can't create handler inside thread that has not called Looper.prepare()" + // + // + // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: + // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' + // on a null object reference" + // + Looper.prepareMainLooper(); + MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval); device.setRotationListener(this); boolean alive; From 6996cbf5d38b2f5bfdc061b421ea204e3aafce67 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 9 Nov 2019 21:13:20 +0100 Subject: [PATCH 30/60] Log device disconnection If scrcpy closes due to socket disconnection, log a warning. --- app/src/scrcpy.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 16f1b4f7..64165ac5 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -218,6 +218,7 @@ event_loop(bool display, bool control) { case EVENT_RESULT_STOPPED_BY_USER: return true; case EVENT_RESULT_STOPPED_BY_EOS: + LOGW("Device disconnected"); return false; case EVENT_RESULT_CONTINUE: break; From f6f2868868497b2804e3308b774f52bcc5418a5c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2019 15:32:33 +0200 Subject: [PATCH 31/60] Handle window events from screen.c Only the screen knows what to do on window events. This paves the way to handle more window events. --- app/src/scrcpy.c | 7 +------ app/src/screen.c | 11 +++++++++++ app/src/screen.h | 4 ++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 64165ac5..0e56696a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -144,12 +144,7 @@ handle_event(SDL_Event *event, bool control) { } break; case SDL_WINDOWEVENT: - switch (event->window.event) { - case SDL_WINDOWEVENT_EXPOSED: - case SDL_WINDOWEVENT_SIZE_CHANGED: - screen_render(&screen); - break; - } + screen_handle_window_event(&screen, &event->window); break; case SDL_TEXTINPUT: if (!control) { diff --git a/app/src/screen.c b/app/src/screen.c index 4bc4c5c5..0ef803fe 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -327,3 +327,14 @@ screen_resize_to_pixel_perfect(struct screen *screen) { LOGD("Resized to pixel-perfect"); } } + +void +screen_handle_window_event(struct screen *screen, + const SDL_WindowEvent *event) { + switch (event->event) { + case SDL_WINDOWEVENT_EXPOSED: + case SDL_WINDOWEVENT_SIZE_CHANGED: + screen_render(screen); + break; + } +} diff --git a/app/src/screen.h b/app/src/screen.h index bc189189..1c5695bc 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -76,4 +76,8 @@ screen_resize_to_fit(struct screen *screen); void screen_resize_to_pixel_perfect(struct screen *screen); +// react to window events +void +screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); + #endif From 35c05bb3cec7e5270229574d0f9d17d4989c107e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 11 Nov 2019 21:26:35 +0100 Subject: [PATCH 32/60] Fix rotation while the window is maximized Keep the windowed window size to handle maximized window the same way as fullscreen window. Fixes --- app/src/screen.c | 68 ++++++++++++++++++++++++++++++++++-------------- app/src/screen.h | 39 ++++++++++++++++----------- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 0ef803fe..df9af985 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -30,23 +30,28 @@ get_window_size(SDL_Window *window) { // get the windowed window size static struct size get_windowed_window_size(const struct screen *screen) { - if (screen->fullscreen) { + if (screen->fullscreen || screen->maximized) { return screen->windowed_window_size; } return get_window_size(screen->window); } +// apply the windowed window size if fullscreen and maximized are disabled +static void +apply_windowed_size(struct screen *screen) { + if (!screen->fullscreen && !screen->maximized) { + SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, + screen->windowed_window_size.height); + } +} + // set the window size to be applied when fullscreen is disabled static void set_window_size(struct screen *screen, struct size new_size) { // setting the window size during fullscreen is implementation defined, // so apply the resize only after fullscreen is disabled - if (screen->fullscreen) { - // SDL_SetWindowSize will be called when fullscreen will be disabled - screen->windowed_window_size = new_size; - } else { - SDL_SetWindowSize(screen->window, new_size.width, new_size.height); - } + screen->windowed_window_size = new_size; + apply_windowed_size(screen); } // get the preferred display bounds (i.e. the screen bounds with some margins) @@ -194,6 +199,8 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } + screen->windowed_window_size = window_size; + return true; } @@ -287,10 +294,6 @@ screen_render(struct screen *screen) { void screen_switch_fullscreen(struct screen *screen) { - if (!screen->fullscreen) { - // going to fullscreen, store the current windowed window size - screen->windowed_window_size = get_window_size(screen->window); - } uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; if (SDL_SetWindowFullscreen(screen->window, new_mode)) { LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); @@ -298,11 +301,7 @@ screen_switch_fullscreen(struct screen *screen) { } screen->fullscreen = !screen->fullscreen; - if (!screen->fullscreen) { - // fullscreen disabled, restore expected windowed window size - SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, - screen->windowed_window_size.height); - } + apply_windowed_size(screen); LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); screen_render(screen); @@ -310,7 +309,7 @@ screen_switch_fullscreen(struct screen *screen) { void screen_resize_to_fit(struct screen *screen) { - if (!screen->fullscreen) { + if (!screen->fullscreen && !screen->maximized) { struct size optimal_size = get_optimal_window_size(screen, screen->frame_size); SDL_SetWindowSize(screen->window, optimal_size.width, @@ -321,7 +320,7 @@ screen_resize_to_fit(struct screen *screen) { void screen_resize_to_pixel_perfect(struct screen *screen) { - if (!screen->fullscreen) { + if (!screen->fullscreen && !screen->maximized) { SDL_SetWindowSize(screen->window, screen->frame_size.width, screen->frame_size.height); LOGD("Resized to pixel-perfect"); @@ -333,8 +332,39 @@ screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event) { switch (event->event) { case SDL_WINDOWEVENT_EXPOSED: - case SDL_WINDOWEVENT_SIZE_CHANGED: screen_render(screen); break; + case SDL_WINDOWEVENT_SIZE_CHANGED: + if (!screen->fullscreen && !screen->maximized) { + // Backup the previous size: if we receive the MAXIMIZED event, + // then the new size must be ignored (it's the maximized size). + // We could not rely on the window flags due to race conditions + // (they could be updated asynchronously, at least on X11). + screen->windowed_window_size_backup = + screen->windowed_window_size; + + // Save the windowed size, so that it is available once the + // window is maximized or fullscreen is enabled. + screen->windowed_window_size = get_window_size(screen->window); + } + screen_render(screen); + break; + case SDL_WINDOWEVENT_MAXIMIZED: + // The backup size must be non-nul. + SDL_assert(screen->windowed_window_size_backup.width); + SDL_assert(screen->windowed_window_size_backup.height); + // Revert the last size, it was updated while screen was maximized. + screen->windowed_window_size = screen->windowed_window_size_backup; +#ifdef DEBUG + // Reset the backup to invalid values to detect unexpected usage + screen->windowed_window_size_backup.width = 0; + screen->windowed_window_size_backup.height = 0; +#endif + screen->maximized = true; + break; + case SDL_WINDOWEVENT_RESTORED: + screen->maximized = false; + apply_windowed_size(screen); + break; } } diff --git a/app/src/screen.h b/app/src/screen.h index 1c5695bc..275609ba 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -15,28 +15,37 @@ struct screen { SDL_Renderer *renderer; SDL_Texture *texture; struct size frame_size; - //used only in fullscreen mode to know the windowed window size + // The window size the last time it was not maximized or fullscreen. struct size windowed_window_size; + // Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be + // able to revert the size to its non-maximized value. + struct size windowed_window_size_backup; bool has_frame; bool fullscreen; + bool maximized; bool no_window; }; -#define SCREEN_INITIALIZER { \ - .window = NULL, \ - .renderer = NULL, \ - .texture = NULL, \ - .frame_size = { \ - .width = 0, \ - .height = 0, \ - }, \ +#define SCREEN_INITIALIZER { \ + .window = NULL, \ + .renderer = NULL, \ + .texture = NULL, \ + .frame_size = { \ + .width = 0, \ + .height = 0, \ + }, \ .windowed_window_size = { \ - .width = 0, \ - .height = 0, \ - }, \ - .has_frame = false, \ - .fullscreen = false, \ - .no_window = false, \ + .width = 0, \ + .height = 0, \ + }, \ + .windowed_window_size_backup = { \ + .width = 0, \ + .height = 0, \ + }, \ + .has_frame = false, \ + .fullscreen = false, \ + .maximized = false, \ + .no_window = false, \ } // initialize default values From aa0f77c8983ef8049c2eba17bace430cf39ba171 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 11 Nov 2019 21:44:27 +0100 Subject: [PATCH 33/60] Accept resize shortcuts on maximized window Allow "resize to fit" and "resize to pixel-perfect" on maximized window: restore the window to normal size then resize. --- app/src/screen.c | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index df9af985..7de57031 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -309,22 +309,35 @@ screen_switch_fullscreen(struct screen *screen) { void screen_resize_to_fit(struct screen *screen) { - if (!screen->fullscreen && !screen->maximized) { - struct size optimal_size = get_optimal_window_size(screen, - screen->frame_size); - SDL_SetWindowSize(screen->window, optimal_size.width, - optimal_size.height); - LOGD("Resized to optimal size"); + if (screen->fullscreen) { + return; } + + if (screen->maximized) { + SDL_RestoreWindow(screen->window); + screen->maximized = false; + } + + struct size optimal_size = + get_optimal_window_size(screen, screen->frame_size); + SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); + LOGD("Resized to optimal size"); } void screen_resize_to_pixel_perfect(struct screen *screen) { - if (!screen->fullscreen && !screen->maximized) { - SDL_SetWindowSize(screen->window, screen->frame_size.width, - screen->frame_size.height); - LOGD("Resized to pixel-perfect"); + if (screen->fullscreen) { + return; } + + if (screen->maximized) { + SDL_RestoreWindow(screen->window); + screen->maximized = false; + } + + SDL_SetWindowSize(screen->window, screen->frame_size.width, + screen->frame_size.height); + LOGD("Resized to pixel-perfect"); } void From b963a3b9d56f744cceba2e19cba3f9fef858c058 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sun, 10 Nov 2019 20:23:58 +0800 Subject: [PATCH 34/60] Check client and server mismatch Send client version as first parameter and check it at server start. Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- app/src/server.c | 1 + .../java/com/genymobile/scrcpy/Server.java | 26 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index de61001f..b40b065b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -131,6 +131,7 @@ execute_server(struct server *server, const struct server_params *params) { #endif "/", // unused "com.genymobile.scrcpy.Server", + SCRCPY_VERSION, max_size_string, bit_rate_string, server->tunnel_forward ? "true" : "false", diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index eba89bdb..ba44d07c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -67,29 +67,39 @@ public final class Server { @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { - if (args.length != 6) { - throw new IllegalArgumentException("Expecting 6 parameters"); + if (args.length < 1) { + throw new IllegalArgumentException("Missing client version"); + } + + String clientVersion = args[0]; + if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { + throw new IllegalArgumentException("The server version (" + clientVersion + ") does not match the client " + + "(" + BuildConfig.VERSION_NAME + ")"); + } + + if (args.length != 7) { + throw new IllegalArgumentException("Expecting 7 parameters"); } Options options = new Options(); - int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8 + int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8 options.setMaxSize(maxSize); - int bitRate = Integer.parseInt(args[1]); + int bitRate = Integer.parseInt(args[2]); options.setBitRate(bitRate); // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[2]); + boolean tunnelForward = Boolean.parseBoolean(args[3]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[3]); + Rect crop = parseCrop(args[4]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[4]); + boolean sendFrameMeta = Boolean.parseBoolean(args[5]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[5]); + boolean control = Boolean.parseBoolean(args[6]); options.setControl(control); return options; From 771bd8404d289d433586fdf25c02ebb4e812623e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 13 Nov 2019 16:04:16 +0100 Subject: [PATCH 35/60] Do not write invalid packet duration Configuration packets have no PTS. Do not compute a packet duration from their PTS. Fixes recording to mp4 on device rotation. --- app/src/recorder.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index c09e21ae..f6f6fd96 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -301,8 +301,12 @@ run_recorder(void *data) { continue; } - // we now know the duration of the previous packet - previous->packet.duration = rec->packet.pts - previous->packet.pts; + // config packets have no PTS, we must ignore them + if (rec->packet.pts != AV_NOPTS_VALUE + && previous->packet.pts != AV_NOPTS_VALUE) { + // we now know the duration of the previous packet + previous->packet.duration = rec->packet.pts - previous->packet.pts; + } bool ok = recorder_write(recorder, &previous->packet); record_packet_delete(previous); From ce5635f28cec04f20dc3ef1c205e1740ac115c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Fernando=20D=C3=ADaz=20A?= Date: Thu, 29 Aug 2019 00:25:17 -0500 Subject: [PATCH 36/60] Add option to specify the initial window position Add --window-x and --window-y parameters. Signed-off-by: Romain Vimont --- app/src/main.c | 46 ++++++++++++++++++++++++++++++++++++++++++++-- app/src/scrcpy.c | 3 ++- app/src/scrcpy.h | 4 ++++ app/src/screen.c | 8 +++++--- app/src/screen.h | 3 ++- 5 files changed, 57 insertions(+), 7 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 12c65ed4..348c3df0 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -110,6 +110,14 @@ static void usage(const char *arg0) { " --window-title text\n" " Set a custom window title.\n" "\n" + " --window-x value\n" + " Set the initial window horizontal position.\n" + " Default is -1 (automatic).\n" + "\n" + " --window-y value\n" + " Set the initial window vertical position.\n" + " Default is -1 (automatic).\n" + "\n" "Shortcuts:\n" "\n" " " CTRL_OR_CMD "+f\n" @@ -250,6 +258,27 @@ parse_max_size(char *optarg, uint16_t *max_size) { return true; } +static bool +parse_window_position(char *optarg, int16_t *position) { + char *endptr; + if (*optarg == '\0') { + LOGE("Window position parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + if (*endptr != '\0') { + LOGE("Invalid window position: %s", optarg); + return false; + } + if (value < -1 || value > 0x7fff) { + LOGE("Window position must be between -1 and 32767: %ld", value); + return false; + } + + *position = (int16_t) value; + return true; +} + static bool parse_port(char *optarg, uint16_t *port) { char *endptr; @@ -308,6 +337,8 @@ guess_record_format(const char *filename) { #define OPT_CROP 1004 #define OPT_RECORD_FORMAT 1005 #define OPT_PREFER_TEXT 1006 +#define OPT_WINDOW_X 1007 +#define OPT_WINDOW_Y 1008 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -331,8 +362,9 @@ parse_args(struct args *args, int argc, char *argv[]) { {"turn-screen-off", no_argument, NULL, 'S'}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"version", no_argument, NULL, 'v'}, - {"window-title", required_argument, NULL, - OPT_WINDOW_TITLE}, + {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, + {"window-x", required_argument, NULL, OPT_WINDOW_X}, + {"window-y", required_argument, NULL, OPT_WINDOW_Y}, {NULL, 0, NULL, 0 }, }; @@ -410,6 +442,16 @@ parse_args(struct args *args, int argc, char *argv[]) { case OPT_WINDOW_TITLE: opts->window_title = optarg; break; + case OPT_WINDOW_X: + if (!parse_window_position(optarg, &opts->window_x)) { + return false; + } + break; + case OPT_WINDOW_Y: + if (!parse_window_position(optarg, &opts->window_y)) { + return false; + } + break; case OPT_PUSH_TARGET: opts->push_target = optarg; break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 0e56696a..d9f0e308 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -387,7 +387,8 @@ scrcpy(const struct scrcpy_options *options) { options->window_title ? options->window_title : device_name; if (!screen_init_rendering(&screen, window_title, frame_size, - options->always_on_top)) { + options->always_on_top, options->window_x, + options->window_y)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 70a41ec1..d0ef2392 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -18,6 +18,8 @@ struct scrcpy_options { uint16_t port; uint16_t max_size; uint32_t bit_rate; + int16_t window_x; + int16_t window_y; bool show_touches; bool fullscreen; bool always_on_top; @@ -38,6 +40,8 @@ struct scrcpy_options { .port = DEFAULT_LOCAL_PORT, \ .max_size = DEFAULT_LOCAL_PORT, \ .bit_rate = DEFAULT_BIT_RATE, \ + .window_x = -1, \ + .window_y = -1, \ .show_touches = false, \ .fullscreen = false, \ .always_on_top = false, \ diff --git a/app/src/screen.c b/app/src/screen.c index 7de57031..4543fab3 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -141,7 +141,8 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) { bool screen_init_rendering(struct screen *screen, const char *window_title, - struct size frame_size, bool always_on_top) { + struct size frame_size, bool always_on_top, + int16_t window_x, int16_t window_y) { screen->frame_size = frame_size; struct size window_size = get_initial_optimal_size(frame_size); @@ -158,8 +159,9 @@ screen_init_rendering(struct screen *screen, const char *window_title, #endif } - screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, + int x = window_x != -1 ? window_x : SDL_WINDOWPOS_UNDEFINED; + int y = window_y != -1 ? window_y : SDL_WINDOWPOS_UNDEFINED; + screen->window = SDL_CreateWindow(window_title, x, y, window_size.width, window_size.height, window_flags); if (!screen->window) { diff --git a/app/src/screen.h b/app/src/screen.h index 275609ba..204e3226 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -55,7 +55,8 @@ screen_init(struct screen *screen); // initialize screen, create window, renderer and texture (window is hidden) bool screen_init_rendering(struct screen *screen, const char *window_title, - struct size frame_size, bool always_on_top); + struct size frame_size, bool always_on_top, + int16_t window_x, int16_t window_y); // show the window void From b6e2f8ae00737098af5139b56cd3ca15bd5c9335 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2019 17:34:01 +0100 Subject: [PATCH 37/60] Update manpage for --window-{x,y} options --- app/scrcpy.1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 203395a4..fb39189b 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -113,6 +113,18 @@ Print the version of scrcpy. .BI \-\-window\-title " text Set a custom window title. +.TP +.BI \-\-window\-x " value +Set the initial window horizontal position. + +Default is -1 (automatic).\n + +.TP +.BI \-\-window\-y " value +Set the initial window vertical position. + +Default is -1 (automatic).\n + .SH SHORTCUTS From 9fd7a80a897103c6675e651b7f1e78b1575ab148 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2019 18:00:11 +0100 Subject: [PATCH 38/60] Add option to specify the initial window size Add --window-width and --window-height parameters. If only one is provided, the other is computed so that the aspect ratio is preserved. --- app/scrcpy.1 | 11 +++++++++++ app/src/main.c | 43 +++++++++++++++++++++++++++++++++++++++++++ app/src/scrcpy.c | 3 ++- app/src/scrcpy.h | 4 ++++ app/src/screen.c | 31 +++++++++++++++++++++++++++---- app/src/screen.h | 3 ++- 6 files changed, 89 insertions(+), 6 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index fb39189b..bde58d65 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -125,6 +125,17 @@ Set the initial window vertical position. Default is -1 (automatic).\n +.TP +.BI \-\-window\-width " value +Set the initial window width. + +Default is 0 (automatic).\n + +.TP +.BI \-\-window\-height " value +Set the initial window height. + +Default is 0 (automatic).\n .SH SHORTCUTS diff --git a/app/src/main.c b/app/src/main.c index 348c3df0..0046a12d 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -118,6 +118,14 @@ static void usage(const char *arg0) { " Set the initial window vertical position.\n" " Default is -1 (automatic).\n" "\n" + " --window-width value\n" + " Set the initial window width.\n" + " Default is -1 (automatic).\n" + "\n" + " --window-height value\n" + " Set the initial window width.\n" + " Default is -1 (automatic).\n" + "\n" "Shortcuts:\n" "\n" " " CTRL_OR_CMD "+f\n" @@ -279,6 +287,27 @@ parse_window_position(char *optarg, int16_t *position) { return true; } +static bool +parse_window_dimension(char *optarg, uint16_t *dimension) { + char *endptr; + if (*optarg == '\0') { + LOGE("Window dimension parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + if (*endptr != '\0') { + LOGE("Invalid window dimension: %s", optarg); + return false; + } + if (value & ~0xffff) { + LOGE("Window position must be between 0 and 65535: %ld", value); + return false; + } + + *dimension = (uint16_t) value; + return true; +} + static bool parse_port(char *optarg, uint16_t *port) { char *endptr; @@ -339,6 +368,8 @@ guess_record_format(const char *filename) { #define OPT_PREFER_TEXT 1006 #define OPT_WINDOW_X 1007 #define OPT_WINDOW_Y 1008 +#define OPT_WINDOW_WIDTH 1009 +#define OPT_WINDOW_HEIGHT 1010 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -365,6 +396,8 @@ parse_args(struct args *args, int argc, char *argv[]) { {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"window-x", required_argument, NULL, OPT_WINDOW_X}, {"window-y", required_argument, NULL, OPT_WINDOW_Y}, + {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, + {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, {NULL, 0, NULL, 0 }, }; @@ -452,6 +485,16 @@ parse_args(struct args *args, int argc, char *argv[]) { return false; } break; + case OPT_WINDOW_WIDTH: + if (!parse_window_dimension(optarg, &opts->window_width)) { + return false; + } + break; + case OPT_WINDOW_HEIGHT: + if (!parse_window_dimension(optarg, &opts->window_height)) { + return false; + } + break; case OPT_PUSH_TARGET: opts->push_target = optarg; break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d9f0e308..5213d779 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -388,7 +388,8 @@ scrcpy(const struct scrcpy_options *options) { if (!screen_init_rendering(&screen, window_title, frame_size, options->always_on_top, options->window_x, - options->window_y)) { + options->window_y, options->window_width, + options->window_height)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d0ef2392..d0612172 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -20,6 +20,8 @@ struct scrcpy_options { uint32_t bit_rate; int16_t window_x; int16_t window_y; + uint16_t window_width; + uint16_t window_height; bool show_touches; bool fullscreen; bool always_on_top; @@ -42,6 +44,8 @@ struct scrcpy_options { .bit_rate = DEFAULT_BIT_RATE, \ .window_x = -1, \ .window_y = -1, \ + .window_width = 0, \ + .window_height = 0, \ .show_touches = false, \ .fullscreen = false, \ .always_on_top = false, \ diff --git a/app/src/screen.c b/app/src/screen.c index 4543fab3..3d021e01 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -122,9 +122,30 @@ get_optimal_window_size(const struct screen *screen, struct size frame_size) { } // initially, there is no current size, so use the frame size as current size +// req_width and req_height, if not 0, are the sizes requested by the user static inline struct size -get_initial_optimal_size(struct size frame_size) { - return get_optimal_size(frame_size, frame_size); +get_initial_optimal_size(struct size frame_size, uint16_t req_width, + uint16_t req_height) { + struct size window_size; + if (!req_width && !req_height) { + window_size = get_optimal_size(frame_size, frame_size); + } else { + if (req_width) { + window_size.width = req_width; + } else { + // compute from the requested height + window_size.width = (uint32_t) req_height * frame_size.width + / frame_size.height; + } + if (req_height) { + window_size.height = req_height; + } else { + // compute from the requested width + window_size.height = (uint32_t) req_width * frame_size.height + / frame_size.width; + } + } + return window_size; } void @@ -142,10 +163,12 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) { bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, - int16_t window_x, int16_t window_y) { + int16_t window_x, int16_t window_y, uint16_t window_width, + uint16_t window_height) { screen->frame_size = frame_size; - struct size window_size = get_initial_optimal_size(frame_size); + struct size window_size = + get_initial_optimal_size(frame_size, window_width, window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; #ifdef HIDPI_SUPPORT window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; diff --git a/app/src/screen.h b/app/src/screen.h index 204e3226..eaa46850 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -56,7 +56,8 @@ screen_init(struct screen *screen); bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, - int16_t window_x, int16_t window_y); + int16_t window_x, int16_t window_y, uint16_t window_width, + uint16_t window_height); // show the window void From 59bc5bc1f55a2671062fb5b6000067f55fdb751f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Fernando=20D=C3=ADaz=20A?= Date: Thu, 29 Aug 2019 00:25:17 -0500 Subject: [PATCH 39/60] Add option to disable window decoration Add --window-borderless parameter. Signed-off-by: Romain Vimont --- app/src/main.c | 9 +++++++++ app/src/scrcpy.c | 3 ++- app/src/scrcpy.h | 2 ++ app/src/screen.c | 5 ++++- app/src/screen.h | 2 +- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 0046a12d..2e8b7897 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -107,6 +107,9 @@ static void usage(const char *arg0) { " -v, --version\n" " Print the version of scrcpy.\n" "\n" + " --window-borderless\n" + " Disable window decorations (display borderless window).\n" + "\n" " --window-title text\n" " Set a custom window title.\n" "\n" @@ -370,6 +373,7 @@ guess_record_format(const char *filename) { #define OPT_WINDOW_Y 1008 #define OPT_WINDOW_WIDTH 1009 #define OPT_WINDOW_HEIGHT 1010 +#define OPT_WINDOW_BORDERLESS 1011 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -398,6 +402,8 @@ parse_args(struct args *args, int argc, char *argv[]) { {"window-y", required_argument, NULL, OPT_WINDOW_Y}, {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, + {"window-borderless", no_argument, NULL, + OPT_WINDOW_BORDERLESS}, {NULL, 0, NULL, 0 }, }; @@ -495,6 +501,9 @@ parse_args(struct args *args, int argc, char *argv[]) { return false; } break; + case OPT_WINDOW_BORDERLESS: + opts->window_borderless = true; + break; case OPT_PUSH_TARGET: opts->push_target = optarg; break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5213d779..c64acf49 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -389,7 +389,8 @@ scrcpy(const struct scrcpy_options *options) { if (!screen_init_rendering(&screen, window_title, frame_size, options->always_on_top, options->window_x, options->window_y, options->window_width, - options->window_height)) { + options->window_height, + options->window_borderless)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d0612172..f6779080 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -30,6 +30,7 @@ struct scrcpy_options { bool turn_screen_off; bool render_expired_frames; bool prefer_text; + bool window_borderless; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -54,6 +55,7 @@ struct scrcpy_options { .turn_screen_off = false, \ .render_expired_frames = false, \ .prefer_text = false, \ + .window_borderless = false, \ } bool diff --git a/app/src/screen.c b/app/src/screen.c index 3d021e01..ab4d434e 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -164,7 +164,7 @@ bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, int16_t window_x, int16_t window_y, uint16_t window_width, - uint16_t window_height) { + uint16_t window_height, bool window_borderless) { screen->frame_size = frame_size; struct size window_size = @@ -181,6 +181,9 @@ screen_init_rendering(struct screen *screen, const char *window_title, "(compile with SDL >= 2.0.5 to enable it)"); #endif } + if (window_borderless) { + window_flags |= SDL_WINDOW_BORDERLESS; + } int x = window_x != -1 ? window_x : SDL_WINDOWPOS_UNDEFINED; int y = window_y != -1 ? window_y : SDL_WINDOWPOS_UNDEFINED; diff --git a/app/src/screen.h b/app/src/screen.h index eaa46850..2346ff15 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -57,7 +57,7 @@ bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, int16_t window_x, int16_t window_y, uint16_t window_width, - uint16_t window_height); + uint16_t window_height, bool window_borderless); // show the window void From 59073223aad2aae42d5033b0a01e7838a8f6cd46 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Nov 2019 18:59:47 +0100 Subject: [PATCH 40/60] Update manpage for --window-borderless option --- app/scrcpy.1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index bde58d65..2547658e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -109,6 +109,10 @@ Make scrcpy window always on top (above other windows). .B \-v, \-\-version Print the version of scrcpy. +.TP +.B \-\-window\-borderless +Disable window decorations (display borderless window). + .TP .BI \-\-window\-title " text Set a custom window title. From 1b78a77962bd19cd89c488a55972ec9a50eb1031 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Nov 2019 21:38:10 +0100 Subject: [PATCH 41/60] Fix error message --- app/src/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main.c b/app/src/main.c index 2e8b7897..23d258d5 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -315,7 +315,7 @@ static bool parse_port(char *optarg, uint16_t *port) { char *endptr; if (*optarg == '\0') { - LOGE("Invalid port parameter is empty"); + LOGE("Port parameter is empty"); return false; } long value = strtol(optarg, &endptr, 0); From fb976816f98d0c9792cf63c414b5fbfaf9e2c4a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Nov 2019 15:35:28 +0100 Subject: [PATCH 42/60] Do not expose frame rate in ScreenEncoder The KEY_FRAME_RATE parameter value is necessary for the configuration of the encoder, but its actual value does not impact the frame rate (only resources used by the encoder). Therefore, it's an internal detail and should not be exposed by the ScreenEncoder class. --- .../com/genymobile/scrcpy/ScreenEncoder.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 52f6f26b..f0b6db44 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -17,32 +17,27 @@ import java.util.concurrent.atomic.AtomicBoolean; public class ScreenEncoder implements Device.RotationListener { - private static final int DEFAULT_FRAME_RATE = 60; // fps private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds + private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms - private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames - - private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000; private static final int NO_PTS = -1; private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private int bitRate; - private int frameRate; private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int iFrameInterval) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; - this.frameRate = frameRate; this.iFrameInterval = iFrameInterval; } public ScreenEncoder(boolean sendFrameMeta, int bitRate) { - this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL); + this(sendFrameMeta, bitRate, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -65,7 +60,7 @@ public class ScreenEncoder implements Device.RotationListener { // Looper.prepareMainLooper(); - MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval); + MediaFormat format = createFormat(bitRate, iFrameInterval); device.setRotationListener(this); boolean alive; try { @@ -148,15 +143,16 @@ public class ScreenEncoder implements Device.RotationListener { return MediaCodec.createEncoderByType("video/avc"); } - private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException { + private static MediaFormat createFormat(int bitRate, int iFrameInterval) throws IOException { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, "video/avc"); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); - format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); + // must be present to configure the encoder, but does not impact the actual frame rate, which is variable + format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); // display the very first frame, and recover from bad quality when no new frames - format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs + format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs return format; } From 1d97d7213d004ba4ac781baa13e9a0884b305cc3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Nov 2019 22:07:19 +0100 Subject: [PATCH 43/60] Add option --max-fps Add an option to limit the capture frame rate. It only works for devices with Android >= 10. Fixes --- README.md | 8 +++++ app/scrcpy.1 | 4 +++ app/src/main.c | 33 +++++++++++++++++++ app/src/scrcpy.c | 1 + app/src/scrcpy.h | 2 ++ app/src/server.c | 3 ++ app/src/server.h | 1 + .../java/com/genymobile/scrcpy/Options.java | 9 +++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 21 +++++++++--- .../java/com/genymobile/scrcpy/Server.java | 17 ++++++---- 10 files changed, 87 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f0717c2a..17703152 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,14 @@ scrcpy --bit-rate 2M scrcpy -b 2M # short version ``` +### Limit capture frame rate + +On device with Android >= 10, the capture frame rate can be limited: + +```bash +scrcpy --max-fps 15 +``` + ### Crop diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 2547658e..948cac4d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -41,6 +41,10 @@ Force recording format (either mp4 or mkv). .B \-h, \-\-help Print this help. +.TP +.BI \-\-max\-fps " value +Limit the framerate of screen capture (only supported on devices with Android >= 10). + .TP .BI "\-m, \-\-max\-size " value Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. diff --git a/app/src/main.c b/app/src/main.c index 23d258d5..da0d2074 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -50,6 +50,10 @@ static void usage(const char *arg0) { " -h, --help\n" " Print this help.\n" "\n" + " --max-fps value\n" + " Limit the frame rate of screen capture (only supported on\n" + " devices with Android >= 10).\n" + "\n" " -m, --max-size value\n" " Limit both the width and height of the video to value. The\n" " other dimension is computed so that the device aspect-ratio\n" @@ -269,6 +273,28 @@ parse_max_size(char *optarg, uint16_t *max_size) { return true; } +static bool +parse_max_fps(const char *optarg, uint16_t *max_fps) { + char *endptr; + if (*optarg == '\0') { + LOGE("Max FPS parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + if (*endptr != '\0') { + LOGE("Invalid max FPS: %s", optarg); + return false; + } + if (value & ~0xffff) { + // in practice, it should not be higher than 60 + LOGE("Max FPS value is invalid: %ld", value); + return false; + } + + *max_fps = (uint16_t) value; + return true; +} + static bool parse_window_position(char *optarg, int16_t *position) { char *endptr; @@ -374,6 +400,7 @@ guess_record_format(const char *filename) { #define OPT_WINDOW_WIDTH 1009 #define OPT_WINDOW_HEIGHT 1010 #define OPT_WINDOW_BORDERLESS 1011 +#define OPT_MAX_FPS 1012 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -383,6 +410,7 @@ parse_args(struct args *args, int argc, char *argv[]) { {"crop", required_argument, NULL, OPT_CROP}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, + {"max-fps", required_argument, NULL, OPT_MAX_FPS}, {"max-size", required_argument, NULL, 'm'}, {"no-control", no_argument, NULL, 'n'}, {"no-display", no_argument, NULL, 'N'}, @@ -438,6 +466,11 @@ parse_args(struct args *args, int argc, char *argv[]) { case 'h': args->help = true; break; + case OPT_MAX_FPS: + if (!parse_max_fps(optarg, &opts->max_fps)) { + return false; + } + break; case 'm': if (!parse_max_size(optarg, &opts->max_size)) { return false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c64acf49..67f1de16 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -280,6 +280,7 @@ scrcpy(const struct scrcpy_options *options) { .local_port = options->port, .max_size = options->max_size, .bit_rate = options->bit_rate, + .max_fps = options->max_fps, .control = options->control, }; if (!server_start(&server, options->serial, ¶ms)) { diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index f6779080..8723f29f 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -18,6 +18,7 @@ struct scrcpy_options { uint16_t port; uint16_t max_size; uint32_t bit_rate; + uint16_t max_fps; int16_t window_x; int16_t window_y; uint16_t window_width; @@ -43,6 +44,7 @@ struct scrcpy_options { .port = DEFAULT_LOCAL_PORT, \ .max_size = DEFAULT_LOCAL_PORT, \ .bit_rate = DEFAULT_BIT_RATE, \ + .max_fps = 0, \ .window_x = -1, \ .window_y = -1, \ .window_width = 0, \ diff --git a/app/src/server.c b/app/src/server.c index b40b065b..36290326 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -118,8 +118,10 @@ static process_t 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]; sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); + sprintf(max_fps_string, "%"PRIu16, params->max_fps); const char *const cmd[] = { "shell", "CLASSPATH=/data/local/tmp/" SERVER_FILENAME, @@ -134,6 +136,7 @@ execute_server(struct server *server, const struct server_params *params) { SCRCPY_VERSION, max_size_string, bit_rate_string, + max_fps_string, server->tunnel_forward ? "true" : "false", params->crop ? params->crop : "-", "true", // always send frame meta (packet boundaries + timestamp) diff --git a/app/src/server.h b/app/src/server.h index 2140d8ab..f46ced19 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -35,6 +35,7 @@ struct server_params { uint16_t local_port; uint16_t max_size; uint32_t bit_rate; + uint16_t max_fps; bool control; }; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index af6b2ee1..5b993f30 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -5,6 +5,7 @@ import android.graphics.Rect; public class Options { private int maxSize; private int bitRate; + private int maxFps; private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly @@ -26,6 +27,14 @@ public class Options { this.bitRate = bitRate; } + public int getMaxFps() { + return maxFps; + } + + public void setMaxFps(int maxFps) { + this.maxFps = maxFps; + } + public boolean isTunnelForward() { return tunnelForward; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index f0b6db44..e58310a1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -6,6 +6,7 @@ import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; +import android.os.Build; import android.os.IBinder; import android.os.Looper; import android.view.Surface; @@ -26,18 +27,20 @@ public class ScreenEncoder implements Device.RotationListener { private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private int bitRate; + private int maxFps; private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; + this.maxFps = maxFps; this.iFrameInterval = iFrameInterval; } - public ScreenEncoder(boolean sendFrameMeta, int bitRate) { - this(sendFrameMeta, bitRate, DEFAULT_I_FRAME_INTERVAL); + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { + this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -60,7 +63,7 @@ public class ScreenEncoder implements Device.RotationListener { // Looper.prepareMainLooper(); - MediaFormat format = createFormat(bitRate, iFrameInterval); + MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); device.setRotationListener(this); boolean alive; try { @@ -143,7 +146,8 @@ public class ScreenEncoder implements Device.RotationListener { return MediaCodec.createEncoderByType("video/avc"); } - private static MediaFormat createFormat(int bitRate, int iFrameInterval) throws IOException { + @SuppressWarnings("checkstyle:MagicNumber") + private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, "video/avc"); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); @@ -153,6 +157,13 @@ public class ScreenEncoder implements Device.RotationListener { format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); // display the very first frame, and recover from bad quality when no new frames format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs + if (maxFps > 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps); + } else { + Ln.w("Max FPS is only supported since Android 10, the option has been ignored"); + } + } return format; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ba44d07c..ad14e5d8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -17,7 +17,7 @@ public final class Server { final Device device = new Device(options); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { - ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate()); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); if (options.getControl()) { Controller controller = new Controller(device, connection); @@ -77,8 +77,8 @@ public final class Server { + "(" + BuildConfig.VERSION_NAME + ")"); } - if (args.length != 7) { - throw new IllegalArgumentException("Expecting 7 parameters"); + if (args.length != 8) { + throw new IllegalArgumentException("Expecting 8 parameters"); } Options options = new Options(); @@ -89,17 +89,20 @@ public final class Server { int bitRate = Integer.parseInt(args[2]); options.setBitRate(bitRate); + int maxFps = Integer.parseInt(args[3]); + options.setMaxFps(maxFps); + // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[3]); + boolean tunnelForward = Boolean.parseBoolean(args[4]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[4]); + Rect crop = parseCrop(args[5]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[5]); + boolean sendFrameMeta = Boolean.parseBoolean(args[6]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[6]); + boolean control = Boolean.parseBoolean(args[7]); options.setControl(control); return options; From 7fd800d58324a3f7520e9b225a9860ac5c712708 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 18 Nov 2019 14:30:21 +0100 Subject: [PATCH 44/60] Generate VERSION_NAME in build_without_gradle.sh Since commit b963a3b9d56f744cceba2e19cba3f9fef858c058, the server uses BuildConfig.VERSION_NAME. Generate this field manually for building without gradle. --- server/build_without_gradle.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index daf85008..d1581ea1 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -11,6 +11,8 @@ set -e +SCRCPY_VERSION_NAME=1.10 + PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} @@ -31,6 +33,7 @@ package com.genymobile.scrcpy; public final class BuildConfig { public static final boolean DEBUG = false; + public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME"; } EOF From 601b0fecdd4fff284cb8ac38e545fcec9b3cf0fd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 18 Nov 2019 14:33:14 +0100 Subject: [PATCH 45/60] Extract DEBUG flag in build_without_gradle.sh --- server/build_without_gradle.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index d1581ea1..b4605aa9 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -11,6 +11,7 @@ set -e +SCRCPY_DEBUG=false SCRCPY_VERSION_NAME=1.10 PLATFORM=${ANDROID_PLATFORM:-29} @@ -32,7 +33,7 @@ mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" package com.genymobile.scrcpy; public final class BuildConfig { - public static final boolean DEBUG = false; + public static final boolean DEBUG = $SCRCPY_DEBUG; public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME"; } EOF From 7aed5d5b60c7ae82f5ca2353c294076a2e0ffc2c Mon Sep 17 00:00:00 2001 From: senta2006 Date: Fri, 15 Nov 2019 17:44:24 +0900 Subject: [PATCH 46/60] Fix typos PR Signed-off-by: Romain Vimont --- app/src/server.c | 2 +- config/checkstyle/checkstyle.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 36290326..b37b39d0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -281,7 +281,7 @@ server_connect_to(struct server *server) { server->control_socket = net_accept(server->server_socket); if (server->control_socket == INVALID_SOCKET) { - // the video_socket will be clean up on destroy + // the video_socket will be cleaned up on destroy return false; } diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 63ee315a..798814d9 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -54,7 +54,7 @@ page at http://checkstyle.sourceforge.net/config.html --> - + @@ -99,7 +99,7 @@ page at http://checkstyle.sourceforge.net/config.html --> - + From 18f2e33a8bf967a813142dbe9556813032ec9f0b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 12:22:11 +0100 Subject: [PATCH 47/60] Fix noconsole.exe The linker flag "-mwindows" has no effect on my current MinGW. Instead, passing "-Wl,--subsystem,windows" works. Fixes --- app/meson.build | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/meson.build b/app/meson.build index 145e0ef6..159ae695 100644 --- a/app/meson.build +++ b/app/meson.build @@ -123,10 +123,8 @@ configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') if get_option('windows_noconsole') - c_args = [ '-mwindows' ] - link_args = [ '-mwindows' ] + link_args = [ '-Wl,--subsystem,windows' ] else - c_args = [] link_args = [] endif @@ -134,7 +132,7 @@ executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, - c_args: c_args, + c_args: [], link_args: link_args) install_man('scrcpy.1') From 213c468c2032d1c1490c6a7951c999c36f452b8f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 13 Nov 2019 12:08:36 +0100 Subject: [PATCH 48/60] Move workarounds to a separate class Extract workarounds (currently only one) to a separate class to avoid polluting the main code. --- .../com/genymobile/scrcpy/ScreenEncoder.java | 11 +--------- .../com/genymobile/scrcpy/Workarounds.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/Workarounds.java diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index e58310a1..504e9540 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -8,7 +8,6 @@ import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.Build; import android.os.IBinder; -import android.os.Looper; import android.view.Surface; import java.io.FileDescriptor; @@ -53,15 +52,7 @@ public class ScreenEncoder implements Device.RotationListener { } public void streamScreen(Device device, FileDescriptor fd) throws IOException { - // Some devices internally create a Handler when creating an input Surface, causing an exception: - // "Can't create handler inside thread that has not called Looper.prepare()" - // - // - // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: - // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' - // on a null object reference" - // - Looper.prepareMainLooper(); + Workarounds.prepareMainLooper(); MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); device.setRotationListener(this); diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java new file mode 100644 index 00000000..4dbf152e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy; + +import android.os.Looper; + +public final class Workarounds { + private Workarounds() { + // not instantiable + } + + public static void prepareMainLooper() { + // Some devices internally create a Handler when creating an input Surface, causing an exception: + // "Can't create handler inside thread that has not called Looper.prepare()" + // + // + // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: + // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' + // on a null object reference" + // + Looper.prepareMainLooper(); + } +} From 90293240cc622bb58cb1de741f86cbc0889c03e8 Mon Sep 17 00:00:00 2001 From: act262 Date: Mon, 28 Oct 2019 11:18:53 +0800 Subject: [PATCH 49/60] Fix meizu 16th NPE Fill AppInfo to avoid NullPointerException on some devices. Fixes Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/ScreenEncoder.java | 1 + .../com/genymobile/scrcpy/Workarounds.java | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 504e9540..c9a37f84 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -53,6 +53,7 @@ public class ScreenEncoder implements Device.RotationListener { public void streamScreen(Device device, FileDescriptor fd) throws IOException { Workarounds.prepareMainLooper(); + Workarounds.fillAppInfo(); MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); device.setRotationListener(this); diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 4dbf152e..f45d82a4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -1,7 +1,12 @@ package com.genymobile.scrcpy; +import android.annotation.SuppressLint; +import android.content.pm.ApplicationInfo; import android.os.Looper; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + public final class Workarounds { private Workarounds() { // not instantiable @@ -18,4 +23,42 @@ public final class Workarounds { // Looper.prepareMainLooper(); } + + @SuppressLint("PrivateApi") + public static void fillAppInfo() { + try { + // ActivityThread activityThread = new ActivityThread(); + Class activityThreadClass = Class.forName("android.app.ActivityThread"); + Constructor activityThreadConstructor = activityThreadClass.getDeclaredConstructor(); + activityThreadConstructor.setAccessible(true); + Object activityThread = activityThreadConstructor.newInstance(); + + // ActivityThread.sCurrentActivityThread = activityThread; + Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); + sCurrentActivityThreadField.setAccessible(true); + sCurrentActivityThreadField.set(null, activityThread); + + // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); + Class appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); + Constructor appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); + appBindDataConstructor.setAccessible(true); + Object appBindData = appBindDataConstructor.newInstance(); + + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = "com.genymobile.scrcpy"; + + // appBindData.appInfo = applicationInfo; + Field appInfo = appBindDataClass.getDeclaredField("appInfo"); + appInfo.setAccessible(true); + appInfo.set(appBindData, applicationInfo); + + // activityThread.mBoundApplication = appBindData; + Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); + mBoundApplicationField.setAccessible(true); + mBoundApplicationField.set(activityThread, appBindData); + } catch (Throwable throwable) { + // this is a workaround, so failing is not an error + Ln.w("Could not fill app info: " + throwable.getMessage()); + } + } } From b145b8d5f4455ed4ebef852a4f63654dbe57b483 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 18:41:43 +0100 Subject: [PATCH 50/60] Reorganize features in README --- README.md | 187 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 105 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 17703152..b4e9a20e 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,9 @@ scrcpy --help ## Features +### Capture configuration -### Reduce size +#### Reduce size Sometimes, it is useful to mirror an Android device at a lower definition to increase performance. @@ -125,7 +126,7 @@ The other dimension is computed to that the device aspect ratio is preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. -### Change bit-rate +#### Change bit-rate The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps): @@ -134,16 +135,15 @@ scrcpy --bit-rate 2M scrcpy -b 2M # short version ``` -### Limit capture frame rate +#### Limit frame rate -On device with Android >= 10, the capture frame rate can be limited: +On devices with Android >= 10, the capture frame rate can be limited: ```bash scrcpy --max-fps 15 ``` - -### Crop +#### Crop The device screen may be cropped to mirror only part of the screen. @@ -156,29 +156,7 @@ scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) If `--max-size` is also specified, resizing is applied after cropping. -### Wireless - -_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a -device over TCP/IP: - -1. Connect the device to the same Wi-Fi as your computer. -2. Get your device IP address (in Settings → About phone → Status). -3. Enable adb over TCP/IP on your device: `adb tcpip 5555`. -4. Unplug your device. -5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. -6. Run `scrcpy` as usual. - -It may be useful to decrease the bit-rate and the definition: - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # short version -``` - -[connect]: https://developer.android.com/studio/command-line/adb.html#wireless - - -### Record screen +### Recording It is possible to record the screen while mirroring: @@ -203,7 +181,31 @@ variation] does not impact the recorded file. [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation -### Multi-devices +### Connection + +#### Wireless + +_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a +device over TCP/IP: + +1. Connect the device to the same Wi-Fi as your computer. +2. Get your device IP address (in Settings → About phone → Status). +3. Enable adb over TCP/IP on your device: `adb tcpip 5555`. +4. Unplug your device. +5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. +6. Run `scrcpy` as usual. + +It may be useful to decrease the bit-rate and the definition: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # short version +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### Multi-devices If several devices are listed in `adb devices`, you must specify the _serial_: @@ -215,7 +217,41 @@ scrcpy -s 0123456789abcdef # short version You can start several instances of _scrcpy_ for several devices. -### Fullscreen +### Window configuration + +#### Title + +By default, the window title is the device model. It can be changed: + +```bash +scrcpy --window-title 'My device' +``` + +#### Position and size + +The initial window position and size may be specified: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Borderless + +To disable window decorations: + +```bash +scrcpy --window-borderless +``` + +#### Always on top + +To keep the scrcpy window always on top: + +```bash +scrcpy --always-on-top +``` + +#### Fullscreen The app may be started directly in fullscreen: @@ -227,16 +263,45 @@ scrcpy -f # short version Fullscreen can then be toggled dynamically with `Ctrl`+`f`. -### Always on top +### Other mirroring options -The window of app can always be above others by: +#### Read-only + +To disable controls (everything which can interact with the device: input keys, +mouse events, drag&drop files): ```bash -scrcpy --always-on-top +scrcpy --no-control +scrcpy -n ``` +#### Turn screen off -### Show touches +It is possible to turn the device screen off while mirroring on start with a +command-line option: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Or by pressing `Ctrl`+`o` at any time. + +To turn it back on, press `POWER` (or `Ctrl`+`p`). + +#### Render expired frames + +By default, to minimize latency, _scrcpy_ always renders the last decoded frame +available, and drops any previous one. + +To force the rendering of all frames (at a cost of a possible increased +latency), use: + +```bash +scrcpy --render-expired-frames +``` + +#### Show touches For presentations, it may be useful to show physical touches (on the physical device). @@ -253,7 +318,9 @@ scrcpy -t Note that it only shows _physical_ touches (with the finger on the device). -### Install APK +### File drop + +#### Install APK To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_ window. @@ -261,7 +328,7 @@ window. There is no visual feedback, a log is printed to the console. -### Push file to device +#### Push file to device To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the _scrcpy_ window. @@ -274,53 +341,9 @@ The target directory can be changed on start: scrcpy --push-target /sdcard/foo/bar/ ``` -### Read-only - -To disable controls (everything which can interact with the device: input keys, -mouse events, drag&drop files): - -```bash -scrcpy --no-control -scrcpy -n -``` - -### Turn screen off - -It is possible to turn the device screen off while mirroring on start with a -command-line option: - -```bash -scrcpy --turn-screen-off -scrcpy -S -``` - -Or by pressing `Ctrl`+`o` at any time. - -To turn it back on, press `POWER` (or `Ctrl`+`p`). -### Render expired frames - -By default, to minimize latency, _scrcpy_ always renders the last decoded frame -available, and drops any previous one. - -To force the rendering of all frames (at a cost of a possible increased -latency), use: - -```bash -scrcpy --render-expired-frames -``` - -### Custom window title - -By default, the window title is the device model. It can be changed: - -```bash -scrcpy --window-title 'My device' -``` - - -### Forward audio +### Audio forwarding Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only). From 704c0ff4dd6f260ad25fd1c1d7e61c4b6089ac49 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 19:24:34 +0100 Subject: [PATCH 51/60] Document copy-paste and --prefer-text in README --- README.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b4e9a20e..776a72e0 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,40 @@ scrcpy -t Note that it only shows _physical_ touches (with the finger on the device). +### Input control + +#### Copy-paste + +It is possible to synchronize clipboards between the computer and the device, in +both directions: + + - `Ctrl`+`c` copies the device clipboard to the computer clipboard; + - `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard; + - `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but + breaks non-ASCII characters). + +#### Text injection preference + +There are two kinds of [events][textevents] generated when typing text: + - _key events_, signaling that a key is pressed or released; + - _text events_, signaling that a text has been entered. + +By default, letters are injected using key events, so that the keyboard behaves +as expected in games (typically for WASD keys). + +But this may [cause issues][prefertext]. If you encounter such a problem, you +can avoid it by: + +```bash +scrcpy --prefer-text +``` + +(but this will break keyboard behavior in games) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + ### File drop #### Install APK @@ -342,7 +376,6 @@ scrcpy --push-target /sdcard/foo/bar/ ``` - ### Audio forwarding Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only). From c610a6b3c7983c89c92e04bb9e7805db14221e74 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 21:52:46 +0100 Subject: [PATCH 52/60] Document scrcpy via SSH tunnel in README --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 776a72e0..f957724d 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,29 @@ scrcpy -s 0123456789abcdef # short version You can start several instances of _scrcpy_ for several devices. +#### SSH tunnel + +To connect to a remote device, it is possible to connect a local `adb` client to +a remote `adb` server (provided they use the same version of the _adb_ +protocol): + +```bash +adb kill-server # kill the local adb server on 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# keep this open +``` + +From another terminal: + +```bash +scrcpy +``` + +Like for wireless connections, it may be useful to reduce quality: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` ### Window configuration From 3599fcaae545c4945d6cc480e5728a8ab815f106 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 22:57:48 +0100 Subject: [PATCH 53/60] Fix help for --window-width and --window-height The default value is 0 (automatic), not -1. --- app/src/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index da0d2074..04be1ab0 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -127,11 +127,11 @@ static void usage(const char *arg0) { "\n" " --window-width value\n" " Set the initial window width.\n" - " Default is -1 (automatic).\n" + " Default is 0 (automatic).\n" "\n" " --window-height value\n" " Set the initial window width.\n" - " Default is -1 (automatic).\n" + " Default is 0 (automatic).\n" "\n" "Shortcuts:\n" "\n" From c2116082ab293772bb2d154707542b4bf0ccedf0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 23:04:31 +0100 Subject: [PATCH 54/60] Remove deprecated options from help and manpage Ref: ff061b4f30c54dedc5073a588c6c697477b805db --- app/scrcpy.1 | 20 ++++++++++---------- app/src/main.c | 16 ++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 948cac4d..6cb062b5 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -15,6 +15,10 @@ provides display and control of Android devices connected on USB (or over TCP/IP .SH OPTIONS +.TP +.B \-\-always\-on\-top +Make scrcpy window always on top (above other windows). + .TP .BI "\-b, \-\-bit\-rate " value Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). @@ -22,7 +26,7 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8000000. .TP -.BI "\-c, \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy +.BI \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any @@ -33,10 +37,6 @@ value is computed on the cropped size. .B \-f, \-\-fullscreen Start in fullscreen. -.TP -.BI "\-F, \-\-record\-format " format -Force recording format (either mp4 or mkv). - .TP .B \-h, \-\-help Print this help. @@ -84,9 +84,13 @@ Record screen to .IR file . The format is determined by the -.B \-F/\-\-record\-format +.B \-\-record\-format option if set, or by the file extension (.mp4 or .mkv). +.TP +.BI \-\-record\-format " format +Force recording format (either mp4 or mkv). + .TP .B \-\-render\-expired\-frames By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency. @@ -105,10 +109,6 @@ Enable "show touches" on start, disable on quit. It only shows physical touches (not clicks from scrcpy). -.TP -.B \-T, \-\-always\-on\-top -Make scrcpy window always on top (above other windows). - .TP .B \-v, \-\-version Print the version of scrcpy. diff --git a/app/src/main.c b/app/src/main.c index 04be1ab0..8a835bf1 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -30,12 +30,15 @@ static void usage(const char *arg0) { "\n" "Options:\n" "\n" + " --always-on-top\n" + " Make scrcpy window always on top (above other windows).\n" + "\n" " -b, --bit-rate value\n" " Encode the video at the given bit-rate, expressed in bits/s.\n" " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Default is %d.\n" "\n" - " -c, --crop width:height:x:y\n" + " --crop width:height:x:y\n" " Crop the device screen on the server.\n" " The values are expressed in the device natural orientation\n" " (typically, portrait for a phone, landscape for a tablet).\n" @@ -44,9 +47,6 @@ static void usage(const char *arg0) { " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" - " -F, --record-format format\n" - " Force recording format (either mp4 or mkv).\n" - "\n" " -h, --help\n" " Print this help.\n" "\n" @@ -85,9 +85,12 @@ static void usage(const char *arg0) { "\n" " -r, --record file.mp4\n" " Record screen to file.\n" - " The format is determined by the -F/--record-format option if\n" + " The format is determined by the --record-format option if\n" " set, or by the file extension (.mp4 or .mkv).\n" "\n" + " --record-format format\n" + " Force recording format (either mp4 or mkv).\n" + "\n" " --render-expired-frames\n" " By default, to minimize latency, scrcpy always renders the\n" " last available decoded frame, and drops any previous ones.\n" @@ -105,9 +108,6 @@ static void usage(const char *arg0) { " Enable \"show touches\" on start, disable on quit.\n" " It only shows physical touches (not clicks from scrcpy).\n" "\n" - " -T, --always-on-top\n" - " Make scrcpy window always on top (above other windows).\n" - "\n" " -v, --version\n" " Print the version of scrcpy.\n" "\n" From cb6b300483db9af6c717b5f9e96551ea9da3853f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 22:37:31 +0100 Subject: [PATCH 55/60] Upgrade FFmpeg (4.2.1) for Windows Include the latest version of FFmpeg in Windows releases. --- Makefile.CrossWindows | 20 ++++++++++---------- cross_win32.txt | 4 ++-- cross_win64.txt | 4 ++-- prebuilt-deps/Makefile | 24 ++++++++++++------------ 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 59f5a302..2b30dcb5 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -100,11 +100,11 @@ dist-win32: build-server build-win32 build-win32-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -115,11 +115,11 @@ dist-win64: build-server build-win64 build-win64-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" diff --git a/cross_win32.txt b/cross_win32.txt index 3ad45c79..d13af0e2 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win32-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev' prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 3f222ba5..09f387e1 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win64-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev' prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 6cfb4100..0c857565 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -10,24 +10,24 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-ffmpeg-shared-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.4-win32-shared.zip \ - 596608277f6b937c3dea7c46e854665d75b3de56790bae07f655ca331440f003 \ - ffmpeg-4.1.4-win32-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \ + 9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \ + ffmpeg-4.2.1-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.4-win32-dev.zip \ - a80c86e263cfad26e202edfa5e6e939a2c88843ae26f031d3e0d981a39fd03fb \ - ffmpeg-4.1.4-win32-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \ + c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \ + ffmpeg-4.2.1-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.4-win64-shared.zip \ - a90889871de2cab8a79b392591313a188189a353f69dde1db98aebe20b280989 \ - ffmpeg-4.1.4-win64-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \ + 55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \ + ffmpeg-4.2.1-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.4-win64-dev.zip \ - 6c9d53f9e94ce1821e975ec668e5b9d6e9deb4a45d0d7e30264685d3dfbbb068 \ - ffmpeg-4.1.4-win64-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \ + 5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \ + ffmpeg-4.2.1-win64-dev prepare-sdl2: @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \ From 4906aff4542bb83d2605d580a817ea76df32bd33 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 22:49:08 +0100 Subject: [PATCH 56/60] Upgrade platform-tools (29.0.5) for Windows Include the latest version of adb in Windows releases. --- prebuilt-deps/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 0c857565..892af6c7 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -35,6 +35,6 @@ prepare-sdl2: SDL2-2.0.10 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.2-windows.zip \ - d78f02e5e2c9c4c1d046dcd4e6fbdf586e5f57ef66eb0da5c2b49d745d85d5ee \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \ + 2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \ platform-tools From 2aa65015bcf09fe3d541f00310ccc69680f89d74 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 22:00:50 +0100 Subject: [PATCH 57/60] Bump version to 1.11 --- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 57b66db6..ba19d7ee 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.10', + version: '1.11', meson_version: '>= 0.37', default_options: 'c_std=c11') diff --git a/server/build.gradle b/server/build.gradle index f1b48a28..0804a8bd 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 11 - versionName "1.10" + versionCode 12 + versionName "1.11" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index b4605aa9..fcd6233e 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=1.10 +SCRCPY_VERSION_NAME=1.11 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} From 40c3c5761397a4e6f89c69729bf524455e07e7f4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 23:39:30 +0100 Subject: [PATCH 58/60] Update links to v1.11 in README and BUILD --- BUILD.md | 6 +++--- README.md | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/BUILD.md b/BUILD.md index d03af618..8801e5fc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -233,10 +233,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.10.jar`][direct-scrcpy-server] - _(SHA-256: cbeb1a4e046f1392c1dc73c3ccffd7f86dec4636b505556ea20929687a119390)_ + - [`scrcpy-server-v1.11`][direct-scrcpy-server] + _(SHA-256: ff3a454012e91d9185cfe8ca7691cea16c43a7dcc08e92fa47ab9f0ea675abd1)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-server-v1.10.jar +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-server-v1.11 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index f957724d..677e7a1c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.10) +# scrcpy (v1.11) This application provides display and control of Android devices connected on USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. @@ -62,13 +62,13 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. For Windows, for simplicity, prebuilt archives with all the dependencies (including `adb`) are available: - - [`scrcpy-win32-v1.10.zip`][direct-win32] - _(SHA-256: f98b400b3764404b33b212e9762dd6f1593ddb766c1480fc2609c94768e4a8e1)_ - - [`scrcpy-win64-v1.10.zip`][direct-win64] - _(SHA-256: 95de34575d873c7e95dfcfb5e74d0f6af4f70b2a5bc6fde0f48d1a05480e3a44)_ + - [`scrcpy-win32-v1.11.zip`][direct-win32] + _(SHA-256: f25ed46e6f3e81e0ff9b9b4df7fe1a4bbd13f8396b7391be0a488b64c675b41e)_ + - [`scrcpy-win64-v1.11.zip`][direct-win64] + _(SHA-256: 3802c9ea0307d437947ff150ec65e53990b0beaacd0c8d0bed19c7650ce141bd)_ -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win32-v1.10.zip -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win64-v1.10.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win32-v1.11.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win64-v1.11.zip You can also [build the app manually][BUILD]. From 8ec077ce1b5ae3c8a5993e062668220e7c684c30 Mon Sep 17 00:00:00 2001 From: seoyeonK <50603274+seoyeonK@users.noreply.github.com> Date: Sat, 16 Nov 2019 16:19:59 +0900 Subject: [PATCH 59/60] Add Korean translation for README and FAQ PR Signed-off-by: Romain Vimont --- FAQ.ko.md | 84 +++++++++ README.ko.md | 498 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 582 insertions(+) create mode 100644 FAQ.ko.md create mode 100644 README.ko.md diff --git a/FAQ.ko.md b/FAQ.ko.md new file mode 100644 index 00000000..6cc1a1d9 --- /dev/null +++ b/FAQ.ko.md @@ -0,0 +1,84 @@ +# 자주하는 질문 (FAQ) + +다음은 자주 제보되는 문제들과 그들의 현황입니다. + + +### Window 운영체제에서, 디바이스가 발견되지 않습니다. + +가장 흔한 제보는 `adb`에 발견되지 않는 디바이스 혹은 권한 관련 문제입니다. +다음 명령어를 호출하여 모든 것들에 이상이 없는지 확인하세요: + + adb devices + +Window는 당신의 디바이스를 감지하기 위해 [drivers]가 필요할 수도 있습니다. + +[drivers]: https://developer.android.com/studio/run/oem-usb.html + + +### 내 디바이스의 미러링만 가능하고, 디바이스와 상호작용을 할 수 없습니다. + +일부 디바이스에서는, [simulating input]을 허용하기 위해서 한가지 옵션을 활성화해야 할 수도 있습니다. +개발자 옵션에서 (developer options) 다음을 활성화 하세요: + +> **USB debugging (Security settings)** +> _권한 부여와 USB 디버깅을 통한 simulating input을 허용한다_ + +[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +### 마우스 클릭이 다른 곳에 적용됩니다. + +Mac 운영체제에서, HiDPI support 와 여러 스크린 창이 있는 경우, 입력 위치가 잘못 파악될 수 있습니다. +[issue 15]를 참고하세요. + +[issue 15]: https://github.com/Genymobile/scrcpy/issues/15 + +차선책은 HiDPI support을 비활성화 하고 build하는 방법입니다: + +```bash +meson x --buildtype release -Dhidpi_support=false +``` + +하지만, 동영상은 낮은 해상도로 재생될 것 입니다. + + +### HiDPI display의 화질이 낮습니다. + +Windows에서는, [scaling behavior] 환경을 설정해야 할 수도 있습니다. + +> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings > +> Override high DPI scaling behavior > Scaling performed by: _Application_. + +[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 + + +### KWin compositor가 실행되지 않습니다 + +Plasma Desktop에서는,_scrcpy_ 가 실행중에는 compositor가 비활성화 됩니다. + +차석책으로는, ["Block compositing"를 비활성화하세요][kwin]. + +[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 + + +###비디오 스트림을 열 수 없는 에러가 발생합니다.(Could not open video stream). + +여러가지 원인이 있을 수 있습니다. 가장 흔한 원인은 디바이스의 하드웨어 인코더(hardware encoder)가 +주어진 해상도를 인코딩할 수 없는 경우입니다. + +``` +ERROR: Exception on thread Thread[main,5,main] +android.media.MediaCodec$CodecException: Error 0xfffffc0e +... +Exit due to uncaughtException in main thread: +ERROR: Could not open video stream +INFO: Initial texture: 1080x2336 +``` + +더 낮은 해상도로 시도 해보세요: + +``` +scrcpy -m 1920 +scrcpy -m 1024 +scrcpy -m 800 +``` diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 00000000..564acae7 --- /dev/null +++ b/README.ko.md @@ -0,0 +1,498 @@ +# scrcpy (v1.11) + +This document will be updated frequently along with the original Readme file +이 문서는 원어 리드미 파일의 업데이트에 따라 종종 업데이트 될 것입니다 + + 이 어플리케이션은 UBS ( 혹은 [TCP/IP][article-tcpip] ) 로 연결된 Android 디바이스를 화면에 보여주고 관리하는 것을 제공합니다. + _GNU/Linux_, _Windows_ 와 _macOS_ 상에서 작동합니다. + (아래 설명에서 디바이스는 안드로이드 핸드폰을 의미합니다.) + +[article-tcpip]:https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ + +![screenshot](https://github.com/Genymobile/scrcpy/blob/master/assets/screenshot-debian-600.jpg?raw=true) + +주요 기능은 다음과 같습니다. + + - **가벼움** (기본적이며 디바이스의 화면만을 보여줌) + - **뛰어난 성능** (30~60fps) + - **높은 품질** (1920×1080 이상의 해상도) + - **빠른 반응 속도** ([35~70ms][lowlatency]) + - **짧은 부팅 시간** (첫 사진을 보여주는데 최대 1초 소요됨) + - **장치 설치와는 무관함** (디바이스에 설치하지 않아도 됨) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## 요구사항 + +안드로이드 장치는 최소 API 21 (Android 5.0) 을 필요로 합니다. + +디바이스에 [adb debugging][enable-adb]이 가능한지 확인하십시오. + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +어떤 디바이스에서는, 키보드와 마우스를 사용하기 위해서 [추가 옵션][control] 이 필요하기도 합니다. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## 앱 설치하기 + + +### Linux (리눅스) + +리눅스 상에서는 보통 [어플을 직접 설치][BUILD] 해야합니다. 어렵지 않으므로 걱정하지 않아도 됩니다. + +[BUILD]:https://github.com/Genymobile/scrcpy/blob/master/BUILD.md + +[Snap] 패키지가 가능합니다 : [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Arch Linux에서, [AUR] 패키지가 가능합니다 : [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Gentoo에서 ,[Ebuild] 가 가능합니다 : [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + + +### Windows (윈도우) + +윈도우 상에서, 간단하게 설치하기 위해 종속성이 있는 사전 구축된 아카이브가 제공됩니다 (`adb` 포함) : +해당 파일은 Readme원본 링크를 통해서 다운로드가 가능합니다. + - [`scrcpy-win`][direct-win] + +[direct-win]: https://github.com/Genymobile/scrcpy/blob/master/README.md#windows + + +[어플을 직접 설치][BUILD] 할 수도 있습니다. + + +### macOS (맥 OS) + +이 어플리케이션은 아래 사항을 따라 설치한다면 [Homebrew] 에서도 사용 가능합니다 : + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +`PATH` 로부터 접근 가능한 `adb` 가 필요합니다. 아직 설치하지 않았다면 다음을 따라 설치해야 합니다 : + +```bash +brew cask install android-platform-tools +``` + +[어플을 직접 설치][BUILD] 할 수도 있습니다. + + +## 실행 + +안드로이드 디바이스를 연결하고 실행하십시오: + +```bash +scrcpy +``` + +다음과 같이 명령창 옵션 기능도 제공합니다. + +```bash +scrcpy --help +``` + +## 기능 + +### 캡쳐 환경 설정 + + +###사이즈 재정의 + +가끔씩 성능을 향상시키기위해 안드로이드 디바이스를 낮은 해상도에서 미러링하는 것이 유용할 때도 있습니다. + +너비와 높이를 제한하기 위해 특정 값으로 지정할 수 있습니다 (e.g. 1024) : + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # 축약 버전 +``` + +이 외의 크기도 디바이스의 가로 세로 비율이 유지된 상태에서 계산됩니다. +이러한 방식으로 디바이스 상에서 1920×1080 는 모니터 상에서1024×576로 미러링될 것 입니다. + + +### bit-rate 변경 + +기본 bit-rate 는 8 Mbps입니다. 비디오 bit-rate 를 변경하기 위해선 다음과 같이 입력하십시오 (e.g. 2 Mbps로 변경): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # 축약 버전 +``` + +###프레임 비율 제한 + +안드로이드 버전 10이상의 디바이스에서는, 다음의 명령어로 캡쳐 화면의 프레임 비율을 제한할 수 있습니다: + +```bash +scrcpy --max-fps 15 +``` + + +### Crop (잘라내기) + +디바이스 화면은 화면의 일부만 미러링하기 위해 잘라질 것입니다. + +예를 들어, *Oculus Go* 의 한 쪽 눈만 미러링할 때 유용합니다 : + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) +scrcpy -c 1224:1440:0:0 # 축약 버전 +``` + +만약 `--max-size` 도 지정하는 경우, 잘라낸 다음에 재정의된 크기가 적용될 것입니다. + + +### 화면 녹화 + +미러링하는 동안 화면 녹화를 할 수 있습니다 : + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +녹화하는 동안 미러링을 멈출 수 있습니다 : + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# Ctrl+C 로 녹화를 중단할 수 있습니다. +# 윈도우 상에서 Ctrl+C 는 정상정으로 종료되지 않을 수 있으므로, 디바이스 연결을 해제하십시오. +``` + +"skipped frames" 은 모니터 화면에 보여지지 않았지만 녹화되었습니다 ( 성능 문제로 인해 ). 프레임은 디바이스 상에서 _타임 스탬프 ( 어느 시점에 데이터가 존재했다는 사실을 증명하기 위해 특정 위치에 시각을 표시 )_ 되었으므로, [packet delay +variation] 은 녹화된 파일에 영향을 끼치지 않습니다. + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + +## 연결 + +### 무선연결 + +_Scrcpy_ 장치와 정보를 주고받기 위해 `adb` 를 사용합니다. `adb` 는 TCIP/IP 를 통해 디바이스와 [연결][connect] 할 수 있습니다 : + +1. 컴퓨터와 디바이스를 동일한 Wi-Fi 에 연결합니다. +2. 디바이스의 IP address 를 확인합니다 (설정 → 내 기기 → 상태 / 혹은 인터넷에 '내 IP'검색 시 확인 가능합니다. ). +3. TCP/IP 를 통해 디바이스에서 adb 를 사용할 수 있게 합니다: `adb tcpip 5555`. +4. 디바이스 연결을 해제합니다. +5. adb 를 통해 디바이스에 연결을 합니다\: `adb connect DEVICE_IP:5555` _(`DEVICE_IP` 대신)_. +6. `scrcpy` 실행합니다. + +다음은 bit-rate 와 해상도를 줄이는데 유용합니다 : + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # 축약 버전 +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + + +### 여러 디바이스 사용 가능 + +만약에 여러 디바이스들이 `adb devices` 목록에 표시되었다면, _serial_ 을 명시해야합니다: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # 축약 버전 +``` + +_scrcpy_ 로 여러 디바이스를 연결해 사용할 수 있습니다. + + +#### SSH tunnel + +떨어져 있는 디바이스와 연결하기 위해서는, 로컬 `adb` client와 떨어져 있는 `adb` 서버를 연결해야 합니다. (디바이스와 클라이언트가 동일한 버전의 _adb_ protocol을 사용할 경우에 제공됩니다.): + +```bash +adb kill-server # 5037의 로컬 local adb server를 중단 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# 실행 유지 +``` + +다른 터미널에서는 : + +```bash +scrcpy +``` + +무선 연결과 동일하게, 화질을 줄이는 것이 나을 수 있습니다: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +## Window에서의 배치 + +### 맞춤형 window 제목 + +기본적으로, window의 이름은 디바이스의 모델명 입니다. +다음의 명령어를 통해 변경하세요. + +```bash +scrcpy --window-title 'My device' +``` + + +### 배치와 크기 + +초기 window창의 배치와 크기는 다음과 같이 설정할 수 있습니다: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + + +### 경계 없애기 + +윈도우 장식(경계선 등)을 다음과 같이 제거할 수 있습니다: + +```bash +scrcpy --window-borderless +``` + +### 항상 모든 윈도우 위에 실행창 고정 + +이 어플리케이션의 윈도우 창은 다음의 명령어로 다른 window 위에 디스플레이 할 수 있습니다: + +```bash +scrcpy --always-on-top +scrcpy -T # 축약 버전 +``` + +### 전체 화면 + +이 어플리케이션은 전체화면으로 바로 시작할 수 있습니다. + +```bash +scrcpy --fullscreen +scrcpy -f # short version +``` + +전체 화면은 `Ctrl`+`f`키로 끄거나 켤 수 있습니다. + + +## 다른 미러링 옵션 + +### 읽기 전용(Read-only) + +권한을 제한하기 위해서는 (디바이스와 관련된 모든 것: 입력 키, 마우스 이벤트 , 파일의 드래그 앤 드랍(drag&drop)): + +```bash +scrcpy --no-control +scrcpy -n +``` + +### 화면 끄기 + +미러링을 실행하는 와중에 디바이스의 화면을 끌 수 있게 하기 위해서는 +다음의 커맨드 라인 옵션을(command line option) 입력하세요: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +혹은 `Ctrl`+`o`을 눌러 언제든지 디바이스의 화면을 끌 수 있습니다. + +다시 화면을 켜기 위해서는`POWER` (혹은 `Ctrl`+`p`)를 누르세요. + + +### 유효기간이 지난 프레임 제공 (Render expired frames) + +디폴트로, 대기시간을 최소화하기 위해 _scrcpy_ 는 항상 마지막으로 디코딩된 프레임을 제공합니다 +과거의 프레임은 하나씩 삭제합니다. + +모든 프레임을 강제로 렌더링하기 위해서는 (대기 시간이 증가될 수 있습니다) +다음의 명령어를 사용하세요: + +```bash +scrcpy --render-expired-frames +``` + + +### 화면에 터치 나타내기 + +발표를 할 때, 물리적인 기기에 한 물리적 터치를 나타내는 것이 유용할 수 있습니다. + +안드로이드 운영체제는 이런 기능을 _Developers options_에서 제공합니다. + +_Scrcpy_ 는 이런 기능을 시작할 때와 종료할 때 옵션으로 제공합니다. + +```bash +scrcpy --show-touches +scrcpy -t +``` + +화면에 _물리적인 터치만_ 나타나는 것에 유의하세요 (손가락을 디바이스에 대는 행위). + + +### 입력 제어 + +#### 복사-붙여넣기 + +컴퓨터와 디바이스 양방향으로 클립보드를 복사하는 것이 가능합니다: + + - `Ctrl`+`c` 디바이스의 클립보드를 컴퓨터로 복사합니다; + - `Ctrl`+`Shift`+`v` 컴퓨터의 클립보드를 디바이스로 복사합니다; + - `Ctrl`+`v` 컴퓨터의 클립보드를 text event 로써 _붙여넣습니다_ ( 그러나, ASCII 코드가 아닌 경우 실행되지 않습니다 ) + +#### 텍스트 삽입 우선 순위 + +텍스트를 입력할 때 생성되는 두 가지의 [events][textevents] 가 있습니다: + - _key events_, 키가 눌려있는 지에 대한 신호; + - _text events_, 텍스트가 입력되었는지에 대한 신호. + +기본적으로, 글자들은 key event 를 이용해 입력되기 때문에, 키보드는 게임에서처럼 처리합니다 ( 보통 WASD 키에 대해서 ). + +그러나 이는 [issues 를 발생][prefertext]시킵니다. 이와 관련된 문제를 접할 경우, 아래와 같이 피할 수 있습니다: + +```bash +scrcpy --prefer-text +``` + +( 그러나 이는 게임에서의 처리를 중단할 수 있습니다 ) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +### 파일 드랍 + +### APK 실행하기 + +APK를 실행하기 위해서는, APK file(파일명이`.apk`로 끝나는 파일)을 드래그하고 _scrcpy_ window에 드랍하세요 (drag and drop) + +시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다. + +### 디바이스에 파일 push하기 + +디바이스의`/sdcard/`에 파일을 push하기 위해서는, +APK파일이 아닌 파일을_scrcpy_ window에 드래그하고 드랍하세요.(drag and drop). + +시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다. + +해당 디렉토리는 시작할 때 변경이 가능합니다: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + +### 오디오의 전달 + +_scrcpy_는 오디오를 직접 전달해주지 않습니다. [USBaudio] (Linux-only)를 사용하세요. + +추가적으로 [issue #14]를 참고하세요. + +[USBaudio]: https://github.com/rom1v/usbaudio +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + +## 단축키 + + | 실행내용 | 단축키 | 단축키 (macOS) + | -------------------------------------- |:----------------------------- |:----------------------------- + | 전체화면 모드로 전환 | `Ctrl`+`f` | `Cmd`+`f` + | window를 1:1비율로 전환하기(픽셀 맞춤) | `Ctrl`+`g` | `Cmd`+`g` + | 검은 공백 제거 위한 window 크기 조정 | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ + |`HOME` 클릭 | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ + | `BACK` 클릭 | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ + | `APP_SWITCH` 클릭 | `Ctrl`+`s` | `Cmd`+`s` + | `MENU` 클릭 | `Ctrl`+`m` | `Ctrl`+`m` + | `VOLUME_UP` 클릭 | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ + | `VOLUME_DOWN` 클릭 | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ + | `POWER` 클릭 | `Ctrl`+`p` | `Cmd`+`p` + | 전원 켜기 | _Right-click²_ | _Right-click²_ + | 미러링 중 디바이스 화면 끄기 | `Ctrl`+`o` | `Cmd`+`o` + | 알림 패널 늘리기 | `Ctrl`+`n` | `Cmd`+`n` + | 알림 패널 닫기 | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` + | 디바이스의 clipboard 컴퓨터로 복사하기 | `Ctrl`+`c` | `Cmd`+`c` + | 컴퓨터의 clipboard 디바이스에 붙여넣기 | `Ctrl`+`v` | `Cmd`+`v` + | Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` + | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i` + +_¹검은 공백을 제거하기 위해서는 그 부분을 더블 클릭하세요_ +_²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상태에서는 뒤로 돌아갑니다. + +## 맞춤 경로 (custom path) + +특정한 _adb_ binary를 사용하기 위해서는, 그것의 경로를 환경변수로 설정하세요. +`ADB`: + + ADB=/path/to/adb scrcpy + +`scrcpy-server.jar`파일의 경로에 오버라이드 하기 위해서는, 그것의 경로를 `SCRCPY_SERVER_PATH`에 저장하세요. + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## _scrcpy_ 인 이유? + +한 동료가 [gnirehtet]와 같이 발음하기 어려운 이름을 찾을 수 있는지 도발했습니다. + +[`strcpy`] 는 **str**ing을 copy하고; `scrcpy`는 **scr**een을 copy합니다. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + + +## 빌드하는 방법? + +[BUILD]을 참고하세요. + +[BUILD]: BUILD.md + +## 흔한 issue + +[FAQ](FAQ.md)을 참고하세요. + + +## 개발자들 + +[developers page]를 참고하세요. + +[developers page]: DEVELOP.md + + +## 라이선스 + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2019 Romain Vimont + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## 관련 글 (articles) + +- [scrcpy 소개][article-intro] +- [무선으로 연결하는 Scrcpy][article-tcpip] + +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ From 8bc056b9c68eea7bccfa5d9123a70443fb1d75b2 Mon Sep 17 00:00:00 2001 From: yangfl Date: Wed, 27 Nov 2019 17:44:14 +0800 Subject: [PATCH 60/60] Fix manpage format --- app/scrcpy.1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 6cb062b5..c77fd985 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -26,7 +26,7 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8000000. .TP -.BI \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy +.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any @@ -42,7 +42,7 @@ Start in fullscreen. Print this help. .TP -.BI \-\-max\-fps " value +.BI "\-\-max\-fps " value Limit the framerate of screen capture (only supported on devices with Android >= 10). .TP @@ -88,7 +88,7 @@ The format is determined by the option if set, or by the file extension (.mp4 or .mkv). .TP -.BI \-\-record\-format " format +.BI "\-\-record\-format " format Force recording format (either mp4 or mkv). .TP @@ -118,29 +118,29 @@ Print the version of scrcpy. Disable window decorations (display borderless window). .TP -.BI \-\-window\-title " text +.BI "\-\-window\-title " text Set a custom window title. .TP -.BI \-\-window\-x " value +.BI "\-\-window\-x " value Set the initial window horizontal position. Default is -1 (automatic).\n .TP -.BI \-\-window\-y " value +.BI "\-\-window\-y " value Set the initial window vertical position. Default is -1 (automatic).\n .TP -.BI \-\-window\-width " value +.BI "\-\-window\-width " value Set the initial window width. Default is 0 (automatic).\n .TP -.BI \-\-window\-height " value +.BI "\-\-window\-height " value Set the initial window height. Default is 0 (automatic).\n