Compare commits

..

2 Commits

Author SHA1 Message Date
Simon Chan
5bba202f01 Add --list-cameras
Add an option to list the device cameras.

Co-authored-by: Romain Vimont <rom@rom1v.com>
2023-08-22 20:15:32 +02:00
Simon Chan
de10be8153 Extract SurfaceCapture from ScreenEncoder
Extract an interface SurfaceCapture from ScreenEncoder, representing a
video source which can be rendered to a Surface for encoding.

Split ScreenEncoder into:
 - ScreenCapture, implementing SurfaceCapture to capture the device
   screen,
 - SurfaceEncoder, to encode any SurfaceCapture.

This separation prepares the introduction of another SurfaceCapture
implementation to capture the camera instead of the device screen.

Co-authored-by: Romain Vimont <rom@rom1v.com>
2023-08-22 20:10:06 +02:00
21 changed files with 294 additions and 182 deletions

View File

@@ -23,6 +23,7 @@ _scrcpy() {
--kill-adb-on-close --kill-adb-on-close
-K --hid-keyboard -K --hid-keyboard
--legacy-paste --legacy-paste
--list-cameras
--list-displays --list-displays
--list-encoders --list-encoders
--lock-video-orientation --lock-video-orientation
@@ -44,8 +45,6 @@ _scrcpy() {
--no-video-playback --no-video-playback
--otg --otg
-p --port= -p --port=
--pause-on-exit
--pause-on-exit=
--power-off-on-close --power-off-on-close
--prefer-text --prefer-text
--print-fps --print-fps
@@ -99,10 +98,6 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur"))
return return
;; ;;
--pause-on-exit)
COMPREPLY=($(compgen -W 'true false if-error' -- "$cur"))
return
;;
-r|--record) -r|--record)
COMPREPLY=($(compgen -f -- "$cur")) COMPREPLY=($(compgen -f -- "$cur"))
return return

View File

@@ -1,2 +1,4 @@
@echo off @echo off
scrcpy.exe --pause-on-exit=if-error %* scrcpy.exe %*
:: if the exit code is >= 1, then pause
if errorlevel 1 pause

View File

@@ -5,7 +5,7 @@ Comment=Display and control your Android device
# For some users, the PATH or ADB environment variables are set from the shell # For some users, the PATH or ADB environment variables are set from the shell
# startup file, like .bashrc or .zshrc… Run an interactive shell to get # startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized. # environment correctly initialized.
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy --pause-on-exit=if-error" Exec=/bin/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press Enter to quit...'"
Icon=scrcpy Icon=scrcpy
Terminal=true Terminal=true
Type=Application Type=Application

View File

@@ -30,6 +30,7 @@ arguments=(
'--kill-adb-on-close[Kill adb when scrcpy terminates]' '--kill-adb-on-close[Kill adb when scrcpy terminates]'
{-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
'--list-cameras[List cameras available on the device]'
'--list-displays[List displays available on the device]' '--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders available on the device]' '--list-encoders[List video and audio encoders available on the device]'
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)'
@@ -50,7 +51,6 @@ arguments=(
'--no-video-playback[Disable video playback]' '--no-video-playback[Disable video playback]'
'--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]'
{-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]'
'--pause-on-exit=[Make scrcpy pause before exiting]:mode:(true false if-error)'
'--power-off-on-close[Turn the device screen off when closing scrcpy]' '--power-off-on-close[Turn the device screen off when closing scrcpy]'
'--prefer-text[Inject alpha characters and space as text events instead of key events]' '--prefer-text[Inject alpha characters and space as text events instead of key events]'
'--print-fps[Start FPS counter, to print frame logs to the console]' '--print-fps[Start FPS counter, to print frame logs to the console]'

View File

@@ -155,6 +155,9 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.B \-\-list\-cameras
List cameras available on the device.
.TP .TP
.B \-\-list\-encoders .B \-\-list\-encoders
List video and audio encoders available on the device. List video and audio encoders available on the device.
@@ -267,16 +270,6 @@ Set the TCP port (range) used by the client to listen.
Default is 27183:27199. Default is 27183:27199.
.TP
\fB\-\-pause\-on\-exit\fR[=\fImode\fR]
Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occured).
This is useful to prevent the terminal window from automatically closing, so that error messages can be read.
Default is "false".
Passing the option without argument is equivalent to passing "true".
.TP .TP
.B \-\-power\-off\-on\-close .B \-\-power\-off\-on\-close
Turn the device screen off when closing scrcpy. Turn the device screen off when closing scrcpy.

View File

@@ -79,7 +79,7 @@ enum {
OPT_AUDIO_SOURCE, OPT_AUDIO_SOURCE,
OPT_KILL_ADB_ON_CLOSE, OPT_KILL_ADB_ON_CLOSE,
OPT_TIME_LIMIT, OPT_TIME_LIMIT,
OPT_PAUSE_ON_EXIT, OPT_LIST_CAMERAS,
}; };
struct sc_option { struct sc_option {
@@ -313,6 +313,11 @@ static const struct sc_option options[] = {
"This is a workaround for some devices not behaving as " "This is a workaround for some devices not behaving as "
"expected when setting the device clipboard programmatically.", "expected when setting the device clipboard programmatically.",
}, },
{
.longopt_id = OPT_LIST_CAMERAS,
.longopt = "list-cameras",
.text = "List device cameras.",
},
{ {
.longopt_id = OPT_LIST_DISPLAYS, .longopt_id = OPT_LIST_DISPLAYS,
.longopt = "list-displays", .longopt = "list-displays",
@@ -464,20 +469,6 @@ static const struct sc_option options[] = {
"Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":" "Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":"
STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".", STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".",
}, },
{
.longopt_id = OPT_PAUSE_ON_EXIT,
.longopt = "pause-on-exit",
.argdesc = "mode",
.optional_arg = true,
.text = "Configure pause on exit. Possible values are \"true\" (always "
"pause on exit), \"false\" (never pause on exit) and "
"\"if-error\" (pause only if an error occured).\n"
"This is useful to prevent the terminal window from "
"automatically closing, so that error messages can be read.\n"
"Default is \"false\".\n"
"Passing the option without argument is equivalent to passing "
"\"true\".",
},
{ {
.longopt_id = OPT_POWER_OFF_ON_CLOSE, .longopt_id = OPT_POWER_OFF_ON_CLOSE,
.longopt = "power-off-on-close", .longopt = "power-off-on-close",
@@ -1652,29 +1643,6 @@ parse_time_limit(const char *s, sc_tick *tick) {
return true; return true;
} }
static bool
parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) {
if (!s || !strcmp(s, "true")) {
*pause_on_exit = SC_PAUSE_ON_EXIT_TRUE;
return true;
}
if (!strcmp(s, "false")) {
*pause_on_exit = SC_PAUSE_ON_EXIT_FALSE;
return true;
}
if (!strcmp(s, "if-error")) {
*pause_on_exit = SC_PAUSE_ON_EXIT_IF_ERROR;
return true;
}
LOGE("Unsupported pause on exit mode: %s "
"(expected true, false or if-error)", optarg);
return false;
}
static bool static bool
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
const char *optstring, const struct option *longopts) { const char *optstring, const struct option *longopts) {
@@ -1982,6 +1950,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
"platform)."); "platform).");
return false; return false;
#endif #endif
case OPT_LIST_CAMERAS:
opts->list_cameras = true;
break;
case OPT_LIST_ENCODERS: case OPT_LIST_ENCODERS:
opts->list_encoders = true; opts->list_encoders = true;
break; break;
@@ -2015,11 +1986,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false; return false;
} }
break; break;
case OPT_PAUSE_ON_EXIT:
if (!parse_pause_on_exit(optarg, &args->pause_on_exit)) {
return false;
}
break;
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;
@@ -2233,37 +2199,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return true; return true;
} }
static enum sc_pause_on_exit
sc_get_pause_on_exit(int argc, char *argv[]) {
// Read arguments backwards so that the last --pause-on-exit is considered
// (same behavior as getopt())
for (int i = argc - 1; i >= 1; --i) {
const char *arg = argv[i];
// Starts with "--pause-on-exit"
if (!strncmp("--pause-on-exit", arg, 15)) {
if (arg[15] == '\0') {
// No argument
return SC_PAUSE_ON_EXIT_TRUE;
}
if (arg[15] != '=') {
// Invalid parameter, ignore
return SC_PAUSE_ON_EXIT_FALSE;
}
const char *value = &arg[16];
if (!strcmp(value, "true")) {
return SC_PAUSE_ON_EXIT_TRUE;
}
if (!strcmp(value, "if-error")) {
return SC_PAUSE_ON_EXIT_IF_ERROR;
}
// Set to false, inclusing when the value is invalid
return SC_PAUSE_ON_EXIT_FALSE;
}
}
return false;
}
bool bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
struct sc_getopt_adapter adapter; struct sc_getopt_adapter adapter;
@@ -2277,11 +2212,5 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
sc_getopt_adapter_destroy(&adapter); sc_getopt_adapter_destroy(&adapter);
if (!ret && args->pause_on_exit == SC_PAUSE_ON_EXIT_FALSE) {
// Check if "--pause-on-exit" is present in the arguments list, because
// it must be taken into account even if command line parsing failed
args->pause_on_exit = sc_get_pause_on_exit(argc, argv);
}
return ret; return ret;
} }

View File

@@ -7,17 +7,10 @@
#include "options.h" #include "options.h"
enum sc_pause_on_exit {
SC_PAUSE_ON_EXIT_TRUE,
SC_PAUSE_ON_EXIT_FALSE,
SC_PAUSE_ON_EXIT_IF_ERROR,
};
struct scrcpy_cli_args { struct scrcpy_cli_args {
struct scrcpy_options opts; struct scrcpy_options opts;
bool help; bool help;
bool version; bool version;
enum sc_pause_on_exit pause_on_exit;
}; };
void void

View File

@@ -39,32 +39,26 @@ main_scrcpy(int argc, char *argv[]) {
.opts = scrcpy_options_default, .opts = scrcpy_options_default,
.help = false, .help = false,
.version = false, .version = false,
.pause_on_exit = SC_PAUSE_ON_EXIT_FALSE,
}; };
#ifndef NDEBUG #ifndef NDEBUG
args.opts.log_level = SC_LOG_LEVEL_DEBUG; args.opts.log_level = SC_LOG_LEVEL_DEBUG;
#endif #endif
enum scrcpy_exit_code ret;
if (!scrcpy_parse_args(&args, argc, argv)) { if (!scrcpy_parse_args(&args, argc, argv)) {
ret = SCRCPY_EXIT_FAILURE; return SCRCPY_EXIT_FAILURE;
goto end;
} }
sc_set_log_level(args.opts.log_level); sc_set_log_level(args.opts.log_level);
if (args.help) { if (args.help) {
scrcpy_print_usage(argv[0]); scrcpy_print_usage(argv[0]);
ret = SCRCPY_EXIT_SUCCESS; return SCRCPY_EXIT_SUCCESS;
goto end;
} }
if (args.version) { if (args.version) {
scrcpy_print_version(); scrcpy_print_version();
ret = SCRCPY_EXIT_SUCCESS; return SCRCPY_EXIT_SUCCESS;
goto end;
} }
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
@@ -78,26 +72,18 @@ main_scrcpy(int argc, char *argv[]) {
#endif #endif
if (!net_init()) { if (!net_init()) {
ret = SCRCPY_EXIT_FAILURE; return SCRCPY_EXIT_FAILURE;
goto end;
} }
sc_log_configure(); sc_log_configure();
#ifdef HAVE_USB #ifdef HAVE_USB
ret = args.opts.otg ? scrcpy_otg(&args.opts) : scrcpy(&args.opts); enum scrcpy_exit_code ret = args.opts.otg ? scrcpy_otg(&args.opts)
: scrcpy(&args.opts);
#else #else
ret = scrcpy(&args.opts); enum scrcpy_exit_code ret = scrcpy(&args.opts);
#endif #endif
end:
if (args.pause_on_exit == SC_PAUSE_ON_EXIT_TRUE ||
(args.pause_on_exit == SC_PAUSE_ON_EXIT_IF_ERROR &&
ret != SCRCPY_EXIT_SUCCESS)) {
printf("Press Enter to continue...\n");
getchar();
}
return ret; return ret;
} }

View File

@@ -81,5 +81,6 @@ const struct scrcpy_options scrcpy_options_default = {
.require_audio = false, .require_audio = false,
.list_encoders = false, .list_encoders = false,
.list_displays = false, .list_displays = false,
.list_cameras = false,
.kill_adb_on_close = false, .kill_adb_on_close = false,
}; };

View File

@@ -181,6 +181,7 @@ struct scrcpy_options {
bool require_audio; bool require_audio;
bool list_encoders; bool list_encoders;
bool list_displays; bool list_displays;
bool list_cameras;
bool kill_adb_on_close; bool kill_adb_on_close;
}; };

View File

@@ -381,6 +381,7 @@ scrcpy(struct scrcpy_options *options) {
.power_on = options->power_on, .power_on = options->power_on,
.list_encoders = options->list_encoders, .list_encoders = options->list_encoders,
.list_displays = options->list_displays, .list_displays = options->list_displays,
.list_cameras = options->list_cameras,
.kill_adb_on_close = options->kill_adb_on_close, .kill_adb_on_close = options->kill_adb_on_close,
}; };
@@ -399,7 +400,8 @@ scrcpy(struct scrcpy_options *options) {
server_started = true; server_started = true;
if (options->list_encoders || options->list_displays) { if (options->list_encoders || options->list_displays
|| options->list_cameras) {
bool ok = await_for_server(NULL); bool ok = await_for_server(NULL);
ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE; ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE;
goto end; goto end;

View File

@@ -316,6 +316,9 @@ execute_server(struct sc_server *server,
if (params->list_displays) { if (params->list_displays) {
ADD_PARAM("list_displays=true"); ADD_PARAM("list_displays=true");
} }
if (params->list_cameras) {
ADD_PARAM("list_cameras=true");
}
#undef ADD_PARAM #undef ADD_PARAM
@@ -895,7 +898,8 @@ run_server(void *data) {
// If --list-* is passed, then the server just prints the requested data // If --list-* is passed, then the server just prints the requested data
// then exits. // then exits.
if (params->list_encoders || params->list_displays) { if (params->list_encoders || params->list_displays
|| params->list_cameras) {
sc_pid pid = execute_server(server, params); sc_pid pid = execute_server(server, params);
if (pid == SC_PROCESS_NONE) { if (pid == SC_PROCESS_NONE) {
goto error_connection_failed; goto error_connection_failed;

View File

@@ -58,6 +58,7 @@ struct sc_server_params {
bool power_on; bool power_on;
bool list_encoders; bool list_encoders;
bool list_displays; bool list_displays;
bool list_cameras;
bool kill_adb_on_close; bool kill_adb_on_close;
}; };

View File

@@ -3,6 +3,12 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.MediaCodec;
import java.util.List; import java.util.List;
public final class LogUtils { public final class LogUtils {
@@ -60,4 +66,48 @@ public final class LogUtils {
} }
return builder.toString(); return builder.toString();
} }
private static String getCameraFacingName(int facing) {
switch (facing) {
case CameraCharacteristics.LENS_FACING_FRONT:
return "front";
case CameraCharacteristics.LENS_FACING_BACK:
return "back";
case CameraCharacteristics.LENS_FACING_EXTERNAL:
return "external";
default:
return "unknown";
}
}
public static String buildCameraListMessage() {
StringBuilder builder = new StringBuilder("List of cameras:");
CameraManager cameraManager = ServiceManager.getCameraManager();
try {
String[] cameraIds = cameraManager.getCameraIdList();
if (cameraIds == null || cameraIds.length == 0) {
builder.append("\n (none)");
} else {
for (String id : cameraIds) {
builder.append("\n --video-source=camera --camera=").append(id);
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
Integer facingInteger = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facingInteger != null) {
int facing = facingInteger;
builder.append(" (").append(getCameraFacingName(facing)).append(')');
}
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class);
for (android.util.Size size : sizes) {
// TODO remove (just for testing)
builder.append("\n - " + size.getWidth() + "x" + size.getHeight());
}
}
}
} catch (CameraAccessException e) {
builder.append("\n (access denied)");
}
return builder.toString();
}
} }

View File

@@ -38,6 +38,7 @@ public class Options {
private boolean listEncoders; private boolean listEncoders;
private boolean listDisplays; private boolean listDisplays;
private boolean listCameras;
// Options not used by the scrcpy client, but useful to use scrcpy-server directly // Options not used by the scrcpy client, but useful to use scrcpy-server directly
private boolean sendDeviceMeta = true; // send device name and size private boolean sendDeviceMeta = true; // send device name and size
@@ -161,6 +162,10 @@ public class Options {
return listDisplays; return listDisplays;
} }
public boolean getListCameras() {
return listCameras;
}
public boolean getSendDeviceMeta() { public boolean getSendDeviceMeta() {
return sendDeviceMeta; return sendDeviceMeta;
} }
@@ -306,6 +311,9 @@ public class Options {
case "list_displays": case "list_displays":
options.listDisplays = Boolean.parseBoolean(value); options.listDisplays = Boolean.parseBoolean(value);
break; break;
case "list_cameras":
options.listCameras = Boolean.parseBoolean(value);
break;
case "send_device_meta": case "send_device_meta":
options.sendDeviceMeta = Boolean.parseBoolean(value); options.sendDeviceMeta = Boolean.parseBoolean(value);
break; break;

View File

@@ -0,0 +1,83 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
import android.view.Surface;
public class ScreenCapture extends SurfaceCapture implements Device.RotationListener, Device.FoldListener {
private final Device device;
private IBinder display;
public ScreenCapture(Device device) {
this.device = device;
}
@Override
public void init() {
display = createDisplay();
device.setRotationListener(this);
device.setFoldListener(this);
}
@Override
public void release() {
device.setRotationListener(null);
device.setFoldListener(null);
SurfaceControl.destroyDisplay(display);
}
@Override
public Size getSize() {
return device.getScreenInfo().getVideoSize();
}
@Override
public void setMaxSize(int size) {
device.setMaxSize(size);
}
@Override
public void setSurface(Surface surface) {
ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
}
@Override
public void onFoldChanged(int displayId, boolean folded) {
requestReset();
}
@Override
public void onRotationChanged(int rotation) {
requestReset();
}
private static IBinder createDisplay() {
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals(
Build.VERSION.CODENAME));
return SurfaceControl.createDisplay("scrcpy", secure);
}
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
SurfaceControl.openTransaction();
try {
SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, layerStack);
} finally {
SurfaceControl.closeTransaction();
}
}
}

View File

@@ -2,6 +2,7 @@ package com.genymobile.scrcpy;
import android.os.BatteryManager; import android.os.BatteryManager;
import android.os.Build; import android.os.Build;
import android.os.Looper;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@@ -99,7 +100,8 @@ public final class Server {
boolean audio = options.getAudio(); boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte(); boolean sendDummyByte = options.getSendDummyByte();
Workarounds.apply(audio); boolean camera = true;
Workarounds.apply(audio, camera);
List<AsyncProcessor> asyncProcessors = new ArrayList<>(); List<AsyncProcessor> asyncProcessors = new ArrayList<>();
@@ -132,7 +134,8 @@ public final class Server {
if (video) { if (video) {
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(), Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
options.getSendFrameMeta()); options.getSendFrameMeta());
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), ScreenCapture screenCapture = new ScreenCapture(device);
SurfaceEncoder screenEncoder = new SurfaceEncoder(screenCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
asyncProcessors.add(screenEncoder); asyncProcessors.add(screenEncoder);
} }
@@ -179,7 +182,7 @@ public final class Server {
Ln.initLogLevel(options.getLogLevel()); Ln.initLogLevel(options.getLogLevel());
if (options.getListEncoders() || options.getListDisplays()) { if (options.getListEncoders() || options.getListDisplays() || options.getListCameras()) {
if (options.getCleanup()) { if (options.getCleanup()) {
CleanUp.unlinkSelf(); CleanUp.unlinkSelf();
} }
@@ -191,6 +194,10 @@ public final class Server {
if (options.getListDisplays()) { if (options.getListDisplays()) {
Ln.i(LogUtils.buildDisplayListMessage()); Ln.i(LogUtils.buildDisplayListMessage());
} }
if (options.getListCameras()) {
Workarounds.apply(false, true);
Ln.i(LogUtils.buildCameraListMessage());
}
// Just print the requested data, do not mirror // Just print the requested data, do not mirror
return; return;
} }

View File

@@ -0,0 +1,61 @@
package com.genymobile.scrcpy;
import android.view.Surface;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A video source which can be rendered on a Surface for encoding.
*/
public abstract class SurfaceCapture {
private final AtomicBoolean resetCapture = new AtomicBoolean();
/**
* Request the encoding session to be restarted, for example if the capture implementation detects that the video source size has changed (on
* device rotation for example).
*/
protected void requestReset() {
resetCapture.set(true);
}
/**
* Consume the reset request (intended to be called by the encoder).
*
* @return {@code true} if a reset request was pending, {@code false} otherwise.
*/
public boolean consumeReset() {
return resetCapture.getAndSet(false);
}
/**
* Called once before the capture starts.
*/
public abstract void init();
/**
* Called after the capture ends (if and only if {@link #init()} has been called).
*/
public abstract void release();
/**
* Return the video size
*
* @return the video size
*/
public abstract Size getSize();
/**
* Set the maximum capture size (set by the encoder if it does not support the current size).
*
* @param size Maximum size
*/
public abstract void setMaxSize(int size);
/**
* Set the target surface.
*
* @param surface the surface which will be encoded
*/
public abstract void setSurface(Surface surface);
}

View File

@@ -17,7 +17,7 @@ import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener, Device.FoldListener, AsyncProcessor { public class SurfaceEncoder implements AsyncProcessor {
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
@@ -27,9 +27,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
private static final int MAX_CONSECUTIVE_ERRORS = 3; private static final int MAX_CONSECUTIVE_ERRORS = 3;
private final AtomicBoolean resetCapture = new AtomicBoolean(); private final SurfaceCapture capture;
private final Device device;
private final Streamer streamer; private final Streamer streamer;
private final String encoderName; private final String encoderName;
private final List<CodecOption> codecOptions; private final List<CodecOption> codecOptions;
@@ -43,9 +41,9 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
private Thread thread; private Thread thread;
private final AtomicBoolean stopped = new AtomicBoolean(); private final AtomicBoolean stopped = new AtomicBoolean();
public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
boolean downsizeOnError) { boolean downsizeOnError) {
this.device = device; this.capture = capture;
this.streamer = streamer; this.streamer = streamer;
this.videoBitRate = videoBitRate; this.videoBitRate = videoBitRate;
this.maxFps = maxFps; this.maxFps = maxFps;
@@ -54,51 +52,29 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
this.downsizeOnError = downsizeOnError; this.downsizeOnError = downsizeOnError;
} }
@Override
public void onFoldChanged(int displayId, boolean folded) {
resetCapture.set(true);
}
@Override
public void onRotationChanged(int rotation) {
resetCapture.set(true);
}
private boolean consumeResetCapture() {
return resetCapture.getAndSet(false);
}
private void streamScreen() throws IOException, ConfigurationException { private void streamScreen() throws IOException, ConfigurationException {
Codec codec = streamer.getCodec(); Codec codec = streamer.getCodec();
MediaCodec mediaCodec = createMediaCodec(codec, encoderName); MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
IBinder display = createDisplay();
device.setRotationListener(this);
device.setFoldListener(this);
streamer.writeVideoHeader(device.getScreenInfo().getVideoSize()); capture.init();
boolean alive;
try { try {
do { streamer.writeVideoHeader(capture.getSize());
ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect();
// include the locked video orientation boolean alive;
Rect videoRect = screenInfo.getVideoSize().toRect();
format.setInteger(MediaFormat.KEY_WIDTH, videoRect.width()); do {
format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height()); Size size = capture.getSize();
format.setInteger(MediaFormat.KEY_WIDTH, size.getWidth());
format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight());
Surface surface = null; Surface surface = null;
try { try {
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = mediaCodec.createInputSurface(); surface = mediaCodec.createInputSurface();
// does not include the locked video orientation capture.setSurface(surface);
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
mediaCodec.start(); mediaCodec.start();
@@ -107,7 +83,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
mediaCodec.stop(); mediaCodec.stop();
} catch (IllegalStateException | IllegalArgumentException e) { } catch (IllegalStateException | IllegalArgumentException e) {
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
if (!prepareRetry(device, screenInfo)) { if (!prepareRetry(size)) {
throw e; throw e;
} }
Ln.i("Retrying..."); Ln.i("Retrying...");
@@ -121,13 +97,11 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
} while (alive); } while (alive);
} finally { } finally {
mediaCodec.release(); mediaCodec.release();
device.setRotationListener(null); capture.release();
device.setFoldListener(null);
SurfaceControl.destroyDisplay(display);
} }
} }
private boolean prepareRetry(Device device, ScreenInfo screenInfo) { private boolean prepareRetry(Size currentSize) {
if (firstFrameSent) { if (firstFrameSent) {
++consecutiveErrors; ++consecutiveErrors;
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
@@ -147,7 +121,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
// Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising) // Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising)
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize()); int newMaxSize = chooseMaxSizeFallback(currentSize);
if (newMaxSize == 0) { if (newMaxSize == 0) {
// Must definitively fail // Must definitively fail
return false; return false;
@@ -155,7 +129,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
// Retry with a smaller device size // Retry with a smaller device size
Ln.i("Retrying with -m" + newMaxSize + "..."); Ln.i("Retrying with -m" + newMaxSize + "...");
device.setMaxSize(newMaxSize); capture.setMaxSize(newMaxSize);
return true; return true;
} }
@@ -176,14 +150,14 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
boolean alive = true; boolean alive = true;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeResetCapture() && !eof) { while (!capture.consumeReset() && !eof) {
if (stopped.get()) { if (stopped.get()) {
alive = false; alive = false;
break; break;
} }
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
try { try {
if (consumeResetCapture()) { if (capture.consumeReset()) {
// must restart encoding with new size // must restart encoding with new size
break; break;
} }
@@ -267,8 +241,8 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
private static IBinder createDisplay() { private static IBinder createDisplay() {
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S" boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals(
.equals(Build.VERSION.CODENAME)); Build.VERSION.CODENAME));
return SurfaceControl.createDisplay("scrcpy", secure); return SurfaceControl.createDisplay("scrcpy", secure);
} }

View File

@@ -28,14 +28,13 @@ public final class Workarounds {
// not instantiable // not instantiable
} }
public static void apply(boolean audio) { public static void apply(boolean audio, boolean camera) {
Workarounds.prepareMainLooper(); Workarounds.prepareMainLooper();
boolean mustFillAppInfo = false; boolean mustFillAppInfo = false;
boolean mustFillBaseContext = false; boolean mustFillBaseContext = false;
boolean mustFillAppContext = false; boolean mustFillAppContext = false;
if (Build.BRAND.equalsIgnoreCase("meizu")) { if (Build.BRAND.equalsIgnoreCase("meizu")) {
// Workarounds must be applied for Meizu phones: // Workarounds must be applied for Meizu phones:
// - <https://github.com/Genymobile/scrcpy/issues/240> // - <https://github.com/Genymobile/scrcpy/issues/240>
@@ -65,6 +64,10 @@ public final class Workarounds {
mustFillAppContext = true; mustFillAppContext = true;
} }
if (camera) {
mustFillAppInfo = true;
}
if (mustFillAppInfo) { if (mustFillAppInfo) {
Workarounds.fillAppInfo(); Workarounds.fillAppInfo();
} }

View File

@@ -1,9 +1,15 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Workarounds;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context;
import android.hardware.camera2.CameraManager;
import android.os.IBinder; import android.os.IBinder;
import android.os.IInterface; import android.os.IInterface;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@@ -26,6 +32,7 @@ public final class ServiceManager {
private static StatusBarManager statusBarManager; private static StatusBarManager statusBarManager;
private static ClipboardManager clipboardManager; private static ClipboardManager clipboardManager;
private static ActivityManager activityManager; private static ActivityManager activityManager;
private static CameraManager cameraManager;
private ServiceManager() { private ServiceManager() {
/* not instantiable */ /* not instantiable */
@@ -129,4 +136,16 @@ public final class ServiceManager {
return activityManager; return activityManager;
} }
public static CameraManager getCameraManager() {
if (cameraManager == null) {
try {
Constructor<CameraManager> ctor = CameraManager.class.getDeclaredConstructor(Context.class);
cameraManager = ctor.newInstance(FakeContext.get());
} catch (Exception e) {
throw new AssertionError(e);
}
}
return cameraManager;
}
} }