Files
scrcpy/server/src/main/java/com/genymobile/scrcpy/Server.java
Romain Vimont 1015b42e53 Rename "codec meta" to "stream meta"
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>
2026-01-10 11:53:22 +01:00

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);
}
}
}