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 = options.getVideoSource() == VideoSource.CAMERA; Workarounds.apply(audio, camera); List 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(), options.getCameraFps(), options.getCameraHighSpeed()); } 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) { int status = 0; try { internalMain(args); } catch (Throwable t) { t.printStackTrace(); status = 1; } finally { // By default, the Java process exits when all non-daemon threads are terminated. // The Android SDK might start some non-daemon threads internally, preventing the scrcpy server to exit. // So force the process to exit explicitly. System.exit(status); } } private static void internalMain(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 } } }