feat: sync scrcpy server

This commit is contained in:
rankun 2020-01-14 12:23:05 +08:00
parent 2c2ad2ab07
commit a26620fb8d
40 changed files with 818 additions and 456 deletions

2
.gitignore vendored
View file

@ -9,5 +9,5 @@
/server/gradlew
/server/gradlew.bat
/server/local.properties
build
build-*
*.DS_Store

View file

@ -58,8 +58,8 @@ INCLUDEPATH += \
# 统一版本号入口,只修改这一个地方即可
VERSION_MAJOR = 1
VERSION_MINOR = 0
VERSION_PATCH = 4
VERSION_MINOR = 12
VERSION_PATCH = 1
# qmake变量的方式定义版本号
VERSION = $${VERSION_MAJOR}.$${VERSION_MINOR}.$${VERSION_PATCH}

View file

@ -46,8 +46,8 @@ void Controller::postControlMsg(ControlMsg *controlMsg)
void Controller::test(QRect rc)
{
ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_MOUSE);
controlMsg->setInjectMouseMsgData(AMOTION_EVENT_ACTION_DOWN, AMOTION_EVENT_BUTTON_PRIMARY, rc);
ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_TOUCH);
controlMsg->setInjectTouchMsgData(POINTER_ID_MOUSE, AMOTION_EVENT_ACTION_DOWN, AMOTION_EVENT_BUTTON_PRIMARY, rc, 1.0f);
postControlMsg(controlMsg);
}

View file

@ -42,18 +42,13 @@ void ControlMsg::setInjectTextMsgData(QString& text)
m_data.injectText.text[tmp.length()] = '\0';
}
void ControlMsg::setInjectMouseMsgData(AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position)
void ControlMsg::setInjectTouchMsgData(quint64 id, AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position, float pressure)
{
m_data.injectMouse.action = action;
m_data.injectMouse.buttons = buttons;
m_data.injectMouse.position = position;
}
void ControlMsg::setInjectTouchMsgData(quint32 id, AndroidMotioneventAction action, QRect position)
{
m_data.injectTouch.action = action;
m_data.injectTouch.id = id;
m_data.injectTouch.action = action;
m_data.injectTouch.buttons = buttons;
m_data.injectTouch.position = position;
m_data.injectTouch.pressure = pressure;
}
void ControlMsg::setInjectScrollMsgData(QRect position, qint32 hScroll, qint32 vScroll)
@ -85,12 +80,22 @@ void ControlMsg::setSetScreenPowerModeData(ControlMsg::ScreenPowerMode mode)
void ControlMsg::writePosition(QBuffer &buffer, const QRect& value)
{
BufferUtil::write16(buffer, value.left());
BufferUtil::write16(buffer, value.top());
BufferUtil::write32(buffer, value.left());
BufferUtil::write32(buffer, value.top());
BufferUtil::write16(buffer, value.width());
BufferUtil::write16(buffer, value.height());
}
quint16 ControlMsg::toFixedPoint16(float f)
{
assert(f >= 0.0f && f <= 1.0f);
quint32 u = f * 0x1p16f; // 2^16
if (u >= 0xffff) {
u = 0xffff;
}
return (quint16) u;
}
QByteArray ControlMsg::serializeData()
{
QByteArray byteArray;
@ -108,15 +113,15 @@ QByteArray ControlMsg::serializeData()
BufferUtil::write16(buffer, strlen(m_data.injectText.text));
buffer.write(m_data.injectText.text, strlen(m_data.injectText.text));
break;
case CMT_INJECT_MOUSE:
buffer.putChar(m_data.injectMouse.action);
BufferUtil::write32(buffer, m_data.injectMouse.buttons);
writePosition(buffer, m_data.injectMouse.position);
break;
case CMT_INJECT_TOUCH:
buffer.putChar(m_data.injectTouch.id);
{
buffer.putChar(m_data.injectTouch.action);
BufferUtil::write64(buffer, m_data.injectTouch.id);
writePosition(buffer, m_data.injectTouch.position);
quint16 pressure = toFixedPoint16(m_data.injectTouch.pressure);
BufferUtil::write16(buffer, pressure);
BufferUtil::write32(buffer, m_data.injectTouch.buttons);
}
break;
case CMT_INJECT_SCROLL:
writePosition(buffer, m_data.injectScroll.position);

View file

@ -11,6 +11,7 @@
#define CONTROL_MSG_TEXT_MAX_LENGTH 300
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093
#define POINTER_ID_MOUSE UINT64_C(-1)
// ControlMsg
class ControlMsg : public QScrcpyEvent
{
@ -19,16 +20,14 @@ public:
CMT_NULL = -1,
CMT_INJECT_KEYCODE = 0,
CMT_INJECT_TEXT,
CMT_INJECT_MOUSE,
CMT_INJECT_TOUCH,
CMT_INJECT_SCROLL,
CMT_BACK_OR_SCREEN_ON,
CMT_EXPAND_NOTIFICATION_PANEL,
CMT_COLLAPSE_NOTIFICATION_PANEL,
CMT_GET_CLIPBOARD,
CMT_SET_CLIPBOARD,
CMT_SET_SCREEN_POWER_MODE,
CMT_INJECT_TOUCH,
CMT_SET_SCREEN_POWER_MODE
};
enum ScreenPowerMode {
@ -42,11 +41,10 @@ public:
void setInjectKeycodeMsgData(AndroidKeyeventAction action, AndroidKeycode keycode, AndroidMetastate metastate);
void setInjectTextMsgData(QString& text);
void setInjectMouseMsgData(AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position);
// id 代表一个触摸点最多支持10个触摸点[0,9]
// action 只能是AMOTION_EVENT_ACTION_DOWNAMOTION_EVENT_ACTION_UPAMOTION_EVENT_ACTION_MOVE
// position action动作对应的位置
void setInjectTouchMsgData(quint32 id, AndroidMotioneventAction action, QRect position);
void setInjectTouchMsgData(quint64 id, AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position, float pressure);
void setInjectScrollMsgData(QRect position, qint32 hScroll, qint32 vScroll);
void setSetClipboardMsgData(QString& text);
void setSetScreenPowerModeData(ControlMsg::ScreenPowerMode mode);
@ -55,6 +53,7 @@ public:
private:
void writePosition(QBuffer& buffer, const QRect& value);
quint16 toFixedPoint16(float f);
private:
struct ControlMsgData {
@ -69,14 +68,11 @@ private:
char* text = Q_NULLPTR;
} injectText;
struct {
quint64 id;
AndroidMotioneventAction action;
AndroidMotioneventButtons buttons;
QRect position;
} injectMouse;
struct {
quint32 id;
AndroidMotioneventAction action;
QRect position;
float pressure;
} injectTouch;
struct {
QRect position;

View file

@ -137,7 +137,7 @@ void InputConvertGame::sendTouchEvent(int id, QPointF pos, AndroidMotioneventAct
if (!controlMsg) {
return;
}
controlMsg->setInjectTouchMsgData(id, action, QRect(calcFrameAbsolutePos(pos).toPoint(), m_frameSize));
controlMsg->setInjectTouchMsgData(id, action, (AndroidMotioneventButtons)0, QRect(calcFrameAbsolutePos(pos).toPoint(), m_frameSize), 1.0f);
sendControlMsg(controlMsg);
}

View file

@ -40,11 +40,11 @@ void InputConvertNormal::mouseEvent(const QMouseEvent* from, const QSize& frameS
pos.setY(pos.y() * frameSize.height() / showSize.height());
// set data
ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_MOUSE);
ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_TOUCH);
if (!controlMsg) {
return;
}
controlMsg->setInjectMouseMsgData(action, convertMouseButtons(from->buttons()), QRect(pos.toPoint(), frameSize));
controlMsg->setInjectTouchMsgData(POINTER_ID_MOUSE, action, convertMouseButtons(from->buttons()), QRect(pos.toPoint(), frameSize), 1.0f);
sendControlMsg(controlMsg);
}

View file

@ -203,6 +203,7 @@ void Device::startServer()
params.localPort = m_params.localPort;
params.maxSize = m_params.maxSize;
params.bitRate = m_params.bitRate;
params.maxFps = m_params.maxFps;
params.crop = "-";
params.sendFrameMeta = m_recorder ? true : false;
params.control = true;

View file

@ -22,6 +22,7 @@ public:
quint16 localPort = 27183; // reverse时本地监听端口
quint16 maxSize = 720; // 视频分辨率
quint32 bitRate = 8000000; // 视频比特率
quint32 maxFps = 60; // 视频最大帧率
bool closeScreen = false; // 启动时自动息屏
bool useReverse = true; // true:先使用adb reverse失败后自动使用adb forwardfalse:直接使用adb forward
bool display = true; // 是否显示画面(或者仅仅后台录制)

View file

@ -9,7 +9,7 @@
#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
#define DEVICE_NAME_FIELD_LENGTH 64
#define SOCKET_NAME "qtscrcpy"
#define SOCKET_NAME "scrcpy"
#define MAX_CONNECT_COUNT 30
#define MAX_RESTART_COUNT 1
@ -129,8 +129,13 @@ bool Server::execute()
args << "app_process";
args << "/"; // unused;
args << "com.genymobile.scrcpy.Server";
// version
QStringList versionList = QCoreApplication::applicationVersion().split(".");
QString version = versionList[0] + "." + versionList[1] + "." + versionList[2];
args << version;
args << QString::number(m_params.maxSize);
args << QString::number(m_params.bitRate);
args << QString::number(m_params.maxFps);
args << (m_tunnelForward ? "true" : "false");
if (m_params.crop.isEmpty()) {
args << "-";

View file

@ -27,6 +27,7 @@ public:
quint16 localPort = 27183; // reverse时本地监听端口
quint16 maxSize = 720; // 视频分辨率
quint32 bitRate = 8000000; // 视频比特率
quint32 maxFps = 60; // 视频最大帧率
QString crop = "-"; // 视频裁剪
bool sendFrameMeta = false; // 是否发送mp4帧数据
bool control = true; // 安卓端是否接收键鼠控制

View file

@ -149,6 +149,8 @@ void Dialog::on_startServerBtn_clicked()
params.serial = ui->serialBox->currentText().trimmed();
params.maxSize = videoSize;
params.bitRate = bitRate;
// on devices with Android >= 10, the capture frame rate can be limited
params.maxFps = 60;
params.recordFileName = absFilePath;
params.closeScreen = ui->closeScreenCheck->isChecked();
params.useReverse = ui->useReverseCheck->isChecked();

View file

@ -8,6 +8,12 @@ void BufferUtil::write32(QBuffer &buffer, quint32 value)
buffer.putChar(value);
}
void BufferUtil::write64(QBuffer &buffer, quint64 value)
{
write32(buffer, value >> 32);
write32(buffer, (quint32) value);
}
void BufferUtil::write16(QBuffer &buffer, quint32 value)
{
buffer.putChar(value >> 8);

View file

@ -4,9 +4,10 @@
class BufferUtil
{
public:
static void write32(QBuffer& buffer, quint32 value);
public:
static void write16(QBuffer& buffer, quint32 value);
static void write32(QBuffer& buffer, quint32 value);
static void write64(QBuffer& buffer, quint64 value);
static quint16 read16(QBuffer& buffer);
static quint32 read32(QBuffer& buffer);
static quint64 read64(QBuffer& buffer);

View file

@ -185,7 +185,7 @@ Mac OS平台你可以直接使用我编译好的可执行程序:
### Android端 没有修改需求的话直接使用自带的scrcpy-server.jar即可
1. 目标平台上搭建Android开发环境
2. 使用Android Studio打开项目根目录中的server项目
3. 第一次打开如果你没有对应版本的gradle会提示找不到gradle是否升级gradle并创建选择取消取消后会弹出gradle选择已有gradle的位置同样取消即可会自动下载
3. 第一次打开如果你没有对应版本的gradle会提示找不到gradle是否升级gradle并创建选择取消取消后会弹出选择已有gradle的位置同样取消即可会自动下载
4. 按需编辑代码即可,当然也可以不编辑
4. 编译出apk以后改名为scrcpy-server.jar并替换third_party/scrcpy-server.jar即可

View file

@ -28,7 +28,7 @@ android {
minSdkVersion 21
targetSdkVersion 29
versionCode 5
versionName "1.4"
versionName "1.12.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View file

@ -1,23 +1,21 @@
package com.genymobile.scrcpy;
/**
* Union of all supported msg types, identified by their {@code type}.
* Union of all supported event types, identified by their {@code type}.
*/
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 = 2;
public static final int TYPE_INJECT_SCROLL = 3;
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;
public static final int TYPE_INJECT_TOUCH = 10;
public static final int TYPE_ROTATE_DEVICE = 10;
private int type;
private String text;
@ -25,7 +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 int id;
private long pointerId;
private float pressure;
private Position position;
private int hScroll;
private int vScroll;
@ -34,69 +33,62 @@ public final class ControlMessage {
}
public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_KEYCODE;
event.action = action;
event.keycode = keycode;
event.metaState = metaState;
return event;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_KEYCODE;
msg.action = action;
msg.keycode = keycode;
msg.metaState = metaState;
return msg;
}
public static ControlMessage createInjectText(String text) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_TEXT;
event.text = text;
return event;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TEXT;
msg.text = text;
return msg;
}
public static ControlMessage createInjectMotion(int action, int buttons, Position position) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_MOUSE;
event.action = action;
event.buttons = buttons;
event.position = position;
return event;
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;
}
public static ControlMessage createInjectMotionTouch(int id, int action, Position position) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_TOUCH;
event.action = action;
event.id = id;
event.position = position;
return event;
}
public static ControlMessage createInjectScroll(Position position, int hScroll, int vScroll) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_SCROLL;
event.position = position;
event.hScroll = hScroll;
event.vScroll = vScroll;
return event;
public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_SCROLL_EVENT;
msg.position = position;
msg.hScroll = hScroll;
msg.vScroll = vScroll;
return msg;
}
public static ControlMessage createSetClipboard(String text) {
ControlMessage event = new ControlMessage();
event.type = TYPE_SET_CLIPBOARD;
event.text = text;
return event;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD;
msg.text = text;
return msg;
}
/**
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
*/
public static ControlMessage createSetScreenPowerMode(int mode) {
ControlMessage event = new ControlMessage();
event.type = TYPE_SET_SCREEN_POWER_MODE;
event.action = mode;
return event;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_SCREEN_POWER_MODE;
msg.action = mode;
return msg;
}
public static ControlMessage createEmpty(int type) {
ControlMessage event = new ControlMessage();
event.type = type;
return event;
ControlMessage msg = new ControlMessage();
msg.type = type;
return msg;
}
public int getType() {
@ -123,8 +115,12 @@ public final class ControlMessage {
return buttons;
}
public int getId() {
return id;
public long getPointerId() {
return pointerId;
}
public float getPressure() {
return pressure;
}
public Position getPosition() {

View file

@ -9,9 +9,9 @@ import java.nio.charset.StandardCharsets;
public class ControlMessageReader {
private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
private static final int INJECT_MOUSE_PAYLOAD_LENGTH = 13;
private static final int INJECT_SCROLL_PAYLOAD_LENGTH = 16;
private static final int INJECT_TOUCH_PAYLOAD_LENGTH = 10;
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;
public static final int TEXT_MAX_LENGTH = 300;
@ -50,6 +50,7 @@ public class ControlMessageReader {
return null;
}
int savedPosition = buffer.position();
int type = buffer.get();
ControlMessage msg;
switch (type) {
@ -59,14 +60,11 @@ public class ControlMessageReader {
case ControlMessage.TYPE_INJECT_TEXT:
msg = parseInjectText();
break;
case ControlMessage.TYPE_INJECT_MOUSE:
msg = parseInjectMouse();
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
msg = parseInjectTouchEvent();
break;
case ControlMessage.TYPE_INJECT_TOUCH:
msg = parseInjectMouseTouch();
break;
case ControlMessage.TYPE_INJECT_SCROLL:
msg = parseInjectScroll();
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
msg = parseInjectScrollEvent();
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
msg = parseSetClipboard();
@ -78,6 +76,7 @@ public class ControlMessageReader {
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
case ControlMessage.TYPE_GET_CLIPBOARD:
case ControlMessage.TYPE_ROTATE_DEVICE:
msg = ControlMessage.createEmpty(type);
break;
default:
@ -123,34 +122,30 @@ public class ControlMessageReader {
return ControlMessage.createInjectText(text);
}
private ControlMessage parseInjectMouse() {
if (buffer.remaining() < INJECT_MOUSE_PAYLOAD_LENGTH) {
@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);
int buttons = buffer.getInt();
Position position = readPosition(buffer);
return ControlMessage.createInjectMotion(action, buttons, position);
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
}
private ControlMessage parseInjectMouseTouch() {
if (buffer.remaining() < INJECT_TOUCH_PAYLOAD_LENGTH) {
return null;
}
int id = toUnsigned(buffer.get());
int action = toUnsigned(buffer.get());
Position position = readPosition(buffer);
return ControlMessage.createInjectMotionTouch(id, action, position);
}
private ControlMessage parseInjectScroll() {
if (buffer.remaining() < INJECT_SCROLL_PAYLOAD_LENGTH) {
private ControlMessage parseInjectScrollEvent() {
if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) {
return null;
}
Position position = readPosition(buffer);
int hScroll = buffer.getInt();
int vScroll = buffer.getInt();
return ControlMessage.createInjectScroll(position, hScroll, vScroll);
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
}
private ControlMessage parseSetClipboard() {
@ -170,8 +165,8 @@ public class ControlMessageReader {
}
private static Position readPosition(ByteBuffer buffer) {
int x = toUnsigned(buffer.getShort());
int y = toUnsigned(buffer.getShort());
int x = buffer.getInt();
int y = buffer.getInt();
int screenWidth = toUnsigned(buffer.getShort());
int screenHeight = toUnsigned(buffer.getShort());
return new Position(x, y, screenWidth, screenHeight);

View file

@ -2,7 +2,6 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager;
import android.graphics.Point;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.InputEvent;
@ -11,95 +10,41 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import java.io.IOException;
import java.util.Vector;
public class Controller {
private static final int DEVICE_ID_VIRTUAL = -1;
private final Device device;
private final DesktopConnection connection;
private final DeviceMessageSender sender;
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
private long lastMouseDown;
private Vector<MotionEvent.PointerProperties> pointerProperties = new Vector<MotionEvent.PointerProperties>();
private Vector<MotionEvent.PointerCoords> pointerCoords = new Vector<MotionEvent.PointerCoords>();
private long lastTouchDown;
private final PointersState pointersState = new PointersState();
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
public Controller(Device device, DesktopConnection connection) {
this.device = device;
this.connection = connection;
initPointers();
sender = new DeviceMessageSender(connection);
}
private int getPointer(int id) {
for (int i = 0; i < pointerProperties.size(); i++) {
if (id == pointerProperties.get(i).id) {
return i;
}
private void initPointers() {
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;
pointerProperties[i] = props;
pointerCoords[i] = coords;
}
MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
props.id = id;
props.toolType = MotionEvent.TOOL_TYPE_FINGER;
pointerProperties.addElement(props);
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
coords.orientation = 0;
coords.pressure = 1;
coords.size = 1;
pointerCoords.addElement(coords);
return pointerProperties.size() - 1;
}
private void releasePointer(int id) {
int index = -1;
for (int i = 0; i < pointerProperties.size(); i++) {
if (id == pointerProperties.get(i).id) {
index = i;
break;
}
}
if ( -1 != index) {
pointerProperties.remove(index);
pointerCoords.remove(index);
}
}
private void setPointerCoords(int id, Point point) {
int index = -1;
for (int i = 0; i < pointerProperties.size(); i++) {
if (id == pointerProperties.get(i).id) {
index = i;
break;
}
}
if ( -1 != index) {
MotionEvent.PointerCoords coords = pointerCoords.get(index);
coords.x = point.x;
coords.y = point.y;
}
}
private void setScroll(int id, int hScroll, int vScroll) {
int index = -1;
for (int i = 0; i < pointerProperties.size(); i++) {
if (id == pointerProperties.get(i).id) {
index = i;
break;
}
}
if ( -1 != index) {
MotionEvent.PointerCoords coords = pointerCoords.get(index);
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
}
}
public DeviceMessageSender getSender() {
return sender;
}
@SuppressWarnings("checkstyle:MagicNumber")
@ -123,6 +68,10 @@ public class Controller {
}
}
public DeviceMessageSender getSender() {
return sender;
}
private void handleEvent() throws IOException {
ControlMessage msg = connection.receiveControlMessage();
switch (msg.getType()) {
@ -132,13 +81,10 @@ public class Controller {
case ControlMessage.TYPE_INJECT_TEXT:
injectText(msg.getText());
break;
case ControlMessage.TYPE_INJECT_MOUSE:
injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition());
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
break;
case ControlMessage.TYPE_INJECT_TOUCH:
injectTouch(msg.getId(), msg.getAction(), msg.getPosition());
break;
case ControlMessage.TYPE_INJECT_SCROLL:
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
@ -160,6 +106,9 @@ public class Controller {
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
device.setScreenPowerMode(msg.getAction());
break;
case ControlMessage.TYPE_ROTATE_DEVICE:
device.rotateDevice();
break;
default:
// do nothing
}
@ -196,87 +145,43 @@ public class Controller {
return successCount;
}
private boolean injectTouch(int id, int action, Position position) {
if (action != MotionEvent.ACTION_DOWN
&& action != MotionEvent.ACTION_UP
&& action != MotionEvent.ACTION_MOVE) {
Ln.w("Unsupported action: " + action);
return false;
}
if (id < 0 || id > 9) {
Ln.w("Unsupported id[0-9]: " + id);
return false;
}
int index = getPointer(id);
int convertAction = action;
switch (action) {
case MotionEvent.ACTION_DOWN:
if (1 != pointerProperties.size()) {
convertAction = (index << 8) | MotionEvent.ACTION_POINTER_DOWN;
}
break;
case MotionEvent.ACTION_MOVE:
if (1 != pointerProperties.size()) {
convertAction = (index << 8) | convertAction;
}
break;
case MotionEvent.ACTION_UP:
if (1 != pointerProperties.size()) {
convertAction = (index << 8) | MotionEvent.ACTION_POINTER_UP;
}
break;
}
Point point = device.getPhysicalPoint(position);
if (point == null) {
// ignore event
return false;
}
if (pointerProperties.isEmpty()) {
// ignore event
return false;
}
setPointerCoords(id, point);
MotionEvent.PointerProperties[] props = pointerProperties.toArray(new MotionEvent.PointerProperties[pointerProperties.size()]);
MotionEvent.PointerCoords[] coords = pointerCoords.toArray(new MotionEvent.PointerCoords[pointerCoords.size()]);
MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), convertAction,
pointerProperties.size(), props, coords, 0, 0, 1f, 1f, 0, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
if (action == MotionEvent.ACTION_UP) {
releasePointer(id);
}
return injectEvent(event);
}
private boolean injectMouse(int action, int buttons, Position position) {
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
long now = SystemClock.uptimeMillis();
if (action == MotionEvent.ACTION_DOWN) {
getPointer(0);
lastMouseDown = now;
}
Point point = device.getPhysicalPoint(position);
if (point == null) {
// ignore event
return false;
}
if (pointerProperties.isEmpty()) {
// ignore event
int pointerIndex = pointersState.getPointerIndex(pointerId);
if (pointerIndex == -1) {
Ln.w("Too many pointers for touch event");
return false;
}
setPointerCoords(0, point);
MotionEvent.PointerProperties[] props = pointerProperties.toArray(new MotionEvent.PointerProperties[pointerProperties.size()]);
MotionEvent.PointerCoords[] coords = pointerCoords.toArray(new MotionEvent.PointerCoords[pointerCoords.size()]);
MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action,
pointerProperties.size(), props, coords, 0, buttons, 1f, 1f, 0, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
Pointer pointer = pointersState.get(pointerIndex);
pointer.setPoint(point);
pointer.setPressure(pressure);
pointer.setUp(action == MotionEvent.ACTION_UP);
if (action == MotionEvent.ACTION_UP) {
releasePointer(0);
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
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, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
return injectEvent(event);
}
@ -288,23 +193,18 @@ public class Controller {
return false;
}
// init
MotionEvent.PointerProperties[] props = {new MotionEvent.PointerProperties()};
props[0].id = 0;
props[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent.PointerCoords[] coords = {new MotionEvent.PointerCoords()};
coords[0].orientation = 0;
coords[0].pressure = 1;
coords[0].size = 1;
MotionEvent.PointerProperties props = pointerProperties[0];
props.id = 0;
// set data
coords[0].x = point.x;
coords[0].y = point.y;
coords[0].setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
coords[0].setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
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(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, props, coords, 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, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_MOUSE, 0);
return injectEvent(event);
}
@ -316,8 +216,7 @@ public class Controller {
}
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) {

View file

@ -15,7 +15,7 @@ public final class DesktopConnection implements Closeable {
private static final int DEVICE_NAME_FIELD_LENGTH = 64;
private static final String SOCKET_NAME = "qtscrcpy";
private static final String SOCKET_NAME = "scrcpy";
private final LocalSocket videoSocket;
private final FileDescriptor videoFd;
@ -24,7 +24,6 @@ public final class DesktopConnection implements Closeable {
private final InputStream controlInputStream;
private final OutputStream controlOutputStream;
private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter();
@ -90,7 +89,7 @@ public final class DesktopConnection implements Closeable {
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
int len = Math.min(DEVICE_NAME_FIELD_LENGTH - 1, deviceNameBytes.length);
int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1);
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
// byte[] are always 0-initialized in java, no need to set '\0' explicitly

View file

@ -2,8 +2,8 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
@ -74,7 +74,6 @@ public final class Device {
@SuppressWarnings("checkstyle:MagicNumber")
private static Size computeVideoSize(int w, int h, int maxSize) {
// Compute the video size and the padding of the content inside this video.
/*
// Principle:
// - scale down the great side of the screen to maxSize (if necessary);
// - scale down the other side so that the aspect ratio is preserved;
@ -97,46 +96,7 @@ public final class Device {
w = portrait ? minor : major;
h = portrait ? major : minor;
}
*/
// Principle:480p/720p/1080p and not larger than device size.
w &= ~7; // in case it's not a multiple of 8
h &= ~7;
boolean vertival = h > w;
boolean validSize = false;
int newWidth = w;
int newHeight = h;
// 480p/720p/1080p
switch (maxSize) {
case 480: // 480p:640x480
newWidth = 640;
newHeight = 480;
validSize = true;
break;
case 720: // 720p:1280x720
newWidth = 1280;
newHeight = 720;
validSize = true;
break;
case 1080: // 1080p:1920x1080
newWidth = 1920;
newHeight = 1080;
validSize = true;
break;
}
// vertival convert
if (validSize && vertival) {
int temp = newWidth;
newWidth = newHeight;
newHeight = temp;
}
// not larger than device size.
if (newWidth > w || newHeight > h) {
newWidth = w;
newHeight = h;
}
return new Size(newWidth, newHeight);
return new Size(w, h);
}
public Point getPhysicalPoint(Position position) {
@ -152,8 +112,8 @@ public final class Device {
}
Rect contentRect = screenInfo.getContentRect();
Point point = position.getPoint();
int scaledX = contentRect.left + point.x * contentRect.width() / videoSize.getWidth();
int scaledY = contentRect.top + point.y * contentRect.height() / videoSize.getHeight();
int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth();
int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight();
return new Point(scaledX, scaledY);
}
@ -202,9 +162,34 @@ 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;
}
SurfaceControl.setDisplayPowerMode(d, mode);
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off " : "on ") + mode);
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
}
/**
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
*/
public void rotateDevice() {
WindowManager wm = serviceManager.getWindowManager();
boolean accelerometerRotation = !wm.isRotationFrozen();
int currentRotation = wm.getRotation();
int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0
String newRotationString = newRotation == 0 ? "portrait" : "landscape";
Ln.i("Device rotation requested: " + newRotationString);
wm.freezeRotation(newRotation);
// restore auto-rotate if necessary
if (accelerometerRotation) {
wm.thawRotation();
}
}
static Rect flipRect(Rect crop) {

View file

@ -27,8 +27,8 @@ public final class DeviceMessageSender {
text = clipboardText;
clipboardText = null;
}
DeviceMessage msg = DeviceMessage.createClipboard(text);
connection.sendDeviceMessage(msg);
DeviceMessage event = DeviceMessage.createClipboard(text);
connection.sendDeviceMessage(event);
}
}
}

View file

@ -27,7 +27,7 @@ public class DeviceMessageWriter {
output.write(rawBuffer, 0, buffer.position());
break;
default:
Ln.w("Unknown device msg: " + msg.getType());
Ln.w("Unknown device message: " + msg.getType());
break;
}
}

View file

@ -8,14 +8,11 @@ import android.util.Log;
*/
public final class Ln {
private static final String TAG = "qtscrcpy";
private static final String TAG = "scrcpy";
private static final String PREFIX = "[server] ";
enum Level {
DEBUG,
INFO,
WARN,
ERROR;
DEBUG, INFO, WARN, ERROR
}
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;

View file

@ -5,9 +5,10 @@ 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;
private boolean sendFrameMeta; // send PTS so that the client may record properly
private boolean control;
public int getMaxSize() {
@ -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;
}

View file

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

View file

@ -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;
}
}

View file

@ -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<Pointer> 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);
}
}
}
}

View file

@ -1,7 +1,5 @@
package com.genymobile.scrcpy;
import android.graphics.Point;
import java.util.Objects;
public class Position {
@ -34,8 +32,7 @@ public class Position {
return false;
}
Position position = (Position) o;
return Objects.equals(point, position.point)
&& Objects.equals(screenSize, position.screenSize);
return Objects.equals(point, position.point) && Objects.equals(screenSize, position.screenSize);
}
@Override
@ -45,10 +42,7 @@ public class Position {
@Override
public String toString() {
return "Position{"
+ "point=" + point
+ ", screenSize=" + screenSize
+ '}';
return "Position{" + "point=" + point + ", screenSize=" + screenSize + '}';
}
}

View file

@ -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.view.Surface;
@ -16,32 +17,29 @@ 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 maxFps;
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 maxFps, int iFrameInterval) {
this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate;
this.frameRate = frameRate;
this.maxFps = maxFps;
this.iFrameInterval = iFrameInterval;
}
public ScreenEncoder(boolean sendFrameMeta, int bitRate) {
this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
}
@Override
@ -54,7 +52,10 @@ public class ScreenEncoder implements Device.RotationListener {
}
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
Workarounds.prepareMainLooper();
Workarounds.fillAppInfo();
MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
device.setRotationListener(this);
boolean alive;
try {
@ -87,7 +88,6 @@ public class ScreenEncoder implements Device.RotationListener {
boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
@ -138,15 +138,24 @@ public class ScreenEncoder implements Device.RotationListener {
return MediaCodec.createEncoderByType("video/avc");
}
private static MediaFormat createFormat(int bitRate, int frameRate, 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);
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
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;
}

View file

@ -1,9 +1,10 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.os.Build;
import java.io.File;
import java.io.IOException;
public final class Server {
@ -18,7 +19,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);
@ -60,7 +61,7 @@ public final class Server {
sender.loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
Ln.d("Devide message sender stopped");
Ln.d("Device message sender stopped");
}
}
}).start();
@ -68,29 +69,42 @@ public final class Server {
@SuppressWarnings("checkstyle:MagicNumber")
private static Options createOptions(String... args) {
if (args.length != 6) {
throw new IllegalArgumentException("Expecting 5 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 != 8) {
throw new IllegalArgumentException("Expecting 8 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);
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[2]);
boolean tunnelForward = Boolean.parseBoolean(args[4]);
options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[3]);
Rect crop = parseCrop(args[5]);
options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[4]);
boolean sendFrameMeta = Boolean.parseBoolean(args[6]);
options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[5]);
boolean control = Boolean.parseBoolean(args[7]);
options.setControl(control);
return options;
@ -117,7 +131,21 @@ public final class Server {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Cannot unlink server", e);
Ln.e("Could not unlink server", e);
}
}
@SuppressWarnings("checkstyle:MagicNumber")
private static void suggestFix(Throwable e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (e instanceof MediaCodec.CodecException) {
MediaCodec.CodecException mce = (MediaCodec.CodecException) e;
if (mce.getErrorCode() == 0xfffffc0e) {
Ln.e("The hardware encoder is not able to encode at the given definition.");
Ln.e("Try with a lower definition:");
Ln.e(" scrcpy -m 1024");
}
}
}
}
@ -126,6 +154,7 @@ public final class Server {
@Override
public void uncaughtException(Thread t, Throwable e) {
Ln.e("Exception on thread " + t, e);
suggestFix(e);
}
});

View file

@ -38,8 +38,7 @@ public final class Size {
return false;
}
Size size = (Size) o;
return width == size.width
&& height == size.height;
return width == size.width && height == size.height;
}
@Override
@ -49,9 +48,6 @@ public final class Size {
@Override
public String toString() {
return "Size{"
+ "width=" + width
+ ", height=" + height
+ '}';
return "Size{" + "width=" + width + ", height=" + height + '}';
}
}

View file

@ -0,0 +1,79 @@
package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Looper;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
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()"
// <https://github.com/Genymobile/scrcpy/issues/240>
//
// 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"
// <https://github.com/Genymobile/scrcpy/issues/921>
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 appInfoField = appBindDataClass.getDeclaredField("appInfo");
appInfoField.setAccessible(true);
appInfoField.set(appBindData, applicationInfo);
// activityThread.mBoundApplication = appBindData;
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true);
mBoundApplicationField.set(activityThread, appBindData);
// Context ctx = activityThread.getSystemContext();
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
Application app = Instrumentation.newApplication(Application.class, ctx);
// activityThread.mInitialApplication = app;
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true);
mInitialApplicationField.set(activityThread, app);
} catch (Throwable throwable) {
// this is a workaround, so failing is not an error
Ln.w("Could not fill app info: " + throwable.getMessage());
}
}
}

View file

@ -1,44 +1,86 @@
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;
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 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() throws NoSuchMethodException {
if (getPrimaryClipMethod == null) {
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);
}
}
return getPrimaryClipMethod;
}
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
if (setPrimaryClipMethod == null) {
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);
}
}
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() {
try {
ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell");
Method method = getGetPrimaryClipMethod();
ClipData clipData = getPrimaryClip(method, manager);
if (clipData == null || clipData.getItemCount() == 0) {
return null;
}
return clipData.getItemAt(0).getText();
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
public void setText(CharSequence text) {
ClipData clipData = ClipData.newPlainText(null, text);
try {
setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell");
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
Method method = getSetPrimaryClipMethod();
ClipData clipData = ClipData.newPlainText(null, text);
setPrimaryClip(method, manager, clipData);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
}

View file

@ -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,26 @@ 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 {
}
private Method getInjectInputEventMethod() throws NoSuchMethodException {
if (injectInputEventMethod == null) {
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
} catch (NoSuchMethodException e) {
throw new AssertionError(e);
}
return injectInputEventMethod;
}
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
try {
return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
Method method = getInjectInputEventMethod();
return (boolean) method.invoke(manager, inputEvent, mode);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
}

View file

@ -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,28 @@ 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 {
}
private Method getIsScreenOnMethod() throws NoSuchMethodException {
if (isScreenOnMethod == null) {
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
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);
}
return isScreenOnMethod;
}
public boolean isScreenOn() {
try {
return (Boolean) isScreenOnMethod.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
Method method = getIsScreenOnMethod();
return (boolean) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
}

View file

@ -17,35 +17,35 @@ public class StatusBarManager {
this.manager = manager;
}
public void expandNotificationsPanel() {
private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException {
if (expandNotificationsPanelMethod == null) {
try {
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
} catch (NoSuchMethodException e) {
Ln.e("ServiceBarManager.expandNotificationsPanel() is not available on this device");
return;
}
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
}
return expandNotificationsPanelMethod;
}
private Method getCollapsePanelsMethod() throws NoSuchMethodException {
if (collapsePanelsMethod == null) {
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
}
return collapsePanelsMethod;
}
public void expandNotificationsPanel() {
try {
expandNotificationsPanelMethod.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Cannot invoke ServiceBarManager.expandNotificationsPanel()", e);
Method method = getExpandNotificationsPanelMethod();
method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
public void collapsePanels() {
if (collapsePanelsMethod == null) {
try {
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
} catch (NoSuchMethodException e) {
Ln.e("ServiceBarManager.collapsePanels() is not available on this device");
return;
}
}
try {
collapsePanelsMethod.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Cannot invoke ServiceBarManager.collapsePanels()", e);
Method method = getCollapsePanelsMethod();
method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
}
}

View file

@ -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,49 @@ public final class SurfaceControl {
}
}
public static IBinder getBuiltInDisplay(int builtInDisplayId) {
try {
private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException {
if (getBuiltInDisplayMethod == null) {
// the method signature has changed in Android Q
// <https://github.com/Genymobile/scrcpy/issues/586>
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId);
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
} else {
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
}
return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId);
} catch (Exception e) {
throw new AssertionError(e);
}
return getBuiltInDisplayMethod;
}
public static IBinder getBuiltInDisplay() {
try {
Method method = getGetBuiltInDisplayMethod();
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 | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
if (setDisplayPowerModeMethod == null) {
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
}
return setDisplayPowerModeMethod;
}
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
try {
CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode);
} catch (Exception e) {
throw new AssertionError(e);
Method method = getSetDisplayPowerModeMethod();
method.invoke(null, displayToken, mode);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}

View file

@ -1,27 +1,95 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.IInterface;
import android.view.IRotationWatcher;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public final class WindowManager {
private final IInterface manager;
private Method getRotationMethod;
private Method freezeRotationMethod;
private Method isRotationFrozenMethod;
private Method thawRotationMethod;
public WindowManager(IInterface manager) {
this.manager = manager;
}
public int getRotation() {
try {
private Method getGetRotationMethod() throws NoSuchMethodException {
if (getRotationMethod == null) {
Class<?> cls = manager.getClass();
try {
return (Integer) manager.getClass().getMethod("getRotation").invoke(manager);
} catch (NoSuchMethodException e) {
// method changed since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager);
getRotationMethod = cls.getMethod("getDefaultDisplayRotation");
} catch (NoSuchMethodException e) {
// old version
getRotationMethod = cls.getMethod("getRotation");
}
} catch (Exception e) {
throw new AssertionError(e);
}
return getRotationMethod;
}
private Method getFreezeRotationMethod() throws NoSuchMethodException {
if (freezeRotationMethod == null) {
freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class);
}
return freezeRotationMethod;
}
private Method getIsRotationFrozenMethod() throws NoSuchMethodException {
if (isRotationFrozenMethod == null) {
isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen");
}
return isRotationFrozenMethod;
}
private Method getThawRotationMethod() throws NoSuchMethodException {
if (thawRotationMethod == null) {
thawRotationMethod = manager.getClass().getMethod("thawRotation");
}
return thawRotationMethod;
}
public int getRotation() {
try {
Method method = getGetRotationMethod();
return (int) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return 0;
}
}
public void freezeRotation(int rotation) {
try {
Method method = getFreezeRotationMethod();
method.invoke(manager, rotation);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
public boolean isRotationFrozen() {
try {
Method method = getIsRotationFrozenMethod();
return (boolean) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
public void thawRotation() {
try {
Method method = getThawRotationMethod();
method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
@ -29,11 +97,12 @@ public final class WindowManager {
try {
Class<?> cls = manager.getClass();
try {
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
} catch (NoSuchMethodException e) {
// display parameter added since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
} catch (NoSuchMethodException e) {
// old version
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
}
} catch (Exception e) {
throw new AssertionError(e);

Binary file not shown.