Initial commit

Start a new clean history from here.
This commit is contained in:
Romain Vimont 2017-12-12 15:12:07 +01:00
commit 54d9148a36
29 changed files with 1537 additions and 0 deletions

View file

@ -0,0 +1,25 @@
/* //device/java/android/android/hardware/ISensorListener.aidl
**
** Copyright 2008, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
package android.view;
/**
* {@hide}
*/
oneway interface IRotationWatcher {
void onRotationChanged(int rotation);
}

View file

@ -0,0 +1,52 @@
package com.genymobile.scrcpy;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import java.io.Closeable;
import java.io.IOException;
public class DesktopConnection implements Closeable {
private static final String SOCKET_NAME = "scrcpy";
private final LocalSocket socket;
private DesktopConnection(LocalSocket socket) throws IOException {
this.socket = socket;
}
private static LocalSocket connect(String abstractName) throws IOException {
LocalSocket localSocket = new LocalSocket();
localSocket.connect(new LocalSocketAddress(abstractName));
return localSocket;
}
public static DesktopConnection open(int width, int height) throws IOException {
LocalSocket socket = connect(SOCKET_NAME);
send(socket, width, height);
return new DesktopConnection(socket);
}
public void close() throws IOException {
socket.shutdownInput();
socket.shutdownOutput();
socket.close();
}
private static void send(LocalSocket socket, int width, int height) throws IOException {
assert width < 0x10000 : "width may not be stored on 16 bits";
assert height < 0x10000 : "height may not be stored on 16 bits";
byte[] buffer = new byte[4];
buffer[0] = (byte) (width >> 8);
buffer[1] = (byte) width;
buffer[2] = (byte) (height >> 8);
buffer[3] = (byte) height;
socket.getOutputStream().write(buffer, 0, 4);
}
public void sendVideoStream(byte[] videoStreamBuffer, int len) throws IOException {
socket.getOutputStream().write(videoStreamBuffer, 0, len);
}
}

View file

@ -0,0 +1,29 @@
package com.genymobile.scrcpy;
import java.io.IOException;
public class ScrCpyServer {
public static void scrcpy() throws IOException {
ScreenInfo initialScreenInfo = ScreenUtil.getScreenInfo();
int width = initialScreenInfo.getLogicalWidth();
int height = initialScreenInfo.getLogicalHeight();
try (DesktopConnection connection = DesktopConnection.open(width, height)) {
try {
new ScreenStreamer(connection).streamScreen();
} catch (IOException e) {
System.err.println("Screen streaming interrupted: " + e.getMessage());
System.err.flush();
}
}
}
public static void main(String... args) throws Exception {
try {
scrcpy();
} catch (Throwable t) {
t.printStackTrace();
throw t;
}
}
}

View file

@ -0,0 +1,26 @@
package com.genymobile.scrcpy;
public class ScreenInfo {
private final int width;
private final int height;
private int rotation;
public ScreenInfo(int width, int height, int rotation) {
this.width = width;
this.height = height;
this.rotation = rotation;
}
public ScreenInfo withRotation(int rotation) {
return new ScreenInfo(width, height, rotation);
}
public int getLogicalWidth() {
return (rotation & 1) == 0 ? width : height;
}
public int getLogicalHeight() {
return (rotation & 1) == 0 ? height : width;
}
}

View file

@ -0,0 +1,46 @@
package com.genymobile.scrcpy;
import android.os.RemoteException;
import android.view.IRotationWatcher;
import java.io.IOException;
import java.io.InterruptedIOException;
public class ScreenStreamer {
private final DesktopConnection connection;
private ScreenStreamerSession currentStreamer; // protected by 'this'
public ScreenStreamer(DesktopConnection connection) {
this.connection = connection;
ScreenUtil.registerRotationWatcher(new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) throws RemoteException {
reset();
}
});
}
private synchronized ScreenStreamerSession newScreenStreamerSession() {
currentStreamer = new ScreenStreamerSession(connection);
return currentStreamer;
}
public void streamScreen() throws IOException {
while (true) {
try {
ScreenStreamerSession screenStreamer = newScreenStreamerSession();
screenStreamer.streamScreen();
} catch (InterruptedIOException e) {
// the current screenrecord process has probably been killed due to reset(), start a new one without failing
}
}
}
public synchronized void reset() {
if (currentStreamer != null) {
// it will stop the blocking call to streamScreen(), so a new streamer will be started
currentStreamer.stop();
}
}
}

View file

@ -0,0 +1,62 @@
package com.genymobile.scrcpy;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenStreamerSession {
private final DesktopConnection connection;
private Process screenRecordProcess; // protected by 'this'
private final AtomicBoolean stopped = new AtomicBoolean();
private final byte[] buffer = new byte[0x10000];
public ScreenStreamerSession(DesktopConnection connection) {
this.connection = connection;
}
public void streamScreen() throws IOException {
// screenrecord may not record more than 3 minutes, so restart it on EOF
while (!stopped.get() && streamScreenOnce()) ;
}
/**
* Starts screenrecord once and relay its output to the desktop connection.
*
* @return {@code true} if EOF is reached, {@code false} otherwise (i.e. requested to stop).
* @throws IOException if an I/O error occurred
*/
private boolean streamScreenOnce() throws IOException {
Process process = startScreenRecord();
setCurrentProcess(process);
InputStream inputStream = process.getInputStream();
int r;
while ((r = inputStream.read(buffer)) != -1 && !stopped.get()) {
connection.sendVideoStream(buffer, r);
}
return r != -1;
}
public void stop() {
// let the thread stop itself without breaking the video stream
stopped.set(true);
killCurrentProcess();
}
private static Process startScreenRecord() throws IOException {
Process process = new ProcessBuilder("screenrecord", "--output-format=h264", "-").start();
process.getOutputStream().close();
return process;
}
private synchronized void setCurrentProcess(Process screenRecordProcess) {
this.screenRecordProcess = screenRecordProcess;
}
private synchronized void killCurrentProcess() {
if (screenRecordProcess != null) {
screenRecordProcess.destroy();
screenRecordProcess = null;
}
}
}

View file

@ -0,0 +1,110 @@
package com.genymobile.scrcpy;
import android.os.IBinder;
import android.os.IInterface;
import android.view.IRotationWatcher;
import java.lang.reflect.Method;
public class ScreenUtil {
private static final ServiceManager serviceManager = new ServiceManager();
public static ScreenInfo getScreenInfo() {
return serviceManager.getDisplayManager().getScreenInfo();
}
public static void registerRotationWatcher(IRotationWatcher rotationWatcher) {
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher);
}
private static class ServiceManager {
private Method getServiceMethod;
public ServiceManager() {
try {
getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
} catch (Exception e) {
throw new AssertionError(e);
}
}
private IInterface getService(String service, String type) {
try {
IBinder binder = (IBinder) getServiceMethod.invoke(null, service);
Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class);
return (IInterface) asInterfaceMethod.invoke(null, binder);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public WindowManager getWindowManager() {
return new WindowManager(getService("window", "android.view.IWindowManager"));
}
public DisplayManager getDisplayManager() {
return new DisplayManager(getService("display", "android.hardware.display.IDisplayManager"));
}
}
private static class WindowManager {
private IInterface manager;
public WindowManager(IInterface manager) {
this.manager = manager;
}
public int getRotation() {
try {
Class<?> cls = manager.getClass();
try {
return (Integer) manager.getClass().getMethod("getRotation").invoke(manager);
} catch (NoSuchMethodException e) {
// method changed since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager);
}
} catch (Exception e) {
throw new AssertionError(e);
}
}
public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
try {
Class<?> cls = manager.getClass();
try {
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
} catch (NoSuchMethodException e) {
// display parameter added since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
}
} catch (Exception e) {
throw new AssertionError(e);
}
}
}
private static class DisplayManager {
private IInterface manager;
public DisplayManager(IInterface manager) {
this.manager = manager;
}
public ScreenInfo getScreenInfo() {
try {
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0);
Class<?> cls = displayInfo.getClass();
// width and height do not depend on the rotation
int width = (Integer) cls.getMethod("getNaturalWidth").invoke(displayInfo);
int height = (Integer) cls.getMethod("getNaturalHeight").invoke(displayInfo);
int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
return new ScreenInfo(width, height, rotation);
} catch (Exception e) {
throw new AssertionError(e);
}
}
}
}