mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-02-16 11:24:27 +01:00
There is no good reason to pass some of them in the constructor and some others as parameters of the streamScreen() method.
314 lines
13 KiB
Java
314 lines
13 KiB
Java
package com.genymobile.scrcpy;
|
|
|
|
import android.graphics.Rect;
|
|
import android.os.BatteryManager;
|
|
import android.os.Build;
|
|
|
|
import java.io.IOException;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
|
|
public final class Server {
|
|
|
|
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.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
|
final Device device = new Device(options);
|
|
List<CodecOption> codecOptions = options.getCodecOptions();
|
|
|
|
Thread initThread = startInitThread(options);
|
|
|
|
int scid = options.getScid();
|
|
boolean tunnelForward = options.isTunnelForward();
|
|
boolean control = options.getControl();
|
|
boolean sendDummyByte = options.getSendDummyByte();
|
|
|
|
Workarounds.prepareMainLooper();
|
|
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
|
// Workarounds must be applied for Meizu phones:
|
|
// - <https://github.com/Genymobile/scrcpy/issues/240>
|
|
// - <https://github.com/Genymobile/scrcpy/issues/365>
|
|
// - <https://github.com/Genymobile/scrcpy/issues/2656>
|
|
//
|
|
// But only apply when strictly necessary, since workarounds can cause other issues:
|
|
// - <https://github.com/Genymobile/scrcpy/issues/940>
|
|
// - <https://github.com/Genymobile/scrcpy/issues/994>
|
|
Workarounds.fillAppInfo();
|
|
}
|
|
|
|
Controller controller = null;
|
|
|
|
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, control, sendDummyByte)) {
|
|
VideoCodec codec = options.getCodec();
|
|
if (options.getSendDeviceMeta()) {
|
|
Size videoSize = device.getScreenInfo().getVideoSize();
|
|
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
|
}
|
|
|
|
if (control) {
|
|
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
|
controller.start();
|
|
|
|
final Controller controllerRef = controller;
|
|
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
|
|
}
|
|
|
|
VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), codec, options.getSendCodecId(), options.getSendFrameMeta());
|
|
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getBitRate(), options.getMaxFps(), codecOptions,
|
|
options.getEncoderName(), options.getDownsizeOnError());
|
|
try {
|
|
// synchronous
|
|
screenEncoder.streamScreen();
|
|
} catch (IOException e) {
|
|
// Broken pipe is expected on close, because the socket is closed by the client
|
|
if (!IO.isBrokenPipe(e)) {
|
|
Ln.e("Video encoding error", e);
|
|
}
|
|
}
|
|
} finally {
|
|
Ln.d("Screen streaming stopped");
|
|
initThread.interrupt();
|
|
if (controller != null) {
|
|
controller.stop();
|
|
}
|
|
|
|
try {
|
|
initThread.join();
|
|
if (controller != null) {
|
|
controller.join();
|
|
}
|
|
} catch (InterruptedException e) {
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Thread startInitThread(final Options options) {
|
|
Thread thread = new Thread(() -> initAndCleanUp(options));
|
|
thread.start();
|
|
return thread;
|
|
}
|
|
|
|
private static Options createOptions(String... args) {
|
|
if (args.length < 1) {
|
|
throw new IllegalArgumentException("Missing client version");
|
|
}
|
|
|
|
String clientVersion = args[0];
|
|
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
|
|
throw new IllegalArgumentException(
|
|
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
|
}
|
|
|
|
Options options = new Options();
|
|
|
|
for (int i = 1; i < args.length; ++i) {
|
|
String arg = args[i];
|
|
int equalIndex = arg.indexOf('=');
|
|
if (equalIndex == -1) {
|
|
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
|
|
}
|
|
String key = arg.substring(0, equalIndex);
|
|
String value = arg.substring(equalIndex + 1);
|
|
switch (key) {
|
|
case "scid":
|
|
int scid = Integer.parseInt(value, 0x10);
|
|
if (scid < -1) {
|
|
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
|
|
}
|
|
options.setScid(scid);
|
|
break;
|
|
case "log_level":
|
|
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
|
options.setLogLevel(level);
|
|
break;
|
|
case "codec":
|
|
VideoCodec codec = VideoCodec.findByName(value);
|
|
if (codec == null) {
|
|
throw new IllegalArgumentException("Video codec " + value + " not supported");
|
|
}
|
|
options.setCodec(codec);
|
|
break;
|
|
case "max_size":
|
|
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
|
options.setMaxSize(maxSize);
|
|
break;
|
|
case "bit_rate":
|
|
int bitRate = Integer.parseInt(value);
|
|
options.setBitRate(bitRate);
|
|
break;
|
|
case "max_fps":
|
|
int maxFps = Integer.parseInt(value);
|
|
options.setMaxFps(maxFps);
|
|
break;
|
|
case "lock_video_orientation":
|
|
int lockVideoOrientation = Integer.parseInt(value);
|
|
options.setLockVideoOrientation(lockVideoOrientation);
|
|
break;
|
|
case "tunnel_forward":
|
|
boolean tunnelForward = Boolean.parseBoolean(value);
|
|
options.setTunnelForward(tunnelForward);
|
|
break;
|
|
case "crop":
|
|
Rect crop = parseCrop(value);
|
|
options.setCrop(crop);
|
|
break;
|
|
case "control":
|
|
boolean control = Boolean.parseBoolean(value);
|
|
options.setControl(control);
|
|
break;
|
|
case "display_id":
|
|
int displayId = Integer.parseInt(value);
|
|
options.setDisplayId(displayId);
|
|
break;
|
|
case "show_touches":
|
|
boolean showTouches = Boolean.parseBoolean(value);
|
|
options.setShowTouches(showTouches);
|
|
break;
|
|
case "stay_awake":
|
|
boolean stayAwake = Boolean.parseBoolean(value);
|
|
options.setStayAwake(stayAwake);
|
|
break;
|
|
case "codec_options":
|
|
List<CodecOption> codecOptions = CodecOption.parse(value);
|
|
options.setCodecOptions(codecOptions);
|
|
break;
|
|
case "encoder_name":
|
|
if (!value.isEmpty()) {
|
|
options.setEncoderName(value);
|
|
}
|
|
break;
|
|
case "power_off_on_close":
|
|
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
|
|
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
|
|
break;
|
|
case "clipboard_autosync":
|
|
boolean clipboardAutosync = Boolean.parseBoolean(value);
|
|
options.setClipboardAutosync(clipboardAutosync);
|
|
break;
|
|
case "downsize_on_error":
|
|
boolean downsizeOnError = Boolean.parseBoolean(value);
|
|
options.setDownsizeOnError(downsizeOnError);
|
|
break;
|
|
case "cleanup":
|
|
boolean cleanup = Boolean.parseBoolean(value);
|
|
options.setCleanup(cleanup);
|
|
break;
|
|
case "power_on":
|
|
boolean powerOn = Boolean.parseBoolean(value);
|
|
options.setPowerOn(powerOn);
|
|
break;
|
|
case "send_device_meta":
|
|
boolean sendDeviceMeta = Boolean.parseBoolean(value);
|
|
options.setSendDeviceMeta(sendDeviceMeta);
|
|
break;
|
|
case "send_frame_meta":
|
|
boolean sendFrameMeta = Boolean.parseBoolean(value);
|
|
options.setSendFrameMeta(sendFrameMeta);
|
|
break;
|
|
case "send_dummy_byte":
|
|
boolean sendDummyByte = Boolean.parseBoolean(value);
|
|
options.setSendDummyByte(sendDummyByte);
|
|
break;
|
|
case "send_codec_id":
|
|
boolean sendCodecId = Boolean.parseBoolean(value);
|
|
options.setSendCodecId(sendCodecId);
|
|
break;
|
|
case "raw_video_stream":
|
|
boolean rawVideoStream = Boolean.parseBoolean(value);
|
|
if (rawVideoStream) {
|
|
options.setSendDeviceMeta(false);
|
|
options.setSendFrameMeta(false);
|
|
options.setSendDummyByte(false);
|
|
options.setSendCodecId(false);
|
|
}
|
|
break;
|
|
default:
|
|
Ln.w("Unknown server option: " + key);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
private static Rect parseCrop(String crop) {
|
|
if (crop.isEmpty()) {
|
|
return null;
|
|
}
|
|
// input format: "width:height:x:y"
|
|
String[] tokens = crop.split(":");
|
|
if (tokens.length != 4) {
|
|
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
|
|
}
|
|
int width = Integer.parseInt(tokens[0]);
|
|
int height = Integer.parseInt(tokens[1]);
|
|
int x = Integer.parseInt(tokens[2]);
|
|
int y = Integer.parseInt(tokens[3]);
|
|
return new Rect(x, y, x + width, y + height);
|
|
}
|
|
|
|
public static void main(String... args) throws Exception {
|
|
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
|
Ln.e("Exception on thread " + t, e);
|
|
});
|
|
|
|
Options options = createOptions(args);
|
|
|
|
Ln.initLogLevel(options.getLogLevel());
|
|
|
|
try {
|
|
scrcpy(options);
|
|
} catch (ConfigurationException e) {
|
|
// Do not print stack trace, a user-friendly error-message has already been logged
|
|
}
|
|
}
|
|
}
|