mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-08-12 19:19:04 +00:00
Initial commit
Start a new clean history from here.
This commit is contained in:
commit
54d9148a36
29 changed files with 1537 additions and 0 deletions
25
server/src/android/view/IRotationWatcher.aidl
Normal file
25
server/src/android/view/IRotationWatcher.aidl
Normal 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);
|
||||
}
|
52
server/src/com/genymobile/scrcpy/DesktopConnection.java
Normal file
52
server/src/com/genymobile/scrcpy/DesktopConnection.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
29
server/src/com/genymobile/scrcpy/ScrCpyServer.java
Normal file
29
server/src/com/genymobile/scrcpy/ScrCpyServer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
26
server/src/com/genymobile/scrcpy/ScreenInfo.java
Normal file
26
server/src/com/genymobile/scrcpy/ScreenInfo.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
46
server/src/com/genymobile/scrcpy/ScreenStreamer.java
Normal file
46
server/src/com/genymobile/scrcpy/ScreenStreamer.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
62
server/src/com/genymobile/scrcpy/ScreenStreamerSession.java
Normal file
62
server/src/com/genymobile/scrcpy/ScreenStreamerSession.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
110
server/src/com/genymobile/scrcpy/ScreenUtil.java
Normal file
110
server/src/com/genymobile/scrcpy/ScreenUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue