mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-03-28 23:14:29 +01:00
The stream metadata will contain both: - the codec ID at the start of the stream - the session metadata (video width and height) at the start of each "session" (typically on rotation) PR #6159 <https://github.com/Genymobile/scrcpy/pull/6159>
293 lines
11 KiB
Java
293 lines
11 KiB
Java
package com.genymobile.scrcpy;
|
|
|
|
import com.genymobile.scrcpy.audio.AudioCapture;
|
|
import com.genymobile.scrcpy.audio.AudioCodec;
|
|
import com.genymobile.scrcpy.audio.AudioDirectCapture;
|
|
import com.genymobile.scrcpy.audio.AudioEncoder;
|
|
import com.genymobile.scrcpy.audio.AudioPlaybackCapture;
|
|
import com.genymobile.scrcpy.audio.AudioRawRecorder;
|
|
import com.genymobile.scrcpy.audio.AudioSource;
|
|
import com.genymobile.scrcpy.control.ControlChannel;
|
|
import com.genymobile.scrcpy.control.Controller;
|
|
import com.genymobile.scrcpy.device.ConfigurationException;
|
|
import com.genymobile.scrcpy.device.DesktopConnection;
|
|
import com.genymobile.scrcpy.device.Device;
|
|
import com.genymobile.scrcpy.device.NewDisplay;
|
|
import com.genymobile.scrcpy.device.Streamer;
|
|
import com.genymobile.scrcpy.opengl.OpenGLRunner;
|
|
import com.genymobile.scrcpy.util.Ln;
|
|
import com.genymobile.scrcpy.util.LogUtils;
|
|
import com.genymobile.scrcpy.video.CameraCapture;
|
|
import com.genymobile.scrcpy.video.NewDisplayCapture;
|
|
import com.genymobile.scrcpy.video.ScreenCapture;
|
|
import com.genymobile.scrcpy.video.SurfaceCapture;
|
|
import com.genymobile.scrcpy.video.SurfaceEncoder;
|
|
import com.genymobile.scrcpy.video.VideoSource;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.os.Build;
|
|
import android.os.Looper;
|
|
import android.system.Os;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.lang.reflect.Field;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
public final class Server {
|
|
|
|
public static final String SERVER_PATH;
|
|
|
|
static {
|
|
String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator);
|
|
// By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath
|
|
SERVER_PATH = classPaths[0];
|
|
}
|
|
|
|
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) {
|
|
Looper.getMainLooper().quitSafely();
|
|
}
|
|
}
|
|
}
|
|
|
|
private Server() {
|
|
// not instantiable
|
|
}
|
|
|
|
private static void scrcpy(Options options) throws IOException, ConfigurationException {
|
|
if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) {
|
|
Ln.e("Camera mirroring is not supported before Android 12");
|
|
throw new ConfigurationException("Camera mirroring is not supported");
|
|
}
|
|
|
|
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
|
if (options.getNewDisplay() != null) {
|
|
Ln.e("New virtual display is not supported before Android 10");
|
|
throw new ConfigurationException("New virtual display is not supported");
|
|
}
|
|
if (options.getDisplayImePolicy() != -1) {
|
|
Ln.e("Display IME policy is not supported before Android 10");
|
|
throw new ConfigurationException("Display IME policy is not supported");
|
|
}
|
|
}
|
|
|
|
CleanUp cleanUp = null;
|
|
|
|
if (options.getCleanup()) {
|
|
cleanUp = CleanUp.start(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();
|
|
|
|
Workarounds.apply();
|
|
|
|
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
|
|
|
|
DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, video, audio, control, sendDummyByte);
|
|
try {
|
|
if (options.getSendDeviceMeta()) {
|
|
connection.sendDeviceMeta(Device.getDeviceName());
|
|
}
|
|
|
|
Controller controller = null;
|
|
|
|
if (control) {
|
|
ControlChannel controlChannel = connection.getControlChannel();
|
|
controller = new Controller(controlChannel, cleanUp, options);
|
|
asyncProcessors.add(controller);
|
|
}
|
|
|
|
if (audio) {
|
|
AudioCodec audioCodec = options.getAudioCodec();
|
|
AudioSource audioSource = options.getAudioSource();
|
|
AudioCapture audioCapture;
|
|
if (audioSource.isDirect()) {
|
|
audioCapture = new AudioDirectCapture(audioSource);
|
|
} else {
|
|
audioCapture = new AudioPlaybackCapture(options.getAudioDup());
|
|
}
|
|
|
|
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendStreamMeta(), options.getSendFrameMeta());
|
|
AsyncProcessor audioRecorder;
|
|
if (audioCodec == AudioCodec.RAW) {
|
|
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
|
|
} else {
|
|
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options);
|
|
}
|
|
asyncProcessors.add(audioRecorder);
|
|
}
|
|
|
|
if (video) {
|
|
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendStreamMeta(),
|
|
options.getSendFrameMeta());
|
|
SurfaceCapture surfaceCapture;
|
|
if (options.getVideoSource() == VideoSource.DISPLAY) {
|
|
NewDisplay newDisplay = options.getNewDisplay();
|
|
if (newDisplay != null) {
|
|
surfaceCapture = new NewDisplayCapture(controller, options);
|
|
} else {
|
|
assert options.getDisplayId() != Device.DISPLAY_ID_NONE;
|
|
surfaceCapture = new ScreenCapture(controller, options);
|
|
}
|
|
} else {
|
|
surfaceCapture = new CameraCapture(options);
|
|
}
|
|
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options);
|
|
asyncProcessors.add(surfaceEncoder);
|
|
|
|
if (controller != null) {
|
|
controller.setSurfaceCapture(surfaceCapture);
|
|
}
|
|
}
|
|
|
|
Completion completion = new Completion(asyncProcessors.size());
|
|
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
|
asyncProcessor.start((fatalError) -> {
|
|
completion.addCompleted(fatalError);
|
|
});
|
|
}
|
|
|
|
Looper.loop(); // interrupted by the Completion implementation
|
|
} finally {
|
|
if (cleanUp != null) {
|
|
cleanUp.interrupt();
|
|
}
|
|
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
|
asyncProcessor.stop();
|
|
}
|
|
|
|
OpenGLRunner.quit(); // quit the OpenGL thread, if any
|
|
|
|
connection.shutdown();
|
|
|
|
try {
|
|
if (cleanUp != null) {
|
|
cleanUp.join();
|
|
}
|
|
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
|
asyncProcessor.join();
|
|
}
|
|
OpenGLRunner.join();
|
|
} catch (InterruptedException e) {
|
|
// ignore
|
|
}
|
|
|
|
connection.close();
|
|
}
|
|
}
|
|
|
|
private static void prepareMainLooper() {
|
|
// Like Looper.prepareMainLooper(), but with quitAllowed set to true
|
|
Looper.prepare();
|
|
synchronized (Looper.class) {
|
|
try {
|
|
@SuppressLint("DiscouragedPrivateApi")
|
|
Field field = Looper.class.getDeclaredField("sMainLooper");
|
|
field.setAccessible(true);
|
|
field.set(null, Looper.myLooper());
|
|
} catch (ReflectiveOperationException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void main(String... args) {
|
|
int status = 0;
|
|
try {
|
|
internalMain(args);
|
|
} catch (Throwable t) {
|
|
Ln.e(t.getMessage(), t);
|
|
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.UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
|
|
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
|
Ln.e("Exception on thread " + t, e);
|
|
if (defaultHandler != null) {
|
|
defaultHandler.uncaughtException(t, e);
|
|
}
|
|
});
|
|
|
|
dropRootPrivileges();
|
|
|
|
prepareMainLooper();
|
|
|
|
Options options = Options.parse(args);
|
|
|
|
Ln.disableSystemStreams();
|
|
Ln.initLogLevel(options.getLogLevel());
|
|
|
|
Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
|
|
|
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();
|
|
Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes()));
|
|
}
|
|
if (options.getListApps()) {
|
|
Workarounds.apply();
|
|
Ln.i("Processing Android apps... (this may take some time)");
|
|
Ln.i(LogUtils.buildAppListMessage());
|
|
}
|
|
// 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
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("deprecation")
|
|
private static void dropRootPrivileges() {
|
|
try {
|
|
if (Os.getuid() == 0) {
|
|
// Copy-paste does not work with root user
|
|
// <https://github.com/Genymobile/scrcpy/issues/6224>
|
|
Os.setuid(2000);
|
|
}
|
|
} catch (Exception e) {
|
|
Ln.w("Cannot set UID", e);
|
|
}
|
|
}
|
|
}
|