Compare commits

...

20 Commits

Author SHA1 Message Date
e411b74a16 Use explicit file protocol for AVIO
AVIO expects a `url` to locate a resource.

Use the file protocol to handle filenames containing colons.

Fixes #5487 <https://github.com/Genymobile/scrcpy/issues/5487>
PR #5499 <https://github.com/Genymobile/scrcpy/pull/5499>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-18 18:48:26 +01:00
5694562a74 Remove duplicate log
The function prepareRetry() already logs a more detailed message:

    Retrying with -mXXXX...
2024-11-18 18:47:57 +01:00
bd9d93194b Pass Options instance directly
Many constructors take a lot of parameters copied from Options. For
simplicity, just pass the Options instance.
2024-11-15 20:16:04 +01:00
794595e3f0 Set displayId to NONE in Options on new display
If a new display is set, force options.getDisplayId() to return
Device.DISPLAY_ID_NONE, to avoid any confusion between a local displayId
and options.getDisplayId().
2024-11-15 20:16:04 +01:00
5e10c37f02 Define all DisplayManager flags locally
For consistency.
2024-11-15 20:16:04 +01:00
0e399b65bd Remove [] around app package names
This simplifies copy-pasting from the result of:

    scrcpy --list-apps
2024-11-15 20:16:04 +01:00
2337f524d1 Improve error message on unknown camera id
If the camera id is explicitly provided (via --camera-id), report a
user-friendly error if no camera with this id is found.
2024-11-15 20:16:04 +01:00
df74cceb6f Use camera prepare() step
For consistency with screen capture.

Refs b60e174780
2024-11-15 20:16:04 +01:00
91373d906b Add FakeContext.getContentResolver()
This avoids the following error on some devices:

    Given calling package android does not match caller's uid 2000

Refs #4639 comment <https://github.com/Genymobile/scrcpy/issues/4639#issuecomment-2466081589>
Fixes #4639 <https://github.com/Genymobile/scrcpy/issues/4639>
PR #5476 <https://github.com/Genymobile/scrcpy/pull/5476>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-14 10:29:03 +01:00
c0e2e27cf9 Force javac to use UTF-8
The source files are encoded in UTF-8.

Refs <https://github.com/Genymobile/scrcpy/issues/4639#issuecomment-2467206100>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-11 09:00:40 +01:00
eff5b4b219 Add --screen-off-timeout
Change the Android "screen off timeout" (the idle delay before the
screen automatically turns off) and restore the initial value on
exit.

PR #5447 <https://github.com/Genymobile/scrcpy/pull/5447>
2024-11-09 10:26:59 +01:00
d3db9c4065 Refactor clean up configuration to simplify
All options were configured dynamically by sending a single byte to an
output stream. But in practice, only the power mode must be changed
dynamically, the others are configured once on start.

For simplicity, pass the value of static options as command line
arguments, and handle dynamic options in a loop only from a separate
thread once the clean up process is started.

This will allow to easily add cleanup options with values which do not
fit in 1 byte.

Also handle the clean up thread (and the loading of initial settings
values) from the CleanUp class, to expose a simpler clean up API.

Refs 9efa162949
PR #5447 <https://github.com/Genymobile/scrcpy/pull/5447>
2024-11-09 10:26:43 +01:00
5936167ff7 Store compensation state as a boolean
We don't need to store the last compensation value anymore, we just need
to know if it's non-zero.
2024-11-04 23:17:43 +01:00
e9dd0f68ad Fix audio regulator compensation
A call to swr_set_compensation() configures the resampler to drop or
duplicate "diff" samples over an interval of "distance" samples.

If the function is not called again, then after "distance" samples, no
more compensation will be applied. So it must always be called, even if
the new computed diff value happens to be the same as the previous one.

In practice, it is unlikely that the diff value is exactly the same
every second, except when it is actively clamped (to 2% of the sample
rate).
2024-11-04 23:09:54 +01:00
104195fc3b Add shortcut to reset video capture/encoding
Reset video capture/encoding on MOD+Shift+r.

Like on device rotation, this starts a new encoding session which
produces a video stream starting by a key frame.

PR #5432 <https://github.com/Genymobile/scrcpy/pull/5432>
2024-11-03 19:31:02 +01:00
9958302e6f Interrupt MediaCodec blocking call on reset
When the MediaCodec input is a Surface, no EOS (end-of-stream) will
never occur automatically: it may only be triggered manually by
MediaCodec.signalEndOfInputStream().

Use this signal to interrupt the blocking call to dequeueOutputBuffer()
immediately on reset, without waiting for the next frame to be dequeued.

PR #5432 <https://github.com/Genymobile/scrcpy/pull/5432>
2024-11-03 19:27:11 +01:00
69b836930a Handle capture reset via listener
When the capture source becomes "invalid" (because the display size
changes for example), a reset request is performed to restart the
encoder.

The reset state was stored in SurfaceCapture. The capture implementation
set the flag, and the encoder consumed it.

However, this mechanism did not allow a reset request to _interrupt_ the
encoder, which may be waiting on a blocking call (until a new frame is
produced).

To be able to interrupt the encoder, a reset request must not only set a
flag, but run a callback provided by the encoder. For that purpose,
introduce the CaptureListener interface, which is notified by the
SurfaceCapture implementation whenever the capture is invalidated.

For now, the listener implementation just set a flag as before, so the
behavior is unchanged. It lays the groundwork for the next commits.

PR #5432 <https://github.com/Genymobile/scrcpy/pull/5432>
2024-11-03 19:26:55 +01:00
790ea5e58c Check screen on for current displayId
Since Android 14, the "screen on" state can be checked per-display.

Refs <956f4084df%5E!/#F17>
PR #5442 <https://github.com/Genymobile/scrcpy/pull/5442>
2024-11-03 19:10:44 +01:00
1270997f6b Remove useless assignment
The local variable virtualDisplayId was already initialized to the exact
same value.
2024-11-03 19:02:57 +01:00
c905fbba8d Fix indentation 2024-11-03 19:02:37 +01:00
41 changed files with 626 additions and 281 deletions

View File

@ -77,6 +77,7 @@ _scrcpy() {
--rotation=
-s --serial=
-S --turn-screen-off
--screen-off-timeout=
--shortcut-mod=
--start-app=
-t --show-touches

View File

@ -80,6 +80,7 @@ arguments=(
'--require-audio=[Make scrcpy fail if audio is enabled but does not work]'
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
{-S,--turn-screen-off}'[Turn the device screen off immediately]'
'--screen-off-timeout=[Set the screen off timeout in seconds]'
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
'--start-app=[Start an Android app]'
{-t,--show-touches}'[Show physical touches]'

View File

@ -671,6 +671,10 @@ Pause or re-pause display
.B MOD+Shift+z
Unpause display
.TP
.B MOD+Shift+r
Reset video capture/encoding
.TP
.B MOD+g
Resize window to 1:1 (pixel\-perfect)

View File

@ -288,7 +288,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
// Enable compensation when the difference exceeds +/- 4ms.
// Disable compensation when the difference is lower than +/- 1ms.
int threshold = ar->compensation != 0
int threshold = ar->compensation_active
? ar->sample_rate / 1000 /* 1ms */
: ar->sample_rate * 4 / 1000; /* 4ms */
@ -309,14 +309,12 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ar->target_buffering, avg, can_read, diff);
if (diff != ar->compensation) {
int ret = swr_set_compensation(swr_ctx, diff, distance);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
} else {
ar->compensation = diff;
}
int ret = swr_set_compensation(swr_ctx, diff, distance);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
} else {
ar->compensation_active = diff != 0;
}
}
@ -392,7 +390,7 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
atomic_init(&ar->played, false);
atomic_init(&ar->received, false);
atomic_init(&ar->underflow, 0);
ar->compensation = 0;
ar->compensation_active = false;
return true;

View File

@ -44,8 +44,8 @@ struct sc_audio_regulator {
// Number of silence samples inserted since the last received packet
atomic_uint_least32_t underflow;
// Current applied compensation value (only used by the receiver thread)
int compensation;
// Non-zero compensation applied (only used by the receiver thread)
bool compensation_active;
// Set to true the first time a sample is received
atomic_bool received;

View File

@ -106,6 +106,7 @@ enum {
OPT_NEW_DISPLAY,
OPT_LIST_APPS,
OPT_START_APP,
OPT_SCREEN_OFF_TIMEOUT,
};
struct sc_option {
@ -793,6 +794,13 @@ static const struct sc_option options[] = {
.longopt = "turn-screen-off",
.text = "Turn the device screen off immediately.",
},
{
.longopt_id = OPT_SCREEN_OFF_TIMEOUT,
.longopt = "screen-off-timeout",
.argdesc = "seconds",
.text = "Set the screen off timeout while scrcpy is running (restore "
"the initial value on exit).",
},
{
.longopt_id = OPT_SHORTCUT_MOD,
.longopt = "shortcut-mod",
@ -1022,6 +1030,10 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "MOD+Shift+z" },
.text = "Unpause display",
},
{
.shortcuts = { "MOD+Shift+r" },
.text = "Reset video capture/encoding",
},
{
.shortcuts = { "MOD+g" },
.text = "Resize window to 1:1 (pixel-perfect)",
@ -2151,6 +2163,20 @@ parse_time_limit(const char *s, sc_tick *tick) {
return true;
}
static bool
parse_screen_off_timeout(const char *s, sc_tick *tick) {
long value;
// value in seconds, but must fit in 31 bits in milliseconds
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF / 1000,
"screen off timeout");
if (!ok) {
return false;
}
*tick = SC_TICK_FROM_SEC(value);
return true;
}
static bool
parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) {
if (!s || !strcmp(s, "true")) {
@ -2726,6 +2752,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_START_APP:
opts->start_app = optarg;
break;
case OPT_SCREEN_OFF_TIMEOUT:
if (!parse_screen_off_timeout(optarg,
&opts->screen_off_timeout)) {
return false;
}
break;
default:
// getopt prints the error message on stderr
return false;

View File

@ -181,6 +181,7 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
// no additional data
return 1;
default:
@ -304,6 +305,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_START_APP:
LOG_CMSG("start app \"%s\"", msg->start_app.name);
break;
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
LOG_CMSG("reset video");
break;
default:
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
break;

View File

@ -42,6 +42,7 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_UHID_DESTROY,
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
SC_CONTROL_MSG_TYPE_START_APP,
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
};
enum sc_copy_key {

View File

@ -284,6 +284,18 @@ open_hard_keyboard_settings(struct sc_input_manager *im) {
}
}
static void
reset_video(struct sc_input_manager *im) {
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO;
if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request reset video");
}
}
static void
apply_orientation_transform(struct sc_input_manager *im,
enum sc_orientation transform) {
@ -521,8 +533,12 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
return;
case SDLK_r:
if (control && !shift && !repeat && down && !paused) {
rotate_device(im);
if (control && !repeat && down && !paused) {
if (shift) {
reset_video(im);
} else {
rotate_device(im);
}
}
return;
case SDLK_k:

View File

@ -62,6 +62,7 @@ const struct scrcpy_options scrcpy_options_default = {
.audio_buffer = -1, // depends on the audio format,
.audio_output_buffer = SC_TICK_FROM_MS(5),
.time_limit = 0,
.screen_off_timeout = -1,
#ifdef HAVE_V4L2
.v4l2_device = NULL,
.v4l2_buffer = 0,

View File

@ -265,6 +265,7 @@ struct scrcpy_options {
sc_tick audio_buffer;
sc_tick audio_output_buffer;
sc_tick time_limit;
sc_tick screen_off_timeout;
#ifdef HAVE_V4L2
const char *v4l2_device;
sc_tick v4l2_buffer;

View File

@ -143,8 +143,14 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) {
return false;
}
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
AVIO_FLAG_WRITE);
char *file_url = sc_str_concat("file:", recorder->filename);
if (!file_url) {
avformat_free_context(recorder->ctx);
return false;
}
int ret = avio_open(&recorder->ctx->pb, file_url, AVIO_FLAG_WRITE);
free(file_url);
if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename);
avformat_free_context(recorder->ctx);

View File

@ -428,6 +428,7 @@ scrcpy(struct scrcpy_options *options) {
.video_bit_rate = options->video_bit_rate,
.audio_bit_rate = options->audio_bit_rate,
.max_fps = options->max_fps,
.screen_off_timeout = options->screen_off_timeout,
.lock_video_orientation = options->lock_video_orientation,
.control = options->control,
.display_id = options->display_id,

View File

@ -320,6 +320,11 @@ execute_server(struct sc_server *server,
if (params->stay_awake) {
ADD_PARAM("stay_awake=true");
}
if (params->screen_off_timeout != -1) {
assert(params->screen_off_timeout >= 0);
uint64_t ms = SC_TICK_TO_MS(params->screen_off_timeout);
ADD_PARAM("screen_off_timeout=%" PRIu64, ms);
}
if (params->video_codec_options) {
VALIDATE_STRING(params->video_codec_options);
ADD_PARAM("video_codec_options=%s", params->video_codec_options);

View File

@ -45,6 +45,7 @@ struct sc_server_params {
uint32_t video_bit_rate;
uint32_t audio_bit_rate;
const char *max_fps; // float to be parsed by the server
sc_tick screen_off_timeout;
int8_t lock_video_orientation;
bool control;
uint32_t display_id;

View File

@ -64,6 +64,26 @@ sc_str_quote(const char *src) {
return quoted;
}
char *
sc_str_concat(const char *start, const char *end) {
assert(start);
assert(end);
size_t start_len = strlen(start);
size_t end_len = strlen(end);
char *result = malloc(start_len + end_len + 1);
if (!result) {
LOG_OOM();
return NULL;
}
memcpy(result, start, start_len);
memcpy(result + start_len, end, end_len + 1);
return result;
}
bool
sc_str_parse_integer(const char *s, long *out) {
char *endptr;

View File

@ -38,6 +38,15 @@ sc_str_join(char *dst, const char *const tokens[], char sep, size_t n);
char *
sc_str_quote(const char *src);
/**
* Concat two strings
*
* Return a new allocated string, contanining the concatenation of the two
* input strings.
*/
char *
sc_str_concat(const char *start, const char *end);
/**
* Parse `s` as an integer into `out`
*

View File

@ -407,6 +407,21 @@ static void test_serialize_open_hard_keyboard(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_reset_video(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO,
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
@ -429,5 +444,6 @@ int main(int argc, char *argv[]) {
test_serialize_uhid_input();
test_serialize_uhid_destroy();
test_serialize_open_hard_keyboard();
test_serialize_reset_video();
return 0;
}

View File

@ -141,6 +141,16 @@ static void test_quote(void) {
free(out);
}
static void test_concat(void) {
const char *s = "2024:11";
char *out = sc_str_concat("my-prefix:", s);
// contains the concat
assert(!strcmp("my-prefix:2024:11", out));
free(out);
}
static void test_utf8_truncate(void) {
const char *s = "aÉbÔc";
assert(strlen(s) == 7); // É and Ô are 2 bytes-wide
@ -389,6 +399,7 @@ int main(int argc, char *argv[]) {
test_join_truncated_before_sep();
test_join_truncated_after_sep();
test_quote();
test_concat();
test_utf8_truncate();
test_parse_integer();
test_parse_integers();

View File

@ -71,6 +71,31 @@ adb shell cmd display power-on 0
```
## Screen off timeout
The Android screen automatically turns off after some delay.
To change this delay while scrcpy is running:
```bash
scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes)
```
The initial value is restored on exit.
It is possible to change this setting manually:
```bash
# get the current screen_off_timeout value
adb shell settings get system screen_off_timeout
# set a new value (in milliseconds)
adb shell settings put system screen_off_timeout 30000
```
Note that the Android value is in milliseconds, but the scrcpy command line
argument is in seconds.
## Show touches
For presentations, it may be useful to show physical touches (on the physical

View File

@ -30,6 +30,7 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Flip display vertically | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(up)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(down)_
| Pause or re-pause display | <kbd>MOD</kbd>+<kbd>z</kbd>
| Unpause display | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>z</kbd>
| Reset video capture/encoding | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>r</kbd>
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_

View File

@ -50,6 +50,11 @@ cd "$SERVER_DIR/src/main/aidl"
android/content/IOnPrimaryClipChangedListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl
# Fake sources to expose hidden Android types to the project
FAKE_SRC=( \
android/content/*java \
)
SRC=( \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/audio/*.java \
@ -68,10 +73,11 @@ done
echo "Compiling java sources..."
cd ../java
javac -bootclasspath "$ANDROID_JAR" \
javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \
-d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \
${FAKE_SRC[@]} \
${SRC[@]}
echo "Dexing..."

View File

@ -0,0 +1,5 @@
package android.content;
public interface IContentProvider {
// android.content.IContentProvider is hidden, this is a fake one to expose the type to the project
}

View File

@ -5,6 +5,8 @@ import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import android.os.BatteryManager;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
@ -16,59 +18,132 @@ import java.io.OutputStream;
*/
public final class CleanUp {
private static final int MSG_TYPE_MASK = 0b11;
private static final int MSG_TYPE_RESTORE_STAY_ON = 0;
private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1;
private static final int MSG_TYPE_RESTORE_DISPLAY_POWER = 2;
private static final int MSG_TYPE_POWER_OFF_SCREEN = 3;
// Dynamic options
private static final int PENDING_CHANGE_DISPLAY_POWER = 1 << 0;
private int pendingChanges;
private boolean pendingRestoreDisplayPower;
private static final int MSG_PARAM_SHIFT = 2;
private Thread thread;
private final OutputStream out;
public CleanUp(OutputStream out) {
this.out = out;
private CleanUp(Options options) {
thread = new Thread(() -> runCleanUp(options), "cleanup");
thread.start();
}
public static CleanUp configure(int displayId) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)};
public static CleanUp start(Options options) {
return new CleanUp(options);
}
public void interrupt() {
thread.interrupt();
}
public void join() throws InterruptedException {
thread.join();
}
private void runCleanUp(Options options) {
boolean disableShowTouches = false;
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
disableShowTouches = !"1".equals(oldValue);
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
}
}
int restoreStayOn = -1;
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 {
int currentStayOn = Integer.parseInt(oldValue);
// Restore only if the current value is different
if (currentStayOn != stayOn) {
restoreStayOn = currentStayOn;
}
} catch (NumberFormatException e) {
// ignore
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
int restoreScreenOffTimeout = -1;
int screenOffTimeout = options.getScreenOffTimeout();
if (screenOffTimeout != -1) {
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(screenOffTimeout));
try {
int currentScreenOffTimeout = Integer.parseInt(oldValue);
// Restore only if the current value is different
if (currentScreenOffTimeout != screenOffTimeout) {
restoreScreenOffTimeout = currentScreenOffTimeout;
}
} catch (NumberFormatException e) {
// ignore
}
} catch (SettingsException e) {
Ln.e("Could not change \"screen_off_timeout\"", e);
}
}
boolean powerOffScreen = options.getPowerOffScreenOnClose();
int displayId = options.getDisplayId();
try {
run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout);
} catch (InterruptedException e) {
// ignore
} catch (IOException e) {
Ln.e("Clean up I/O exception", e);
}
}
private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout)
throws IOException, InterruptedException {
String[] cmd = {
"app_process",
"/",
CleanUp.class.getName(),
String.valueOf(displayId),
String.valueOf(restoreStayOn),
String.valueOf(disableShowTouches),
String.valueOf(powerOffScreen),
String.valueOf(restoreScreenOffTimeout),
};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", Server.SERVER_PATH);
Process process = builder.start();
return new CleanUp(process.getOutputStream());
}
OutputStream out = process.getOutputStream();
private boolean sendMessage(int type, int param) {
assert (type & ~MSG_TYPE_MASK) == 0;
int msg = type | param << MSG_PARAM_SHIFT;
try {
out.write(msg);
out.flush();
return true;
} catch (IOException e) {
Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e);
return false;
while (true) {
int localPendingChanges;
boolean localPendingRestoreDisplayPower;
synchronized (this) {
while (pendingChanges == 0) {
wait();
}
localPendingChanges = pendingChanges;
localPendingRestoreDisplayPower = pendingRestoreDisplayPower;
pendingChanges = 0;
}
if ((localPendingChanges & PENDING_CHANGE_DISPLAY_POWER) != 0) {
out.write(localPendingRestoreDisplayPower ? 1 : 0);
out.flush();
}
}
}
public boolean setRestoreStayOn(int restoreValue) {
// Restore the value (between 0 and 7), -1 to not restore
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
assert restoreValue >= -1 && restoreValue <= 7;
return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111);
}
public boolean setDisableShowTouches(boolean disableOnExit) {
return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0);
}
public boolean setRestoreDisplayPower(boolean restoreOnExit) {
return sendMessage(MSG_TYPE_RESTORE_DISPLAY_POWER, restoreOnExit ? 1 : 0);
}
public boolean setPowerOffScreen(boolean powerOffScreenOnExit) {
return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0);
public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) {
pendingRestoreDisplayPower = restoreDisplayPower;
pendingChanges |= PENDING_CHANGE_DISPLAY_POWER;
notify();
}
public static void unlinkSelf() {
@ -83,35 +158,21 @@ public final class CleanUp {
unlinkSelf();
int displayId = Integer.parseInt(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
boolean disableShowTouches = Boolean.parseBoolean(args[2]);
boolean powerOffScreen = Boolean.parseBoolean(args[3]);
int restoreScreenOffTimeout = Integer.parseInt(args[4]);
int restoreStayOn = -1;
boolean disableShowTouches = false;
// Dynamic option
boolean restoreDisplayPower = false;
boolean powerOffScreen = false;
try {
// Wait for the server to die
int msg;
while ((msg = System.in.read()) != -1) {
int type = msg & MSG_TYPE_MASK;
int param = msg >> MSG_PARAM_SHIFT;
switch (type) {
case MSG_TYPE_RESTORE_STAY_ON:
restoreStayOn = param > 7 ? -1 : param;
break;
case MSG_TYPE_DISABLE_SHOW_TOUCHES:
disableShowTouches = param != 0;
break;
case MSG_TYPE_RESTORE_DISPLAY_POWER:
restoreDisplayPower = param != 0;
break;
case MSG_TYPE_POWER_OFF_SCREEN:
powerOffScreen = param != 0;
break;
default:
Ln.w("Unexpected msg type: " + type);
break;
}
// Only restore display power
assert msg == 0 || msg == 1;
restoreDisplayPower = msg != 0;
}
} catch (IOException e) {
// Expected when the server is dead
@ -137,7 +198,16 @@ public final class CleanUp {
}
}
if (Device.isScreenOn() && displayId != Device.DISPLAY_ID_NONE) {
if (restoreScreenOffTimeout != -1) {
Ln.i("Restoring \"screen off timeout\"");
try {
Settings.putValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(restoreScreenOffTimeout));
} catch (SettingsException e) {
Ln.e("Could not restore \"screen_off_timeout\"", e);
}
}
if (displayId != Device.DISPLAY_ID_NONE && Device.isScreenOn(displayId)) {
if (powerOffScreen) {
Ln.i("Power off screen");
Device.powerOffScreen(displayId);

View File

@ -1,9 +1,14 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.TargetApi;
import android.content.AttributionSource;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.IContentProvider;
import android.os.Binder;
import android.os.Process;
public final class FakeContext extends ContextWrapper {
@ -17,6 +22,38 @@ public final class FakeContext extends ContextWrapper {
return INSTANCE;
}
private final ContentResolver contentResolver = new ContentResolver(this) {
@SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
// @Override (but super-class method not visible)
protected IContentProvider acquireProvider(Context c, String name) {
return ServiceManager.getActivityManager().getContentProviderExternal(name, new Binder());
}
@SuppressWarnings("unused")
// @Override (but super-class method not visible)
public boolean releaseProvider(IContentProvider icp) {
return false;
}
@SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
// @Override (but super-class method not visible)
protected IContentProvider acquireUnstableProvider(Context c, String name) {
return null;
}
@SuppressWarnings("unused")
// @Override (but super-class method not visible)
public boolean releaseUnstableProvider(IContentProvider icp) {
return false;
}
@SuppressWarnings("unused")
// @Override (but super-class method not visible)
public void unstableProviderDied(IContentProvider icp) {
// ignore
}
};
private FakeContext() {
super(Workarounds.getSystemContext());
}
@ -49,4 +86,9 @@ public final class FakeContext extends ContextWrapper {
public Context getApplicationContext() {
return this;
}
@Override
public ContentResolver getContentResolver() {
return contentResolver;
}
}

View File

@ -45,6 +45,7 @@ public class Options {
private boolean cameraHighSpeed;
private boolean showTouches;
private boolean stayAwake;
private int screenOffTimeout = -1;
private List<CodecOption> videoCodecOptions;
private List<CodecOption> audioCodecOptions;
@ -174,6 +175,10 @@ public class Options {
return stayAwake;
}
public int getScreenOffTimeout() {
return screenOffTimeout;
}
public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions;
}
@ -363,6 +368,12 @@ public class Options {
case "stay_awake":
options.stayAwake = Boolean.parseBoolean(value);
break;
case "screen_off_timeout":
options.screenOffTimeout = Integer.parseInt(value);
if (options.screenOffTimeout < -1) {
throw new IllegalArgumentException("Invalid screen off timeout: " + options.screenOffTimeout);
}
break;
case "video_codec_options":
options.videoCodecOptions = CodecOption.parse(value);
break;
@ -468,6 +479,11 @@ public class Options {
}
}
if (options.newDisplay != null) {
assert options.displayId == 0 : "Must not set both displayId and newDisplay";
options.displayId = Device.DISPLAY_ID_NONE;
}
return options;
}

View File

@ -16,8 +16,6 @@ import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import com.genymobile.scrcpy.video.CameraCapture;
import com.genymobile.scrcpy.video.NewDisplayCapture;
import com.genymobile.scrcpy.video.ScreenCapture;
@ -25,7 +23,6 @@ import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource;
import android.os.BatteryManager;
import android.os.Build;
import java.io.File;
@ -76,51 +73,6 @@ public final class Server {
// not instantiable
}
private static void initAndCleanUp(Options options, CleanUp cleanUp) {
// This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once
// and for all, they cannot be changed from another thread)
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
if (!"1".equals(oldValue)) {
if (!cleanUp.setDisableShowTouches(true)) {
Ln.e("Could not disable show touch on exit");
}
}
} 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 {
int restoreStayOn = Integer.parseInt(oldValue);
if (restoreStayOn != stayOn) {
// Restore only if the current value is different
if (!cleanUp.setRestoreStayOn(restoreStayOn)) {
Ln.e("Could not restore stay on on exit");
}
}
} catch (NumberFormatException e) {
// ignore
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
if (options.getPowerOffScreenOnClose()) {
if (!cleanUp.setPowerOffScreen(true)) {
Ln.e("Could not power off screen on exit");
}
}
}
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");
@ -150,14 +102,9 @@ public final class Server {
}
CleanUp cleanUp = null;
Thread initThread = null;
NewDisplay newDisplay = options.getNewDisplay();
int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE;
if (options.getCleanup()) {
cleanUp = CleanUp.configure(displayId);
initThread = startInitThread(options, cleanUp);
cleanUp = CleanUp.start(options);
}
int scid = options.getScid();
@ -181,7 +128,7 @@ public final class Server {
if (control) {
ControlChannel controlChannel = connection.getControlChannel();
controller = new Controller(displayId, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
controller = new Controller(controlChannel, cleanUp, options);
asyncProcessors.add(controller);
}
@ -200,8 +147,7 @@ public final class Server {
if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
} else {
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
options.getAudioEncoder());
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options);
}
asyncProcessors.add(audioRecorder);
}
@ -211,20 +157,22 @@ public final class Server {
options.getSendFrameMeta());
SurfaceCapture surfaceCapture;
if (options.getVideoSource() == VideoSource.DISPLAY) {
NewDisplay newDisplay = options.getNewDisplay();
if (newDisplay != null) {
surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize());
surfaceCapture = new NewDisplayCapture(controller, options);
} else {
assert displayId != Device.DISPLAY_ID_NONE;
surfaceCapture = new ScreenCapture(controller, displayId, options.getMaxSize(), options.getCrop(),
options.getLockVideoOrientation());
assert options.getDisplayId() != Device.DISPLAY_ID_NONE;
surfaceCapture = new ScreenCapture(controller, options);
}
} else {
surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
surfaceCapture = new CameraCapture(options);
}
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options);
asyncProcessors.add(surfaceEncoder);
if (controller != null) {
controller.setSurfaceCapture(surfaceCapture);
}
}
Completion completion = new Completion(asyncProcessors.size());
@ -236,8 +184,8 @@ public final class Server {
completion.await();
} finally {
if (initThread != null) {
initThread.interrupt();
if (cleanUp != null) {
cleanUp.interrupt();
}
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.stop();
@ -246,8 +194,8 @@ public final class Server {
connection.shutdown();
try {
if (initThread != null) {
initThread.join();
if (cleanUp != null) {
cleanUp.join();
}
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.join();
@ -260,12 +208,6 @@ public final class Server {
}
}
private static Thread startInitThread(final Options options, final CleanUp cleanUp) {
Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup");
thread.start();
return thread;
}
public static void main(String... args) {
int status = 0;
try {

View File

@ -2,6 +2,7 @@ package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.Codec;
@ -67,12 +68,12 @@ public final class AudioEncoder implements AsyncProcessor {
private boolean ended;
public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
public AudioEncoder(AudioCapture capture, Streamer streamer, Options options) {
this.capture = capture;
this.streamer = streamer;
this.bitRate = bitRate;
this.codecOptions = codecOptions;
this.encoderName = encoderName;
this.bitRate = options.getAudioBitRate();
this.codecOptions = options.getAudioCodecOptions();
this.encoderName = options.getAudioEncoder();
}
private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) {

View File

@ -24,6 +24,7 @@ public final class ControlMessage {
public static final int TYPE_UHID_DESTROY = 14;
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
public static final int TYPE_START_APP = 16;
public static final int TYPE_RESET_VIDEO = 17;
public static final long SEQUENCE_INVALID = 0;

View File

@ -46,6 +46,7 @@ public class ControlMessageReader {
case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_ROTATE_DEVICE:
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case ControlMessage.TYPE_RESET_VIDEO:
return ControlMessage.createEmpty(type);
case ControlMessage.TYPE_UHID_CREATE:
return parseUhidCreate();

View File

@ -3,12 +3,14 @@ package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DeviceApp;
import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.VirtualDisplayListener;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.InputManager;
@ -93,12 +95,15 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private boolean keepDisplayPowerOff;
public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) {
this.displayId = displayId;
// Used for resetting video encoding on RESET_VIDEO message
private SurfaceCapture surfaceCapture;
public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) {
this.displayId = options.getDisplayId();
this.controlChannel = controlChannel;
this.cleanUp = cleanUp;
this.clipboardAutosync = clipboardAutosync;
this.powerOn = powerOn;
this.clipboardAutosync = options.getClipboardAutosync();
this.powerOn = options.getPowerOn();
initPointers();
sender = new DeviceMessageSender(controlChannel);
@ -143,6 +148,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
}
}
public void setSurfaceCapture(SurfaceCapture surfaceCapture) {
this.surfaceCapture = surfaceCapture;
}
private UhidManager getUhidManager() {
if (uhidManager == null) {
uhidManager = new UhidManager(sender);
@ -166,7 +175,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private void control() throws IOException {
// on start, power on the device
if (powerOn && displayId == 0 && !Device.isScreenOn()) {
if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
// dirty hack
@ -293,6 +302,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
case ControlMessage.TYPE_START_APP:
startAppAsync(msg.getText());
break;
case ControlMessage.TYPE_RESET_VIDEO:
resetVideo();
break;
default:
// do nothing
}
@ -490,7 +502,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
}
private boolean pressBackOrTurnScreenOn(int action) {
if (Device.isScreenOn()) {
if (displayId == Device.DISPLAY_ID_NONE || Device.isScreenOn(displayId)) {
return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC);
}
@ -680,4 +692,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
}
}
}
private void resetVideo() {
if (surfaceCapture != null) {
Ln.i("Video capture reset");
surfaceCapture.requestInvalidate();
}
}
}

View File

@ -82,8 +82,9 @@ public final class Device {
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode);
}
public static boolean isScreenOn() {
return ServiceManager.getPowerManager().isScreenOn();
public static boolean isScreenOn(int displayId) {
assert displayId != DISPLAY_ID_NONE;
return ServiceManager.getPowerManager().isScreenOn(displayId);
}
public static void expandNotificationPanel() {
@ -181,7 +182,7 @@ public final class Device {
public static boolean powerOffScreen(int displayId) {
assert displayId != DISPLAY_ID_NONE;
if (!isScreenOn()) {
if (!isScreenOn(displayId)) {
return true;
}
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);

View File

@ -236,7 +236,7 @@ public final class LogUtils {
} else {
builder.append("\n ").append(String.format("%" + column + "s", " "));
}
builder.append(" [").append(app.getPackageName()).append(']');
builder.append(" ").append(app.getPackageName());
}
return builder.toString();

View File

@ -1,9 +1,12 @@
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.HandlerExecutor;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
@ -56,19 +59,18 @@ public class CameraCapture extends SurfaceCapture {
private final AtomicBoolean disconnected = new AtomicBoolean();
public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps,
boolean highSpeed) {
this.explicitCameraId = explicitCameraId;
this.cameraFacing = cameraFacing;
this.explicitSize = explicitSize;
this.maxSize = maxSize;
this.aspectRatio = aspectRatio;
this.fps = fps;
this.highSpeed = highSpeed;
public CameraCapture(Options options) {
this.explicitCameraId = options.getCameraId();
this.cameraFacing = options.getCameraFacing();
this.explicitSize = options.getCameraSize();
this.maxSize = options.getMaxSize();
this.aspectRatio = options.getCameraAspectRatio();
this.fps = options.getCameraFps();
this.highSpeed = options.getCameraHighSpeed();
}
@Override
public void init() throws IOException {
protected void init() throws ConfigurationException, IOException {
cameraThread = new HandlerThread("camera");
cameraThread.start();
cameraHandler = new Handler(cameraThread.getLooper());
@ -77,12 +79,7 @@ public class CameraCapture extends SurfaceCapture {
try {
cameraId = selectCamera(explicitCameraId, cameraFacing);
if (cameraId == null) {
throw new IOException("No matching camera found");
}
size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed);
if (size == null) {
throw new IOException("Could not select camera size");
throw new ConfigurationException("No matching camera found");
}
Ln.i("Using camera '" + cameraId + "'");
@ -92,14 +89,30 @@ public class CameraCapture extends SurfaceCapture {
}
}
private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException {
if (explicitCameraId != null) {
return explicitCameraId;
@Override
public void prepare() throws IOException {
try {
size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed);
if (size == null) {
throw new IOException("Could not select camera size");
}
} catch (CameraAccessException e) {
throw new IOException(e);
}
}
private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException {
CameraManager cameraManager = ServiceManager.getCameraManager();
String[] cameraIds = cameraManager.getCameraIdList();
if (explicitCameraId != null) {
if (!Arrays.asList(cameraIds).contains(explicitCameraId)) {
Ln.e("Camera with id " + explicitCameraId + " not found\n" + LogUtils.buildCameraListMessage(false));
throw new ConfigurationException("Camera id not found");
}
return explicitCameraId;
}
if (cameraFacing == null) {
// Use the first one
return cameraIds.length > 0 ? cameraIds[0] : null;
@ -232,13 +245,7 @@ public class CameraCapture extends SurfaceCapture {
}
this.maxSize = maxSize;
try {
size = selectSize(cameraId, null, maxSize, aspectRatio, highSpeed);
return size != null;
} catch (CameraAccessException e) {
Ln.w("Could not select camera size", e);
return false;
}
return true;
}
@SuppressLint("MissingPermission")
@ -256,7 +263,7 @@ public class CameraCapture extends SurfaceCapture {
public void onDisconnected(CameraDevice camera) {
Ln.w("Camera disconnected");
disconnected.set(true);
requestReset();
invalidate();
}
@Override
@ -355,4 +362,9 @@ public class CameraCapture extends SurfaceCapture {
public boolean isClosed() {
return disconnected.get();
}
@Override
public void requestInvalidate() {
// do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring)
}
}

View File

@ -0,0 +1,33 @@
package com.genymobile.scrcpy.video;
import android.media.MediaCodec;
import java.util.concurrent.atomic.AtomicBoolean;
public class CaptureReset implements SurfaceCapture.CaptureListener {
private final AtomicBoolean reset = new AtomicBoolean();
// Current instance of MediaCodec to "interrupt" on reset
private MediaCodec runningMediaCodec;
public boolean consumeReset() {
return reset.getAndSet(false);
}
public synchronized void reset() {
reset.set(true);
if (runningMediaCodec != null) {
runningMediaCodec.signalEndOfInputStream();
}
}
public synchronized void setRunningMediaCodec(MediaCodec runningMediaCodec) {
this.runningMediaCodec = runningMediaCodec;
}
@Override
public void onInvalidated() {
reset();
}
}

View File

@ -1,6 +1,7 @@
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.control.PositionMapper;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.NewDisplay;
@ -14,9 +15,13 @@ import android.hardware.display.VirtualDisplay;
import android.os.Build;
import android.view.Surface;
import java.io.IOException;
public class NewDisplayCapture extends SurfaceCapture {
// Internal fields copied from android.hardware.display.DisplayManager
private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6;
private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7;
private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8;
@ -39,14 +44,15 @@ public class NewDisplayCapture extends SurfaceCapture {
private Size size;
private int dpi;
public NewDisplayCapture(VirtualDisplayListener vdListener, NewDisplay newDisplay, int maxSize) {
public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) {
this.vdListener = vdListener;
this.newDisplay = newDisplay;
this.maxSize = maxSize;
this.newDisplay = options.getNewDisplay();
assert newDisplay != null;
this.maxSize = options.getMaxSize();
}
@Override
public void init() {
protected void init() {
size = newDisplay.getSize();
dpi = newDisplay.getDpi();
if (size == null || dpi == 0) {
@ -72,17 +78,11 @@ public class NewDisplayCapture extends SurfaceCapture {
}
}
@Override
public void start(Surface surface) {
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
public void startNew(Surface surface) {
int virtualDisplayId;
try {
int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
| DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC
| VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
| VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
| VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT
| VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL
@ -93,8 +93,8 @@ public class NewDisplayCapture extends SurfaceCapture {
| VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
| VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED;
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS
| VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS
| VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
}
}
virtualDisplay = ServiceManager.getDisplayManager()
@ -107,13 +107,21 @@ public class NewDisplayCapture extends SurfaceCapture {
}
if (vdListener != null) {
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight());
PositionMapper positionMapper = new PositionMapper(size, contentRect, 0);
vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper);
}
}
@Override
public void start(Surface surface) throws IOException {
if (virtualDisplay == null) {
startNew(surface);
} else {
virtualDisplay.setSurface(surface);
}
}
@Override
public void release() {
if (virtualDisplay != null) {
@ -143,4 +151,9 @@ public class NewDisplayCapture extends SurfaceCapture {
int num = size.getMax();
return initialDpi * num / den;
}
@Override
public void requestInvalidate() {
invalidate();
}
}

View File

@ -1,8 +1,10 @@
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.control.PositionMapper;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln;
@ -48,12 +50,13 @@ public class ScreenCapture extends SurfaceCapture {
private IRotationWatcher rotationWatcher;
private IDisplayFoldListener displayFoldListener;
public ScreenCapture(VirtualDisplayListener vdListener, int displayId, int maxSize, Rect crop, int lockVideoOrientation) {
public ScreenCapture(VirtualDisplayListener vdListener, Options options) {
this.vdListener = vdListener;
this.displayId = displayId;
this.maxSize = maxSize;
this.crop = crop;
this.lockVideoOrientation = lockVideoOrientation;
this.displayId = options.getDisplayId();
assert displayId != Device.DISPLAY_ID_NONE;
this.maxSize = options.getMaxSize();
this.crop = options.getCrop();
this.lockVideoOrientation = options.getLockVideoOrientation();
}
@Override
@ -85,7 +88,7 @@ public class ScreenCapture extends SurfaceCapture {
Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)");
}
setSessionDisplaySize(null);
requestReset();
invalidate();
} else {
Size size = di.getSize();
@ -102,7 +105,7 @@ public class ScreenCapture extends SurfaceCapture {
// Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare()
// considers that the current size is the requested size (to avoid a duplicate requestReset())
setSessionDisplaySize(size);
requestReset();
invalidate();
} else if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()");
}
@ -246,7 +249,7 @@ public class ScreenCapture extends SurfaceCapture {
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")");
}
requestReset();
invalidate();
}
};
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
@ -272,7 +275,7 @@ public class ScreenCapture extends SurfaceCapture {
// Ignore events related to other display ids
return;
}
requestReset();
invalidate();
}
};
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
@ -291,4 +294,9 @@ public class ScreenCapture extends SurfaceCapture {
}
}
}
@Override
public void requestInvalidate() {
invalidate();
}
}

View File

@ -6,36 +6,37 @@ import com.genymobile.scrcpy.device.Size;
import android.view.Surface;
import java.io.IOException;
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);
public interface CaptureListener {
void onInvalidated();
}
private CaptureListener listener;
/**
* Consume the reset request (intended to be called by the encoder).
*
* @return {@code true} if a reset request was pending, {@code false} otherwise.
* Notify the listener that the capture has been invalidated (for example, because its size changed).
*/
public boolean consumeReset() {
return resetCapture.getAndSet(false);
protected void invalidate() {
listener.onInvalidated();
}
/**
* Called once before the first capture starts.
*/
public abstract void init() throws ConfigurationException, IOException;
public final void init(CaptureListener listener) throws ConfigurationException, IOException {
this.listener = listener;
init();
}
/**
* Called once before the first capture starts.
*/
protected abstract void init() throws ConfigurationException, IOException;
/**
* Called after the last capture ends (if and only if {@link #init()} has been called).
@ -45,7 +46,7 @@ public abstract class SurfaceCapture {
/**
* Called once before each capture starts, before {@link #getSize()}.
*/
public void prepare() throws ConfigurationException {
public void prepare() throws ConfigurationException, IOException {
// empty by default
}
@ -78,4 +79,11 @@ public abstract class SurfaceCapture {
public boolean isClosed() {
return false;
}
/**
* Manually request to invalidate (typically a user request).
* <p>
* The capture implementation is free to ignore the request and do nothing.
*/
public abstract void requestInvalidate();
}

View File

@ -2,6 +2,7 @@ package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.device.Streamer;
@ -49,15 +50,16 @@ public class SurfaceEncoder implements AsyncProcessor {
private Thread thread;
private final AtomicBoolean stopped = new AtomicBoolean();
public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List<CodecOption> codecOptions,
String encoderName, boolean downsizeOnError) {
private final CaptureReset reset = new CaptureReset();
public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, Options options) {
this.capture = capture;
this.streamer = streamer;
this.videoBitRate = videoBitRate;
this.maxFps = maxFps;
this.codecOptions = codecOptions;
this.encoderName = encoderName;
this.downsizeOnError = downsizeOnError;
this.videoBitRate = options.getVideoBitRate();
this.maxFps = options.getMaxFps();
this.codecOptions = options.getVideoCodecOptions();
this.encoderName = options.getVideoEncoder();
this.downsizeOnError = options.getDownsizeOnError();
}
private void streamCapture() throws IOException, ConfigurationException {
@ -65,14 +67,14 @@ public class SurfaceEncoder implements AsyncProcessor {
MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
capture.init();
capture.init(reset);
try {
boolean alive;
boolean headerWritten = false;
do {
capture.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled
reset.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled
capture.prepare();
Size size = capture.getSize();
if (!headerWritten) {
@ -92,7 +94,21 @@ public class SurfaceEncoder implements AsyncProcessor {
mediaCodec.start();
alive = encode(mediaCodec, streamer);
// Set the MediaCodec instance to "interrupt" (by signaling an EOS) on reset
reset.setRunningMediaCodec(mediaCodec);
if (stopped.get()) {
alive = false;
} else {
boolean resetRequested = reset.consumeReset();
if (!resetRequested) {
// If a reset is requested during encode(), it will interrupt the encoding by an EOS
encode(mediaCodec, streamer);
}
// The capture might have been closed internally (for example if the camera is disconnected)
alive = !stopped.get() && !capture.isClosed();
}
// do not call stop() on exception, it would trigger an IllegalStateException
mediaCodec.stop();
} catch (IllegalStateException | IllegalArgumentException e) {
@ -100,9 +116,9 @@ public class SurfaceEncoder implements AsyncProcessor {
if (!prepareRetry(size)) {
throw e;
}
Ln.i("Retrying...");
alive = true;
} finally {
reset.setRunningMediaCodec(null);
mediaCodec.reset();
if (surface != null) {
surface.release();
@ -163,25 +179,16 @@ public class SurfaceEncoder implements AsyncProcessor {
return 0;
}
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException {
boolean eof = false;
boolean alive = true;
private void encode(MediaCodec codec, Streamer streamer) throws IOException {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!capture.consumeReset() && !eof) {
if (stopped.get()) {
alive = false;
break;
}
boolean eos;
do {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
try {
if (capture.consumeReset()) {
// must restart encoding with new size
break;
}
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (outputBufferId >= 0) {
eos = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
// On EOS, there might be data or not, depending on bufferInfo.size
if (outputBufferId >= 0 && bufferInfo.size > 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
@ -198,14 +205,7 @@ public class SurfaceEncoder implements AsyncProcessor {
codec.releaseOutputBuffer(outputBufferId, false);
}
}
}
if (capture.isClosed()) {
// The capture might have been closed internally (for example if the camera is disconnected)
alive = false;
}
return !eof && alive;
} while (!eos);
}
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
@ -298,6 +298,7 @@ public class SurfaceEncoder implements AsyncProcessor {
public void stop() {
if (thread != null) {
stopped.set(true);
reset.reset();
}
}

View File

@ -6,6 +6,7 @@ import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.IContentProvider;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
@ -64,7 +65,7 @@ public final class ActivityManager {
}
@TargetApi(AndroidVersions.API_29_ANDROID_10)
private ContentProvider getContentProviderExternal(String name, IBinder token) {
public IContentProvider getContentProviderExternal(String name, IBinder token) {
try {
Method method = getGetContentProviderExternalMethod();
Object[] args;
@ -83,11 +84,7 @@ public final class ActivityManager {
// IContentProvider provider = providerHolder.provider;
Field providerField = providerHolder.getClass().getDeclaredField("provider");
providerField.setAccessible(true);
Object provider = providerField.get(providerHolder);
if (provider == null) {
return null;
}
return new ContentProvider(this, provider, name, token);
return (IContentProvider) providerField.get(providerHolder);
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
return null;
@ -104,7 +101,12 @@ public final class ActivityManager {
}
public ContentProvider createSettingsProvider() {
return getContentProviderExternal("settings", new Binder());
IBinder token = new Binder();
IContentProvider provider = getContentProviderExternal("settings", token);
if (provider == null) {
return null;
}
return new ContentProvider(this, provider, "settings", token);
}
private Method getStartActivityAsUserMethod() throws NoSuchMethodException, ClassNotFoundException {

View File

@ -1,7 +1,9 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln;
import android.os.Build;
import android.os.IInterface;
import java.lang.reflect.Method;
@ -21,14 +23,22 @@ public final class PowerManager {
private Method getIsScreenOnMethod() throws NoSuchMethodException {
if (isScreenOnMethod == null) {
isScreenOnMethod = manager.getClass().getMethod("isInteractive");
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
isScreenOnMethod = manager.getClass().getMethod("isDisplayInteractive", int.class);
} else {
isScreenOnMethod = manager.getClass().getMethod("isInteractive");
}
}
return isScreenOnMethod;
}
public boolean isScreenOn() {
public boolean isScreenOn(int displayId) {
try {
Method method = getIsScreenOnMethod();
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
return (boolean) method.invoke(manager, displayId);
}
return (boolean) method.invoke(manager);
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);