diff --git a/scrcpy-server.jar b/scrcpy-server.jar index 4fd3129a..8ffbdc19 100644 Binary files a/scrcpy-server.jar and b/scrcpy-server.jar differ diff --git a/server/build.gradle b/server/build.gradle index 299f0e60..4aa6111d 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -4,7 +4,7 @@ android { compileSdkVersion 29 defaultConfig { applicationId "com.genymobile.scrcpy" - minSdkVersion 21 + minSdkVersion 19 targetSdkVersion 29 versionCode 14 versionName "1.12.1" diff --git a/server/src/main/java/com/genymobile/scrcpy/AccessibilityNodeInfoDumper.java b/server/src/main/java/com/genymobile/scrcpy/AccessibilityNodeInfoDumper.java new file mode 100644 index 00000000..8c5be4e9 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AccessibilityNodeInfoDumper.java @@ -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 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(); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AccessibilityNodeInfoHelper.java b/server/src/main/java/com/genymobile/scrcpy/AccessibilityNodeInfoHelper.java new file mode 100644 index 00000000..3daea9df --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AccessibilityNodeInfoHelper.java @@ -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; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Common.java b/server/src/main/java/com/genymobile/scrcpy/Common.java new file mode 100644 index 00000000..7717b598 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Common.java @@ -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) { + + } + } +} \ No newline at end of file diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 4956c9bd..7ed4df00 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -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); diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index f7ec577a..3295c57a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -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) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 9448098a..98ee3ee5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -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(); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/IO.java b/server/src/main/java/com/genymobile/scrcpy/IO.java index 57c017db..da4098d2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/IO.java @@ -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 . 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)); } + } diff --git a/server/src/main/java/com/genymobile/scrcpy/JpegEncoder.java b/server/src/main/java/com/genymobile/scrcpy/JpegEncoder.java index c617f8fc..bc2668fe 100644 --- a/server/src/main/java/com/genymobile/scrcpy/JpegEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/JpegEncoder.java @@ -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(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 954af7ab..371b47ec 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -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; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 1b8d6d5d..67c1ee85 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -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; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 90199238..0fa73091 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -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 : Frame rate (frames/sec).\n" + + " -P : Display projection (1080, 720, 480, 360...).\n" + " -Q : JPEG quality (0-100).\n" - + " -r : Frame rate (frames/s).\n" - + " -P : 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); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Size.java b/server/src/main/java/com/genymobile/scrcpy/Size.java index fd4b6971..37f50f63 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/Size.java @@ -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