mirror of https://github.com/Genymobile/scrcpy
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
8.5 KiB
Java
223 lines
8.5 KiB
Java
package com.genymobile.scrcpy;
|
|
|
|
import android.os.BatteryManager;
|
|
import android.os.Build;
|
|
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
public final class Server {
|
|
|
|
private static class Completion {
|
|
private int running;
|
|
private boolean fatalError;
|
|
|
|
Completion(int running) {
|
|
this.running = running;
|
|
}
|
|
|
|
synchronized void addCompleted(boolean fatalError) {
|
|
--running;
|
|
if (fatalError) {
|
|
this.fatalError = true;
|
|
}
|
|
if (running == 0 || this.fatalError) {
|
|
notify();
|
|
}
|
|
}
|
|
|
|
synchronized void await() {
|
|
try {
|
|
while (running > 0 && !fatalError) {
|
|
wait();
|
|
}
|
|
} catch (InterruptedException e) {
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
private Server() {
|
|
// not instantiable
|
|
}
|
|
|
|
private static void initAndCleanUp(Options options) {
|
|
boolean mustDisableShowTouchesOnCleanUp = false;
|
|
int restoreStayOn = -1;
|
|
boolean restoreNormalPowerMode = options.getControl(); // only restore power mode if control is enabled
|
|
if (options.getShowTouches() || options.getStayAwake()) {
|
|
if (options.getShowTouches()) {
|
|
try {
|
|
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
|
|
// If "show touches" was disabled, it must be disabled back on clean up
|
|
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
|
|
} catch (SettingsException e) {
|
|
Ln.e("Could not change \"show_touches\"", e);
|
|
}
|
|
}
|
|
|
|
if (options.getStayAwake()) {
|
|
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
|
try {
|
|
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
|
try {
|
|
restoreStayOn = Integer.parseInt(oldValue);
|
|
if (restoreStayOn == stayOn) {
|
|
// No need to restore
|
|
restoreStayOn = -1;
|
|
}
|
|
} catch (NumberFormatException e) {
|
|
restoreStayOn = 0;
|
|
}
|
|
} catch (SettingsException e) {
|
|
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.getCleanup()) {
|
|
try {
|
|
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, restoreNormalPowerMode,
|
|
options.getPowerOffScreenOnClose());
|
|
} catch (IOException e) {
|
|
Ln.e("Could not configure cleanup", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void scrcpy(Options options) throws IOException, ConfigurationException {
|
|
Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && options.getVideoSource() == VideoSource.CAMERA) {
|
|
Ln.e("Camera mirroring is not supported before Android 12");
|
|
throw new ConfigurationException("Camera mirroring is not supported");
|
|
}
|
|
|
|
final Device device = new Device(options);
|
|
|
|
Thread initThread = startInitThread(options);
|
|
|
|
int scid = options.getScid();
|
|
boolean tunnelForward = options.isTunnelForward();
|
|
boolean control = options.getControl();
|
|
boolean video = options.getVideo();
|
|
boolean audio = options.getAudio();
|
|
boolean sendDummyByte = options.getSendDummyByte();
|
|
|
|
boolean camera = true;
|
|
Workarounds.apply(audio, camera);
|
|
|
|
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
|
|
|
|
DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, video, audio, control, sendDummyByte);
|
|
try {
|
|
if (options.getSendDeviceMeta()) {
|
|
connection.sendDeviceMeta(Device.getDeviceName());
|
|
}
|
|
|
|
if (control) {
|
|
Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
|
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
|
|
asyncProcessors.add(controller);
|
|
}
|
|
|
|
if (audio) {
|
|
AudioCodec audioCodec = options.getAudioCodec();
|
|
AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
|
|
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
|
|
AsyncProcessor audioRecorder;
|
|
if (audioCodec == AudioCodec.RAW) {
|
|
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
|
|
} else {
|
|
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
|
|
options.getAudioEncoder());
|
|
}
|
|
asyncProcessors.add(audioRecorder);
|
|
}
|
|
|
|
if (video) {
|
|
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
|
|
options.getSendFrameMeta());
|
|
SurfaceCapture surfaceCapture;
|
|
if (options.getVideoSource() == VideoSource.DISPLAY) {
|
|
surfaceCapture = new ScreenCapture(device);
|
|
} else {
|
|
surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
|
|
options.getMaxSize(), options.getCameraAspectRatio());
|
|
}
|
|
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
|
|
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
|
|
asyncProcessors.add(surfaceEncoder);
|
|
}
|
|
|
|
Completion completion = new Completion(asyncProcessors.size());
|
|
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
|
asyncProcessor.start((fatalError) -> {
|
|
completion.addCompleted(fatalError);
|
|
});
|
|
}
|
|
|
|
completion.await();
|
|
} finally {
|
|
initThread.interrupt();
|
|
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
|
asyncProcessor.stop();
|
|
}
|
|
|
|
try {
|
|
initThread.join();
|
|
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
|
asyncProcessor.join();
|
|
}
|
|
} catch (InterruptedException e) {
|
|
// ignore
|
|
}
|
|
|
|
connection.close();
|
|
}
|
|
}
|
|
|
|
private static Thread startInitThread(final Options options) {
|
|
Thread thread = new Thread(() -> initAndCleanUp(options), "init-cleanup");
|
|
thread.start();
|
|
return thread;
|
|
}
|
|
|
|
public static void main(String... args) throws Exception {
|
|
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
|
Ln.e("Exception on thread " + t, e);
|
|
});
|
|
|
|
Options options = Options.parse(args);
|
|
|
|
Ln.initLogLevel(options.getLogLevel());
|
|
|
|
if (options.getList()) {
|
|
if (options.getCleanup()) {
|
|
CleanUp.unlinkSelf();
|
|
}
|
|
|
|
if (options.getListEncoders()) {
|
|
Ln.i(LogUtils.buildVideoEncoderListMessage());
|
|
Ln.i(LogUtils.buildAudioEncoderListMessage());
|
|
}
|
|
if (options.getListDisplays()) {
|
|
Ln.i(LogUtils.buildDisplayListMessage());
|
|
}
|
|
if (options.getListCameras() || options.getListCameraSizes()) {
|
|
Workarounds.apply(false, true);
|
|
Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes()));
|
|
}
|
|
// Just print the requested data, do not mirror
|
|
return;
|
|
}
|
|
|
|
try {
|
|
scrcpy(options);
|
|
} catch (ConfigurationException e) {
|
|
// Do not print stack trace, a user-friendly error-message has already been logged
|
|
}
|
|
}
|
|
}
|