Add Android 4.4 support and dump hierarchy

This commit is contained in:
wenxiaomao1023 2020-03-05 16:42:05 +08:00
parent acc9bcbc0a
commit 46d1c009d8
14 changed files with 856 additions and 251 deletions

Binary file not shown.

View file

@ -4,7 +4,7 @@ android {
compileSdkVersion 29
defaultConfig {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
minSdkVersion 19
targetSdkVersion 29
versionCode 14
versionName "1.12.1"

View file

@ -0,0 +1,360 @@
package com.genymobile.scrcpy;
import android.util.Xml;
import android.view.accessibility.AccessibilityNodeInfo;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import android.os.Build;
import android.app.UiAutomation;
import android.os.Looper;
import android.os.HandlerThread;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import android.os.Handler;
public class AccessibilityNodeInfoDumper {
private UiAutomation mUiAutomation;
private HandlerThread mHandlerThread;
private Handler handler;
private final Device device;
private final DesktopConnection connection;
private AccessibilityEventListenerImpl accessibilityEventListenerImpl;
private static final String[] NAF_EXCLUDED_CLASSES = new String[]{
android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(),
android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName()
};
public AccessibilityNodeInfoDumper(Handler handler, Device device, DesktopConnection connection) {
this.handler = handler;
this.device = device;
this.connection = connection;
this.accessibilityEventListenerImpl = new AccessibilityEventListenerImpl();
}
private class AccessibilityEventListenerImpl implements UiAutomation.OnAccessibilityEventListener {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
// Ln.i("EventType: " + AccessibilityEvent.eventTypeToString(event.getEventType()));
switch (event.getEventType()) {
case AccessibilityEvent.TYPE_ANNOUNCEMENT:
case AccessibilityEvent.TYPE_ASSIST_READING_CONTEXT:
case AccessibilityEvent.TYPE_GESTURE_DETECTION_END:
case AccessibilityEvent.TYPE_GESTURE_DETECTION_START:
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:
case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
case AccessibilityEvent.TYPE_TOUCH_INTERACTION_START:
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
case AccessibilityEvent.TYPE_VIEW_CLICKED:
case AccessibilityEvent.TYPE_VIEW_CONTEXT_CLICKED:
case AccessibilityEvent.TYPE_VIEW_FOCUSED:
case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT:
case AccessibilityEvent.TYPE_VIEW_LONG_CLICKED:
case AccessibilityEvent.TYPE_VIEW_SCROLLED:
case AccessibilityEvent.TYPE_VIEW_SELECTED:
case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED:
case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY:
case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
// case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
dumpWindowHierarchy(device, bos);
} catch (IOException e) {
e.printStackTrace();
Ln.e("onAccessibilityEvent error: " + e.getMessage());
} finally {
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
}
}
}
Ln.i("dumpHierarchy size: " + bos.toByteArray().length);
ByteBuffer b = ByteBuffer.allocate(4 + bos.toByteArray().length);
b.order(ByteOrder.LITTLE_ENDIAN);
b.putInt(bos.toByteArray().length);
b.put(bos.toByteArray());
byte[] hierarchy = b.array();
try {
IO.writeFully(connection.getVideoChannel(), hierarchy, 0, hierarchy.length);// IOException
} catch (IOException e) {
Common.stopScrcpy(handler, "hierarchy");
}
break;
}
}
}
public void start() {
Object connection = null;
mHandlerThread = new HandlerThread("ScrcpyUiAutomationHandlerThread");
mHandlerThread.start();
try {
Class<?> UiAutomationConnection = Class.forName("android.app.UiAutomationConnection");
Constructor<?> newInstance = UiAutomationConnection.getDeclaredConstructor();
newInstance.setAccessible(true);
connection = newInstance.newInstance();
Class<?> IUiAutomationConnection = Class.forName("android.app.IUiAutomationConnection");
Constructor<?> newUiAutomation = UiAutomation.class.getDeclaredConstructor(Looper.class, IUiAutomationConnection);
mUiAutomation = (UiAutomation) newUiAutomation.newInstance(mHandlerThread.getLooper(), connection);
Method connect = UiAutomation.class.getDeclaredMethod("connect");
connect.invoke(mUiAutomation);
Ln.i("mUiAutomation: " + mUiAutomation);
if (mUiAutomation != null) {
mUiAutomation.setOnAccessibilityEventListener(accessibilityEventListenerImpl);
}
} catch (Exception e) {
e.printStackTrace();
Ln.e("AccessibilityNodeInfoDumper start: " + e.getMessage());
stop();
}
}
public void stop() {
if (mUiAutomation != null) {
mUiAutomation.setOnAccessibilityEventListener(null);
try {
Method disconnect = UiAutomation.class.getDeclaredMethod("disconnect");
disconnect.invoke(mUiAutomation);
} catch (Exception e) {
e.printStackTrace();
Ln.e("disconnect: " + e.getMessage());
}
mUiAutomation = null;
}
if (mHandlerThread != null) {
mHandlerThread.quit();
}
}
private void restart() {
stop();
start();
}
public void dumpWindowHierarchy(Device device, OutputStream out) throws IOException {
XmlSerializer serializer = Xml.newSerializer();
// serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
serializer.setOutput(out, "UTF-8");
serializer.startDocument("UTF-8", true);
serializer.startTag("", "hierarchy"); // TODO(allenhair): Should we use a namespace?
serializer.attribute("", "rotation", Integer.toString(device.getRotation()));
int width = device.getScreenInfo().getContentRect().width();
int height = device.getScreenInfo().getContentRect().height();
if (mUiAutomation == null) {
restart();
}
if (mUiAutomation != null) {
Set<AccessibilityNodeInfo> roots = new HashSet();
// Start with the active window, which seems to sometimes be missing from the list returned
// by the UiAutomation.
AccessibilityNodeInfo activeRoot = mUiAutomation.getRootInActiveWindow();
if (activeRoot != null) {
roots.add(activeRoot);
}
// Support multi-window searches for API level 21 and up.
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
for (AccessibilityWindowInfo window : mUiAutomation.getWindows()) {
AccessibilityNodeInfo root = window.getRoot();
if (root == null) {
Ln.w(String.format("Skipping null root node for window: %s", window.toString()));
continue;
}
roots.add(root);
}
}
AccessibilityNodeInfo[] nodeInfos = roots.toArray(new AccessibilityNodeInfo[roots.size()]);
for (int i = 0; i < nodeInfos.length; i++) {
dumpNodeRec(nodeInfos[i], serializer, 0, width, height, nodeInfos.length - 1 - i);
}
}
serializer.endTag("", "hierarchy");
serializer.endDocument();
}
private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer, int index,
int width, int height, int zIndex) throws IOException {
serializer.startTag("", "node");
if (!nafExcludedClass(node) && !nafCheck(node))
serializer.attribute("", "NAF", Boolean.toString(true));
serializer.attribute("", "index", Integer.toString(index));
serializer.attribute("", "text", safeCharSeqToString(node.getText()));
serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName()));
serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription()));
serializer.attribute("", "checkable", Boolean.toString(node.isCheckable()));
serializer.attribute("", "checked", Boolean.toString(node.isChecked()));
serializer.attribute("", "clickable", Boolean.toString(node.isClickable()));
serializer.attribute("", "enabled", Boolean.toString(node.isEnabled()));
serializer.attribute("", "focusable", Boolean.toString(node.isFocusable()));
serializer.attribute("", "focused", Boolean.toString(node.isFocused()));
serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable()));
serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
serializer.attribute("", "password", Boolean.toString(node.isPassword()));
serializer.attribute("", "selected", Boolean.toString(node.isSelected()));
serializer.attribute("", "visible-to-user", Boolean.toString(node.isVisibleToUser()));
serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
node, width, height).toShortString());
serializer.attribute("", "z-index", Integer.toString(zIndex));
int count = node.getChildCount();
for (int i = 0; i < count; i++) {
AccessibilityNodeInfo child = node.getChild(i);
if (child != null) {
if (child.isVisibleToUser()) {
dumpNodeRec(child, serializer, i, width, height, zIndex);
child.recycle();
}
// else {
// Ln.i(String.format("Skipping invisible child: %s", child.toString()));
// }
} else {
Ln.i(String.format("Null child %d/%d, parent: %s", i, count, node.toString()));
}
}
serializer.endTag("", "node");
}
/**
* The list of classes to exclude my not be complete. We're attempting to
* only reduce noise from standard layout classes that may be falsely
* configured to accept clicks and are also enabled.
*
* @param node
* @return true if node is excluded.
*/
private static boolean nafExcludedClass(AccessibilityNodeInfo node) {
String className = safeCharSeqToString(node.getClassName());
for (String excludedClassName : NAF_EXCLUDED_CLASSES) {
if (className.endsWith(excludedClassName))
return true;
}
return false;
}
/**
* We're looking for UI controls that are enabled, clickable but have no
* text nor content-description. Such controls configuration indicate an
* interactive control is present in the UI and is most likely not
* accessibility friendly. We refer to such controls here as NAF controls
* (Not Accessibility Friendly)
*
* @param node
* @return false if a node fails the check, true if all is OK
*/
private static boolean nafCheck(AccessibilityNodeInfo node) {
boolean isNaf = node.isClickable() && node.isEnabled()
&& safeCharSeqToString(node.getContentDescription()).isEmpty()
&& safeCharSeqToString(node.getText()).isEmpty();
if (!isNaf)
return true;
// check children since sometimes the containing element is clickable
// and NAF but a child's text or description is available. Will assume
// such layout as fine.
return childNafCheck(node);
}
/**
* This should be used when it's already determined that the node is NAF and
* a further check of its children is in order. A node maybe a container
* such as LinerLayout and may be set to be clickable but have no text or
* content description but it is counting on one of its children to fulfill
* the requirement for being accessibility friendly by having one or more of
* its children fill the text or content-description. Such a combination is
* considered by this dumper as acceptable for accessibility.
*
* @param node
* @return false if node fails the check.
*/
private static boolean childNafCheck(AccessibilityNodeInfo node) {
int childCount = node.getChildCount();
for (int x = 0; x < childCount; x++) {
AccessibilityNodeInfo childNode = node.getChild(x);
if (childNode != null) {//wen add childNode null
if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty()
|| !safeCharSeqToString(childNode.getText()).isEmpty())
return true;
if (childNafCheck(childNode))
return true;
}
}
return false;
}
private static String safeCharSeqToString(CharSequence cs) {
if (cs == null)
return "";
else {
return stripInvalidXMLChars(cs);
}
}
private static String stripInvalidXMLChars(CharSequence cs) {
StringBuffer ret = new StringBuffer();
char ch;
/* http://www.w3.org/TR/xml11/#charsets
[#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF],
[#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF],
[#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF],
[#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF],
[#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF],
[#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF],
[#x10FFFE-#x10FFFF].
*/
for (int i = 0; i < cs.length(); i++) {
ch = cs.charAt(i);
if ((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) ||
(ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) ||
(ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) ||
(ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) ||
(ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) ||
(ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) ||
(ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) ||
(ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) ||
(ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) ||
(ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) ||
(ch >= 0x10FFFE && ch <= 0x10FFFF))
ret.append(".");
else
ret.append(ch);
}
return ret.toString();
}
}

View file

@ -0,0 +1,44 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
import android.os.Build;
import android.view.accessibility.AccessibilityNodeInfo;
public class AccessibilityNodeInfoHelper {
/**
* Returns the node's bounds clipped to the size of the display
*
* @param node
* @param width pixel width of the display
* @param height pixel height of the display
* @return null if node is null, else a Rect containing visible bounds
*/
static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int width, int height) {
if (node == null) {
return null;
}
// targeted node's bounds
Rect nodeRect = new Rect();
node.getBoundsInScreen(nodeRect);
Rect displayRect = new Rect();
displayRect.top = 0;
displayRect.left = 0;
displayRect.right = width;
displayRect.bottom = height;
nodeRect.intersect(displayRect);
// On platforms that give us access to the node's window
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Trim any portion of the bounds that are outside the window
Rect window = new Rect();
if (node.getWindow() != null) {
node.getWindow().getBoundsInScreen(window);
nodeRect.intersect(window);
}
}
return nodeRect;
}
}

View file

@ -0,0 +1,18 @@
package com.genymobile.scrcpy;
import android.os.Message;
import android.os.Handler;
public class Common {
public static void stopScrcpy(Handler handler, String obj) {
Message msg = Message.obtain();
msg.what = 1;
msg.obj = obj;
try {
handler.sendMessage(msg);
} catch (java.lang.IllegalStateException e) {
}
}
}

View file

@ -40,11 +40,11 @@ public class ControlMessageReader {
int head = buffer.position();
int r = input.read(rawBuffer, head, rawBuffer.length - head);
if (r == -1) {
Ln.i("=========================================>>>");
Ln.i("head: " + head);
Ln.i("rawBuffer.length: " + rawBuffer.length);
Ln.i("rawBuffer: " + Arrays.toString(rawBuffer));
Ln.i("=========================================<<<");
// Ln.i("=========================================>>>");
// Ln.i("head: " + head);
// Ln.i("rawBuffer.length: " + rawBuffer.length);
// Ln.i("rawBuffer: " + Arrays.toString(rawBuffer));
// Ln.i("=========================================<<<");
throw new EOFException("Controller socket closed");
}
buffer.position(head + r);

View file

@ -11,28 +11,39 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.channels.ServerSocketChannel;
public final class DesktopConnection implements Closeable {
private static final int DEVICE_NAME_FIELD_LENGTH = 64;
private static final String SOCKET_NAME = "scrcpy";
private static final int SOCKET_PORT = 6612;
private final LocalSocket videoSocket;
private final Socket videoSocket;
private final FileDescriptor videoFd;
private final SocketChannel videoChannel;
private final LocalSocket controlSocket;
private final Socket controlSocket;
private final InputStream controlInputStream;
private final OutputStream controlOutputStream;
private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter();
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
this.videoSocket = videoSocket;
this.controlSocket = controlSocket;
controlInputStream = controlSocket.getInputStream();
controlOutputStream = controlSocket.getOutputStream();
videoFd = videoSocket.getFileDescriptor();
private DesktopConnection(SocketChannel videoSocket, SocketChannel controlSocket) throws IOException {
this.videoSocket = videoSocket.socket();
this.controlSocket = controlSocket.socket();
controlInputStream = controlSocket.socket().getInputStream();
controlOutputStream = controlSocket.socket().getOutputStream();
// videoFd = videoSocket.getFileDescriptor();
videoFd = null;//no use
videoChannel = videoSocket.socket().getChannel();
Ln.i("videoChannel: " + videoChannel);
}
private static LocalSocket connect(String abstractName) throws IOException {
@ -42,10 +53,11 @@ public final class DesktopConnection implements Closeable {
}
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
LocalSocket videoSocket;
LocalSocket controlSocket;
SocketChannel videoSocket = null;
SocketChannel controlSocket = null;
if (tunnelForward) {
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
ServerSocketChannel localServerSocket = ServerSocketChannel.open();
localServerSocket.socket().bind(new InetSocketAddress(SOCKET_PORT));
try {
videoSocket = localServerSocket.accept();
// send one byte so the client may read() to detect a connection error
@ -60,13 +72,13 @@ public final class DesktopConnection implements Closeable {
localServerSocket.close();
}
} else {
videoSocket = connect(SOCKET_NAME);
try {
controlSocket = connect(SOCKET_NAME);
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
// videoSocket = connect(SOCKET_NAME);
// try {
// controlSocket = connect(SOCKET_NAME);
// } catch (IOException | RuntimeException e) {
// videoSocket.close();
// throw e;
// }
}
DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket);
@ -84,26 +96,30 @@ public final class DesktopConnection implements Closeable {
controlSocket.close();
}
@SuppressWarnings("checkstyle:MagicNumber")
private void send(String deviceName, int width, int height) throws IOException {
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
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
buffer[DEVICE_NAME_FIELD_LENGTH] = (byte) (width >> 8);
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
IO.writeFully(videoFd, buffer, 0, buffer.length);
}
// @SuppressWarnings("checkstyle:MagicNumber")
// private void send(String deviceName, int width, int height) throws IOException {
// byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
//
// byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
// 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
//
// buffer[DEVICE_NAME_FIELD_LENGTH] = (byte) (width >> 8);
// buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
// buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
// buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
// IO.writeFully(videoFd, buffer, 0, buffer.length);
// }
public FileDescriptor getVideoFd() {
return videoFd;
}
public SocketChannel getVideoChannel() {
return videoChannel;
}
public ControlMessage receiveControlMessage() throws IOException {
ControlMessage msg = reader.next();
while (msg == null) {

View file

@ -106,6 +106,8 @@ public final class Device {
Size videoSize = screenInfo.getVideoSize();
Size clientVideoSize = position.getScreenSize();
if (!videoSize.equals(clientVideoSize)) {
Ln.i("video width: " + videoSize.getWidth() + ", video height: " + videoSize.getHeight());
Ln.i("client width: " + clientVideoSize.getWidth() + ", client height: " + clientVideoSize.getHeight());
// The client sends a click relative to a video with wrong dimensions,
// the device may have been rotated since the event was generated, so ignore the event
return null;
@ -195,4 +197,8 @@ public final class Device {
static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
}
public int getRotation() {
return serviceManager.getWindowManager().getRotation();
}
}

View file

@ -1,40 +1,36 @@
package com.genymobile.scrcpy;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public final class IO {
private IO() {
// not instantiable
}
public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
public synchronized static void writeFully(SocketChannel channel, ByteBuffer from) throws IOException {
// ByteBuffer position is not updated as expected by Os.write() on old Android versions, so
// count the remaining bytes manually.
// See <https://github.com/Genymobile/scrcpy/issues/291>.
int remaining = from.remaining();
while (remaining > 0) {
try {
int w = Os.write(fd, from);
int w = channel.write(from);
if (BuildConfig.DEBUG && w < 0) {
// w should not be negative, since an exception is thrown on error
throw new AssertionError("Os.write() returned a negative value (" + w + ")");
throw new AssertionError("write() returned a negative value (" + w + ")");
}
remaining -= w;
} catch (ErrnoException e) {
if (e.errno != OsConstants.EINTR) {
throw new IOException(e);
}
} catch (Exception e) {
throw new IOException(e);
}
}
}
public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException {
writeFully(fd, ByteBuffer.wrap(buffer, offset, len));
public synchronized static void writeFully(SocketChannel channel, byte[] buffer, int offset, int len) throws IOException {
writeFully(channel, ByteBuffer.wrap(buffer, offset, len));
}
}

View file

@ -6,9 +6,20 @@ import java.nio.ByteBuffer;
public class JpegEncoder {
static {
System.loadLibrary("compress");
try {
System.loadLibrary("compress");
} catch (UnsatisfiedLinkError e1) {
try {
System.load("/data/local/tmp/libturbojpeg.so");
System.load("/data/local/tmp/libcompress.so");
} catch (UnsatisfiedLinkError e2) {
Ln.e("UnsatisfiedLinkError: " + e2.getMessage());
System.exit(1);
}
}
}
public static native byte[] compress(ByteBuffer buffer, int width, int pitch, int height, int quality);
public static native void test();
}

View file

@ -14,6 +14,9 @@ public class Options {
//wen add
private int quality;
private int scale;
private boolean controlOnly;
private boolean nalu;
private boolean dumpHierarchy;
public int getMaxSize() {
return maxSize;
@ -87,4 +90,27 @@ public class Options {
this.scale = scale;
}
public boolean getControlOnly() {
return controlOnly;
}
public void setControlOnly(boolean controlOnly) {
this.controlOnly = controlOnly;
}
public boolean getNALU() {
return nalu;
}
public void setNALU(boolean nalu) {
this.nalu = nalu;
}
public boolean getDumpHierarchy() {
return dumpHierarchy;
}
public void setDumpHierarchy(boolean dumpHierarchy) {
this.dumpHierarchy = dumpHierarchy;
}
}

View file

@ -3,9 +3,6 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
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;
@ -14,11 +11,11 @@ import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import android.graphics.ImageFormat;
import android.graphics.YuvImage;
import android.media.Image;
import android.media.MediaExtractor;
import android.util.Log;
import java.io.File;
@ -36,12 +33,12 @@ import android.os.Handler;
import android.os.Message;
import java.util.Arrays;
import android.os.Looper;
import java.nio.ByteOrder;
import android.os.Process;
import android.os.HandlerThread;
import java.nio.channels.SocketChannel;
public class ScreenEncoder implements Device.RotationListener {
@ -51,6 +48,7 @@ public class ScreenEncoder implements Device.RotationListener {
private static final int NO_PTS = -1;
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final AtomicInteger mRotation = new AtomicInteger(0);
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private int bitRate;
@ -61,9 +59,18 @@ public class ScreenEncoder implements Device.RotationListener {
private int quality;
private int scale;
private boolean controlOnly;
private Handler mHandler;
private ImageReader mImageReader;
private ImageListener mImageListener;
private HandlerThread mHandlerThread;
private ImageReader.OnImageAvailableListener imageAvailableListenerImpl;
private Device device;
private final Object rotationLock = new Object();
private final Object imageReaderLock = new Object();
private boolean bImageReaderDisable = true;//Segmentation fault
private boolean alive = true;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) {
this.sendFrameMeta = sendFrameMeta;
@ -76,219 +83,285 @@ public class ScreenEncoder implements Device.RotationListener {
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
}
public ScreenEncoder(int quality, int maxFps, int scale) {
this.quality = quality;
this.maxFps = maxFps;
this.scale = scale;
public ScreenEncoder(Options options, Device device/*int rotation*/) {
this.quality = options.getQuality();
this.maxFps = options.getMaxFps();
this.scale = options.getScale();
this.controlOnly = options.getControlOnly();
this.bitRate = options.getBitRate();
this.device = device;
mRotation.set(device.getRotation());
mHandlerThread = new HandlerThread("ScrcpyImageReaderHandlerThread");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
Ln.i("hander message: " + msg);
if (msg.what == 1) {//exit
setAlive(false);
synchronized (rotationLock) {
rotationLock.notify();
}
}
}
};
}
@Override
public void onRotationChanged(int rotation) {
Ln.i("rotation: " + rotation);
mRotation.set(rotation);
rotationChanged.set(true);
synchronized (rotationLock) {
rotationLock.notify();
}
}
public boolean consumeRotationChange() {
return rotationChanged.getAndSet(false);
}
private final class ImageListener implements ImageReader.OnImageAvailableListener {
private class ImageAvailableListenerImpl implements ImageReader.OnImageAvailableListener {
Handler handler;
SocketChannel fd;
Device device;
int type = 0;// 0:libjpeg-turbo 1:bitmap
int quality;
int framePeriodMs;
int count = 0;
long lastTime = System.currentTimeMillis();
long timeA = lastTime;
public ImageAvailableListenerImpl(Handler handler, Device device, SocketChannel fd, int frameRate, int quality) {
this.handler = handler;
this.fd = fd;
this.device = device;
this.quality = quality;
this.framePeriodMs = (int) (1000 / frameRate);
}
@Override
public void onImageAvailable(ImageReader reader) {
Ln.i("onImageAvailable !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
Ln.i("onImageAvailable !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
Ln.i("onImageAvailable !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
Ln.i("onImageAvailable !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
Ln.i("onImageAvailable !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
public void onImageAvailable(ImageReader imageReader) {
byte[] jpegData = null;
byte[] jpegSize = null;
Image image = null;
synchronized (imageReaderLock) {
try {
if (bImageReaderDisable) {
Ln.i("bImageReaderDisable !!!!!!!!!");
return;
}
image = imageReader.acquireLatestImage();
if (image == null) {
return;
}
long currentTime = System.currentTimeMillis();
if (framePeriodMs > currentTime - lastTime) {
return;
}
lastTime = currentTime;
int width = image.getWidth();
int height = image.getHeight();
int format = image.getFormat();//RGBA_8888 0x00000001
final Image.Plane[] planes = image.getPlanes();
final ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * width;
int pitch = width + rowPadding / pixelStride;
// Ln.i("pitch: " + pitch + ", pixelStride: " + pixelStride + ", rowStride: " + rowStride + ", rowPadding: " + rowPadding);
if (type == 0) {
jpegData = JpegEncoder.compress(buffer, width, pitch, height, quality);
} else if (type == 1) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
Bitmap bitmap = Bitmap.createBitmap(pitch, height, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream);
jpegData = stream.toByteArray();
bitmap.recycle();
}
if (jpegData == null) {
Ln.e("jpegData is null");
return;
}
ByteBuffer b = ByteBuffer.allocate(4 + jpegData.length);
b.order(ByteOrder.LITTLE_ENDIAN);
b.putInt(jpegData.length);
b.put(jpegData);
jpegSize = b.array();
try {
IO.writeFully(fd, jpegSize, 0, jpegSize.length);// IOException
} catch (IOException e) {
Common.stopScrcpy(handler, "image");
}
} catch (Exception e) {
Ln.e("onImageAvailable: " + e.getMessage());
} finally {
if (image != null) {
image.close();
}
}
}
count++;
long timeB = System.currentTimeMillis();
if (timeB - timeA >= 1000) {
timeA = timeB;
Ln.i("frame rate: " + count + ", jpeg size: " + jpegSize.length);
count = 0;
}
}
}
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
public Handler getHandler() {
return mHandler;
}
public void streamScreen(Device device, SocketChannel fd) throws IOException {
Workarounds.prepareMainLooper();
Workarounds.fillAppInfo();
device.setRotationListener(this);
boolean alive;
try {
banner(device, fd);
writeMinicapBanner(device, fd, scale);
do {
// mHandler = new Handler(Looper.getMainLooper());
IBinder display = createDisplay();
Rect contentRect = device.getScreenInfo().getContentRect();
// Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
Rect videoRect = new Rect(0, 0, contentRect.width() / scale, contentRect.height() / scale);
mImageReader = ImageReader.newInstance(videoRect.width(), videoRect.height(), PixelFormat.RGBA_8888, 2);
mImageListener = new ImageListener();
mImageReader.setOnImageAvailableListener(mImageListener, mHandler);
Surface surface = mImageReader.getSurface();
setDisplaySurface(display, surface, contentRect, videoRect);
try {
alive = encode(mImageReader, fd);
} finally {
writeRotation(fd);
if (controlOnly) {
synchronized (rotationLock) {
try {
rotationLock.wait();
} catch (InterruptedException e) {
}
}
} else {
IBinder display = createDisplay();
Rect contentRect = device.getScreenInfo().getContentRect();
// Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
Rect videoRect = getDesiredSize(contentRect, scale);
synchronized (imageReaderLock) {
mImageReader = ImageReader.newInstance(videoRect.width(), videoRect.height(), PixelFormat.RGBA_8888, 2);
bImageReaderDisable = false;
}
if (imageAvailableListenerImpl == null) {
imageAvailableListenerImpl = new ImageAvailableListenerImpl(mHandler, device, fd, maxFps, quality);
}
mImageReader.setOnImageAvailableListener(imageAvailableListenerImpl, mHandler);
Surface surface = mImageReader.getSurface();
setDisplaySurface(display, surface, contentRect, videoRect);
synchronized (rotationLock) {
try {
rotationLock.wait();
} catch (InterruptedException e) {
}
}
synchronized (imageReaderLock) {
if (mImageReader != null) {
bImageReaderDisable = true;
mImageReader.close();
}
}
destroyDisplay(display);
surface.release();
}
alive = getAlive();
Ln.i("alive: " + alive);
} while (alive);
} catch (Exception e) {
e.printStackTrace();
Ln.e("streamScreen: " + e.getMessage());
} finally {
if (mHandlerThread != null) {
mHandlerThread.quit();
}
device.setRotationListener(null);
}
}
private boolean encode(ImageReader imageReader, FileDescriptor fd) throws IOException {
int count = 0;
long current = System.currentTimeMillis();
int type = 0;// 0:libjpeg-turbo 1:bitmap
int frameRate = this.maxFps;
int quality = this.quality;
int framePeriodMs = (int) (1000 / frameRate);
while (!consumeRotationChange()) {
long timeA = System.currentTimeMillis();
Image image = null;
int loop = 0;
int wait = 1;
// TODO onImageAvailable这个方法不回调未找到原因暂时写成while
while ((image = imageReader.acquireNextImage()) == null && ++loop < 10) {
try {
Thread.sleep(wait++);
} catch (InterruptedException e) {
}
}
if (image == null) {
continue;
}
int width = image.getWidth();
int height = image.getHeight();
int format = image.getFormat();//RGBA_8888 0x00000001
final Image.Plane[] planes = image.getPlanes();
final ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * width;
int pitch = width + rowPadding / pixelStride;
byte[] jpegData = null;
byte[] jpegSize = null;
if (type == 0) {
jpegData = JpegEncoder.compress(buffer, width, pitch, height, quality);
} else if (type == 1) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
Bitmap bitmap = Bitmap.createBitmap(pitch, height, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream);
jpegData = stream.toByteArray();
bitmap.recycle();
}
image.close();
if (jpegData == null) {
Ln.e("jpegData is null");
continue;
}
ByteBuffer b = ByteBuffer.allocate(4);
b.order(ByteOrder.LITTLE_ENDIAN);
b.putInt(jpegData.length);
jpegSize = b.array();
IO.writeFully(fd, jpegSize, 0, jpegSize.length);
IO.writeFully(fd, jpegData, 0, jpegData.length);
count++;
long timeB = System.currentTimeMillis();
if (timeB - current >= 1000) {
current = timeB;
Ln.i("frame rate: " + count + ", jpeg size: " + jpegData.length);
count = 0;
}
if (framePeriodMs > 0) {
long ms = framePeriodMs - timeB + timeA;
if (ms > 0) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
}
}
}
private Rect getDesiredSize(Rect contentRect, int resolution) {
int realWidth = contentRect.width();
int realHeight = contentRect.height();
int desiredWidth = realWidth;
int desiredHeight = realHeight;
int h = realHeight;
if (realWidth < realHeight) {
h = realWidth;
}
return true;
if (h > resolution) {
desiredWidth = contentRect.width() * resolution / h;
desiredHeight = contentRect.height() * resolution / h;
desiredWidth = (desiredWidth + 4) & ~7;
desiredHeight = (desiredHeight + 4) & ~7;
} else {
desiredWidth &= ~7;
desiredHeight &= ~7;
}
Ln.i("realWidth: " + realWidth + ", realHeight: " + realHeight + ", desiredWidth: " + desiredWidth + ", desiredHeight: " + desiredHeight);
return new Rect(0, 0, desiredWidth, desiredHeight);
}
// minicap banner
private void banner(Device device, FileDescriptor fd) throws IOException {
private void writeRotation(SocketChannel fd) {
ByteBuffer r = ByteBuffer.allocate(8);
r.order(ByteOrder.LITTLE_ENDIAN);
r.putInt(4);
r.putInt(mRotation.get());
byte[] rArray = r.array();
try {
IO.writeFully(fd, rArray, 0, rArray.length);// IOException
} catch (IOException e) {
Common.stopScrcpy(getHandler(), "rotation");
}
}
private void writeMinicapBanner(Device device, SocketChannel fd, int scale) throws IOException {
final byte BANNER_SIZE = 24;
Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
int width = videoRect.width();
int height = videoRect.height();
final byte version = 1;
final byte quirks = 2;
int pid = Process.myPid();
Rect contentRect = device.getScreenInfo().getContentRect();
Rect videoRect = getDesiredSize(contentRect, scale);
int realWidth = contentRect.width();
int realHeight = contentRect.height();
int desiredWidth = videoRect.width();
int desiredHeight = videoRect.height();
byte orientation = (byte) device.getRotation();
ByteBuffer b = ByteBuffer.allocate(BANNER_SIZE);
b.order(ByteOrder.LITTLE_ENDIAN);
b.put((byte) 1);//version
b.put((byte) version);//version
b.put(BANNER_SIZE);//banner size
b.putInt(pid);//pid
b.putInt(width);//real width
b.putInt(height);//real height
b.putInt(width);//desired width
b.putInt(height);//desired height
b.put((byte) 0);//orientation
b.put((byte) 2);//quirks
b.putInt(realWidth);//real width
b.putInt(realHeight);//real height
b.putInt(desiredWidth);//desired width
b.putInt(desiredHeight);//desired height
b.put((byte) orientation);//orientation
b.put((byte) quirks);//quirks
byte[] array = b.array();
IO.writeFully(fd, array, 0, array.length);
}
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
headerBuffer.clear();
long pts;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
pts = NO_PTS; // non-media data packet
} else {
if (ptsOrigin == 0) {
ptsOrigin = bufferInfo.presentationTimeUs;
}
pts = bufferInfo.presentationTimeUs - ptsOrigin;
}
headerBuffer.putLong(pts);
headerBuffer.putInt(packetSize);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
private static MediaCodec createCodec() throws IOException {
return MediaCodec.createEncoderByType("video/avc");
}
@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);
// 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, 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;
IO.writeFully(fd, array, 0, array.length);// IOException
Ln.i("banner\n"
+ "{\n"
+ " version: " + version + "\n"
+ " size: " + BANNER_SIZE + "\n"
+ " real width: " + realWidth + "\n"
+ " real height: " + realHeight + "\n"
+ " desired width: " + desiredWidth + "\n"
+ " desired height: " + desiredHeight + "\n"
+ " orientation: " + orientation + "\n"
+ " quirks: " + quirks + "\n"
+ "}\n"
);
}
private static IBinder createDisplay() {
return SurfaceControl.createDisplay("scrcpy", true);
}
private static void configure(MediaCodec codec, MediaFormat format) {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}
private static void setSize(MediaFormat format, int width, int height) {
format.setInteger(MediaFormat.KEY_WIDTH, width);
format.setInteger(MediaFormat.KEY_HEIGHT, height);
}
private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) {
SurfaceControl.openTransaction();
try {
@ -303,4 +376,12 @@ public class ScreenEncoder implements Device.RotationListener {
private static void destroyDisplay(IBinder display) {
SurfaceControl.destroyDisplay(display);
}
private synchronized boolean getAlive() {
return alive;
}
private synchronized void setAlive(boolean b) {
alive = b;
}
}

View file

@ -6,21 +6,30 @@ import android.os.Build;
import java.io.File;
import java.io.IOException;
import java.lang.System;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import android.os.Handler;
public final class Server {
private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
private static Handler handler;
private Server() {
// not instantiable
}
private static void scrcpy(Options options) throws IOException {
AccessibilityNodeInfoDumper dumper = null;
final Device device = new Device(options);
boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getQuality(), options.getMaxFps(), options.getScale());
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options, device);
handler = screenEncoder.getHandler();
if (options.getControl()) {
Controller controller = new Controller(device, connection);
@ -29,13 +38,26 @@ public final class Server {
startDeviceMessageSender(controller.getSender());
}
if (options.getDumpHierarchy()) {
dumper = new AccessibilityNodeInfoDumper(handler, device, connection);
dumper.start();
}
try {
// synchronous
screenEncoder.streamScreen(device, connection.getVideoFd());
screenEncoder.streamScreen(device, connection.getVideoChannel());
} catch (IOException e) {
Ln.i("exit: " + e.getMessage());
//do exit(0)
} finally {
if (options.getDumpHierarchy() && dumper != null) {
dumper.stop();
}
// this is expected on close
Ln.d("Screen streaming stopped");
System.exit(0);
}
}
}
@ -48,7 +70,7 @@ public final class Server {
} catch (IOException e) {
// this is expected on close
Ln.d("Controller stopped");
Ln.d("E:" + e.getMessage());
Common.stopScrcpy(handler, "control");
}
}
}).start();
@ -118,7 +140,10 @@ public final class Server {
org.apache.commons.cli.Options options = new org.apache.commons.cli.Options();
options.addOption("Q", true, "JPEG quality (0-100)");
options.addOption("r", true, "Frame rate (frames/s)");
options.addOption("P", true, "Display projection (scale 1,2,4...)");
options.addOption("P", true, "Display projection (1080, 720, 480...).");
options.addOption("c", false, "Control only");
options.addOption("L", false, "Library path");
options.addOption("D", false, "Dump window hierarchy");
options.addOption("h", false, "Show help");
try {
commandLine = parser.parse(options, args);
@ -129,25 +154,39 @@ public final class Server {
if (commandLine.hasOption('h')) {
System.out.println(
"Usage: %s [-h]\n"
"Usage: [-h]\n\n"
+ "jpeg:\n"
+ " -r <value>: Frame rate (frames/sec).\n"
+ " -P <value>: Display projection (1080, 720, 480, 360...).\n"
+ " -Q <value>: JPEG quality (0-100).\n"
+ " -r <value>: Frame rate (frames/s).\n"
+ " -P <value>: Display projection (scale 1,2,4...).\n"
+ "\n"
+ " -c: Control only.\n"
+ " -L: Library path.\n"
+ " -D: Dump window hierarchy.\n"
+ " -h: Show help.\n"
);
System.exit(0);
}
if (commandLine.hasOption('L')) {
System.out.println(System.getProperty("java.library.path"));
System.exit(0);
}
Options o = new Options();
o.setMaxSize(0);
o.setBitRate(1000000);
o.setTunnelForward(true);
o.setCrop(null);
o.setSendFrameMeta(true);
o.setControl(true);
// global
o.setMaxFps(24);
o.setQuality(50);
o.setScale(2);
o.setScale(480);
// jpeg
o.setQuality(60);
o.setBitRate(1000000);
o.setSendFrameMeta(true);
// control
o.setControlOnly(false);
// dump
o.setDumpHierarchy(false);
if (commandLine.hasOption('Q')) {
int i = 0;
try {
@ -178,6 +217,12 @@ public final class Server {
o.setScale(i);
}
}
if (commandLine.hasOption('c')) {
o.setControlOnly(true);
}
if (commandLine.hasOption('D')) {
o.setDumpHierarchy(true);
}
return o;
}
@ -209,14 +254,14 @@ public final class Server {
@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");
}
}
// if (e instanceof MediaCodec.CodecException) {//api level 21
// 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");
// }
// }
}
}
@ -231,10 +276,11 @@ public final class Server {
// unlinkSelf();
// Options options = createOptions(args);
Options options = customOptions(args);
Ln.i("Options frame rate: " + options.getMaxFps() + " (1 ~ 100)");
final Options options = customOptions(args);
Ln.i("Options frame rate: " + options.getMaxFps() + " (1 ~ 60)");
Ln.i("Options quality: " + options.getQuality() + " (1 ~ 100)");
Ln.i("Options scale: " + options.getScale() + " (1,2,4...)");
Ln.i("Options projection: " + options.getScale() + " (1080, 720, 480, 360...)");
Ln.i("Options control only: " + options.getControlOnly() + " (true / false)");
scrcpy(options);
}
}

View file

@ -38,7 +38,8 @@ public final class Size {
return false;
}
Size size = (Size) o;
return width == size.width && height == size.height;
// return width == size.width && height == size.height;
return width == size.width || height == size.height;
}
@Override