Compare commits
20 Commits
opengl_fil
...
pr5499
Author | SHA1 | Date | |
---|---|---|---|
e411b74a16 | |||
5694562a74 | |||
bd9d93194b | |||
794595e3f0 | |||
5e10c37f02 | |||
0e399b65bd | |||
2337f524d1 | |||
df74cceb6f | |||
91373d906b | |||
c0e2e27cf9 | |||
eff5b4b219 | |||
d3db9c4065 | |||
5936167ff7 | |||
e9dd0f68ad | |||
104195fc3b | |||
9958302e6f | |||
69b836930a | |||
790ea5e58c | |||
1270997f6b | |||
c905fbba8d |
@ -77,6 +77,7 @@ _scrcpy() {
|
||||
--rotation=
|
||||
-s --serial=
|
||||
-S --turn-screen-off
|
||||
--screen-off-timeout=
|
||||
--shortcut-mod=
|
||||
--start-app=
|
||||
-t --show-touches
|
||||
|
@ -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]'
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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`
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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_
|
||||
|
@ -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..."
|
||||
|
@ -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
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user