Compare commits
12 Commits
pr4075
...
activity_t
Author | SHA1 | Date | |
---|---|---|---|
9bff8ccadb | |||
0d4157357a | |||
95e61e2a0b | |||
48a00fb481 | |||
3b7e2ca9c8 | |||
5bd7514871 | |||
d3c2955fb9 | |||
5042f8de93 | |||
7536f95d1c | |||
6832e8d629 | |||
28313631e5 | |||
fdbc9397a7 |
@ -60,6 +60,7 @@ _scrcpy() {
|
|||||||
-t --show-touches
|
-t --show-touches
|
||||||
--tcpip
|
--tcpip
|
||||||
--tcpip=
|
--tcpip=
|
||||||
|
--time-limit=
|
||||||
--tunnel-host=
|
--tunnel-host=
|
||||||
--tunnel-port=
|
--tunnel-port=
|
||||||
--v4l2-buffer=
|
--v4l2-buffer=
|
||||||
|
@ -65,6 +65,7 @@ arguments=(
|
|||||||
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
|
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
|
||||||
{-t,--show-touches}'[Show physical touches]'
|
{-t,--show-touches}'[Show physical touches]'
|
||||||
'--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]'
|
'--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]'
|
||||||
|
'--time-limit=[Set the maximum mirroring time, in seconds]'
|
||||||
'--tunnel-host=[Set the IP address of the adb tunnel to reach the scrcpy server]'
|
'--tunnel-host=[Set the IP address of the adb tunnel to reach the scrcpy server]'
|
||||||
'--tunnel-port=[Set the TCP port of the adb tunnel to reach the scrcpy server]'
|
'--tunnel-port=[Set the TCP port of the adb tunnel to reach the scrcpy server]'
|
||||||
'--v4l2-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]'
|
'--v4l2-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]'
|
||||||
|
@ -51,6 +51,7 @@ src = [
|
|||||||
'src/util/term.c',
|
'src/util/term.c',
|
||||||
'src/util/thread.c',
|
'src/util/thread.c',
|
||||||
'src/util/tick.c',
|
'src/util/tick.c',
|
||||||
|
'src/util/timeout.c',
|
||||||
]
|
]
|
||||||
|
|
||||||
conf = configuration_data()
|
conf = configuration_data()
|
||||||
|
@ -354,6 +354,10 @@ If a destination address is provided, then scrcpy connects to this address befor
|
|||||||
|
|
||||||
If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting.
|
If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-time\-limit " seconds
|
||||||
|
Set the maximum mirroring time, in seconds.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-tunnel\-host " ip
|
.BI "\-\-tunnel\-host " ip
|
||||||
Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward.
|
Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward.
|
||||||
|
@ -78,6 +78,7 @@ enum {
|
|||||||
OPT_NO_VIDEO_PLAYBACK,
|
OPT_NO_VIDEO_PLAYBACK,
|
||||||
OPT_AUDIO_SOURCE,
|
OPT_AUDIO_SOURCE,
|
||||||
OPT_KILL_ADB_ON_CLOSE,
|
OPT_KILL_ADB_ON_CLOSE,
|
||||||
|
OPT_TIME_LIMIT,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_option {
|
struct sc_option {
|
||||||
@ -580,6 +581,12 @@ static const struct sc_option options[] = {
|
|||||||
"connected over USB), enables TCP/IP mode, then connects to "
|
"connected over USB), enables TCP/IP mode, then connects to "
|
||||||
"this address before starting.",
|
"this address before starting.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_TIME_LIMIT,
|
||||||
|
.longopt = "time-limit",
|
||||||
|
.argdesc = "seconds",
|
||||||
|
.text = "Set the maximum mirroring time, in seconds.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_TUNNEL_HOST,
|
.longopt_id = OPT_TUNNEL_HOST,
|
||||||
.longopt = "tunnel-host",
|
.longopt = "tunnel-host",
|
||||||
@ -1618,6 +1625,18 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
parse_time_limit(const char *s, sc_tick *tick) {
|
||||||
|
long value;
|
||||||
|
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "time limit");
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*tick = SC_TICK_FROM_SEC(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||||
const char *optstring, const struct option *longopts) {
|
const char *optstring, const struct option *longopts) {
|
||||||
@ -1953,6 +1972,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
case OPT_KILL_ADB_ON_CLOSE:
|
case OPT_KILL_ADB_ON_CLOSE:
|
||||||
opts->kill_adb_on_close = true;
|
opts->kill_adb_on_close = true;
|
||||||
break;
|
break;
|
||||||
|
case OPT_TIME_LIMIT:
|
||||||
|
if (!parse_time_limit(optarg, &opts->time_limit)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
|
@ -6,3 +6,4 @@
|
|||||||
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
|
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
|
||||||
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
|
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
|
||||||
#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7)
|
#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7)
|
||||||
|
#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8)
|
||||||
|
@ -42,6 +42,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.display_buffer = 0,
|
.display_buffer = 0,
|
||||||
.audio_buffer = SC_TICK_FROM_MS(50),
|
.audio_buffer = SC_TICK_FROM_MS(50),
|
||||||
.audio_output_buffer = SC_TICK_FROM_MS(5),
|
.audio_output_buffer = SC_TICK_FROM_MS(5),
|
||||||
|
.time_limit = 0,
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
.v4l2_device = NULL,
|
.v4l2_device = NULL,
|
||||||
.v4l2_buffer = 0,
|
.v4l2_buffer = 0,
|
||||||
|
@ -142,6 +142,7 @@ struct scrcpy_options {
|
|||||||
sc_tick display_buffer;
|
sc_tick display_buffer;
|
||||||
sc_tick audio_buffer;
|
sc_tick audio_buffer;
|
||||||
sc_tick audio_output_buffer;
|
sc_tick audio_output_buffer;
|
||||||
|
sc_tick time_limit;
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
const char *v4l2_device;
|
const char *v4l2_device;
|
||||||
sc_tick v4l2_buffer;
|
sc_tick v4l2_buffer;
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
#include "util/net.h"
|
#include "util/net.h"
|
||||||
#include "util/rand.h"
|
#include "util/rand.h"
|
||||||
|
#include "util/timeout.h"
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
# include "v4l2_sink.h"
|
# include "v4l2_sink.h"
|
||||||
#endif
|
#endif
|
||||||
@ -73,6 +74,7 @@ struct scrcpy {
|
|||||||
struct sc_hid_mouse mouse_hid;
|
struct sc_hid_mouse mouse_hid;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
struct sc_timeout timeout;
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
@ -171,6 +173,9 @@ event_loop(struct scrcpy *s) {
|
|||||||
case SC_EVENT_RECORDER_ERROR:
|
case SC_EVENT_RECORDER_ERROR:
|
||||||
LOGE("Recorder error");
|
LOGE("Recorder error");
|
||||||
return SCRCPY_EXIT_FAILURE;
|
return SCRCPY_EXIT_FAILURE;
|
||||||
|
case SC_EVENT_TIME_LIMIT_REACHED:
|
||||||
|
LOGI("Time limit reached");
|
||||||
|
return SCRCPY_EXIT_SUCCESS;
|
||||||
case SDL_QUIT:
|
case SDL_QUIT:
|
||||||
LOGD("User requested to quit");
|
LOGD("User requested to quit");
|
||||||
return SCRCPY_EXIT_SUCCESS;
|
return SCRCPY_EXIT_SUCCESS;
|
||||||
@ -280,6 +285,14 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
|
|||||||
// event
|
// event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_timeout_on_timeout(struct sc_timeout *timeout, void *userdata) {
|
||||||
|
(void) timeout;
|
||||||
|
(void) userdata;
|
||||||
|
|
||||||
|
PUSH_EVENT(SC_EVENT_TIME_LIMIT_REACHED);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a scrcpy id to differentiate multiple running scrcpy instances
|
// Generate a scrcpy id to differentiate multiple running scrcpy instances
|
||||||
static uint32_t
|
static uint32_t
|
||||||
scrcpy_generate_scid() {
|
scrcpy_generate_scid() {
|
||||||
@ -321,6 +334,8 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
bool controller_initialized = false;
|
bool controller_initialized = false;
|
||||||
bool controller_started = false;
|
bool controller_started = false;
|
||||||
bool screen_initialized = false;
|
bool screen_initialized = false;
|
||||||
|
bool timeout_initialized = false;
|
||||||
|
bool timeout_started = false;
|
||||||
|
|
||||||
struct sc_acksync *acksync = NULL;
|
struct sc_acksync *acksync = NULL;
|
||||||
|
|
||||||
@ -743,6 +758,27 @@ aoa_hid_end:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options->time_limit) {
|
||||||
|
bool ok = sc_timeout_init(&s->timeout);
|
||||||
|
if (!ok) {
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout_initialized = true;
|
||||||
|
|
||||||
|
sc_tick deadline = sc_tick_now() + options->time_limit;
|
||||||
|
static const struct sc_timeout_callbacks cbs = {
|
||||||
|
.on_timeout = sc_timeout_on_timeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
ok = sc_timeout_start(&s->timeout, deadline, &cbs, NULL);
|
||||||
|
if (!ok) {
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout_started = true;
|
||||||
|
}
|
||||||
|
|
||||||
ret = event_loop(s);
|
ret = event_loop(s);
|
||||||
LOGD("quit...");
|
LOGD("quit...");
|
||||||
|
|
||||||
@ -751,6 +787,10 @@ aoa_hid_end:
|
|||||||
sc_screen_hide_window(&s->screen);
|
sc_screen_hide_window(&s->screen);
|
||||||
|
|
||||||
end:
|
end:
|
||||||
|
if (timeout_started) {
|
||||||
|
sc_timeout_stop(&s->timeout);
|
||||||
|
}
|
||||||
|
|
||||||
// The demuxer is not stopped explicitly, because it will stop by itself on
|
// The demuxer is not stopped explicitly, because it will stop by itself on
|
||||||
// end-of-stream
|
// end-of-stream
|
||||||
#ifdef HAVE_USB
|
#ifdef HAVE_USB
|
||||||
@ -786,6 +826,13 @@ end:
|
|||||||
sc_server_stop(&s->server);
|
sc_server_stop(&s->server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timeout_started) {
|
||||||
|
sc_timeout_join(&s->timeout);
|
||||||
|
}
|
||||||
|
if (timeout_initialized) {
|
||||||
|
sc_timeout_destroy(&s->timeout);
|
||||||
|
}
|
||||||
|
|
||||||
// now that the sockets are shutdown, the demuxer and controller are
|
// now that the sockets are shutdown, the demuxer and controller are
|
||||||
// interrupted, we can join them
|
// interrupted, we can join them
|
||||||
if (video_demuxer_started) {
|
if (video_demuxer_started) {
|
||||||
|
77
app/src/util/timeout.c
Normal file
77
app/src/util/timeout.c
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
#include "timeout.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_timeout_init(struct sc_timeout *timeout) {
|
||||||
|
bool ok = sc_mutex_init(&timeout->mutex);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = sc_cond_init(&timeout->cond);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout->stopped = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
run_timeout(void *data) {
|
||||||
|
struct sc_timeout *timeout = data;
|
||||||
|
sc_tick deadline = timeout->deadline;
|
||||||
|
|
||||||
|
sc_mutex_lock(&timeout->mutex);
|
||||||
|
bool timed_out = false;
|
||||||
|
while (!timeout->stopped && !timed_out) {
|
||||||
|
timed_out = !sc_cond_timedwait(&timeout->cond, &timeout->mutex,
|
||||||
|
deadline);
|
||||||
|
}
|
||||||
|
sc_mutex_unlock(&timeout->mutex);
|
||||||
|
|
||||||
|
timeout->cbs->on_timeout(timeout, timeout->cbs_userdata);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline,
|
||||||
|
const struct sc_timeout_callbacks *cbs, void *cbs_userdata) {
|
||||||
|
bool ok = sc_thread_create(&timeout->thread, run_timeout, "scrcpy-timeout",
|
||||||
|
timeout);
|
||||||
|
if (!ok) {
|
||||||
|
LOGE("Timeout: could not start thread");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout->deadline = deadline;
|
||||||
|
|
||||||
|
assert(cbs && cbs->on_timeout);
|
||||||
|
timeout->cbs = cbs;
|
||||||
|
timeout->cbs_userdata = cbs_userdata;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_timeout_stop(struct sc_timeout *timeout) {
|
||||||
|
sc_mutex_lock(&timeout->mutex);
|
||||||
|
timeout->stopped = true;
|
||||||
|
sc_mutex_unlock(&timeout->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_timeout_join(struct sc_timeout *timeout) {
|
||||||
|
sc_thread_join(&timeout->thread, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_timeout_destroy(struct sc_timeout *timeout) {
|
||||||
|
sc_mutex_destroy(&timeout->mutex);
|
||||||
|
sc_cond_destroy(&timeout->cond);
|
||||||
|
}
|
43
app/src/util/timeout.h
Normal file
43
app/src/util/timeout.h
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#ifndef SC_TIMEOUT_H
|
||||||
|
#define SC_TIMEOUT_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include "thread.h"
|
||||||
|
#include "tick.h"
|
||||||
|
|
||||||
|
struct sc_timeout {
|
||||||
|
sc_thread thread;
|
||||||
|
sc_tick deadline;
|
||||||
|
|
||||||
|
sc_mutex mutex;
|
||||||
|
sc_cond cond;
|
||||||
|
bool stopped;
|
||||||
|
|
||||||
|
const struct sc_timeout_callbacks *cbs;
|
||||||
|
void *cbs_userdata;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct sc_timeout_callbacks {
|
||||||
|
void (*on_timeout)(struct sc_timeout *timeout, void *userdata);
|
||||||
|
};
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_timeout_init(struct sc_timeout *timeout);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_timeout_destroy(struct sc_timeout *timeout);
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline,
|
||||||
|
const struct sc_timeout_callbacks *cbs, void *cbs_userdata);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_timeout_stop(struct sc_timeout *timeout);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_timeout_join(struct sc_timeout *timeout);
|
||||||
|
|
||||||
|
#endif
|
@ -21,20 +21,15 @@ scrcpy --no-video --audio-codec=aac --record=file.aac
|
|||||||
# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac
|
# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac
|
||||||
```
|
```
|
||||||
|
|
||||||
To disable playback while recording:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --no-playback --record=file.mp4
|
|
||||||
scrcpy -Nr file.mkv
|
|
||||||
# interrupt recording with Ctrl+C
|
|
||||||
```
|
|
||||||
|
|
||||||
Timestamps are captured on the device, so [packet delay variation] does not
|
Timestamps are captured on the device, so [packet delay variation] does not
|
||||||
impact the recorded file, which is always clean (only if you use `--record` of
|
impact the recorded file, which is always clean (only if you use `--record` of
|
||||||
course, not if you capture your scrcpy window and audio output on the computer).
|
course, not if you capture your scrcpy window and audio output on the computer).
|
||||||
|
|
||||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||||
|
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
The video and audio streams are encoded on the device, but are muxed on the
|
The video and audio streams are encoded on the device, but are muxed on the
|
||||||
client side. Two formats (containers) are supported:
|
client side. Two formats (containers) are supported:
|
||||||
- Matroska (`.mkv`)
|
- Matroska (`.mkv`)
|
||||||
@ -48,3 +43,36 @@ needs not end with `.mkv` or `.mp4`):
|
|||||||
```
|
```
|
||||||
scrcpy --record=file --record-format=mkv
|
scrcpy --record=file --record-format=mkv
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## No playback
|
||||||
|
|
||||||
|
To disable playback while recording:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --no-playback --record=file.mp4
|
||||||
|
scrcpy -Nr file.mkv
|
||||||
|
# interrupt recording with Ctrl+C
|
||||||
|
```
|
||||||
|
|
||||||
|
It is also possible to disable video and audio playback separately:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Record both video and audio, but only play video
|
||||||
|
scrcpy --record=file.mkv --no-audio-playback
|
||||||
|
```
|
||||||
|
|
||||||
|
## Time limit
|
||||||
|
|
||||||
|
To limit the recording time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --record=file.mkv --time-limit=20 # in seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--time-limit` option is not limited to recording, it also impacts simple
|
||||||
|
mirroring:
|
||||||
|
|
||||||
|
```
|
||||||
|
scrcpy --time-limit=20
|
||||||
|
```
|
||||||
|
@ -134,7 +134,7 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
Ln.d("Audio encoder stopped");
|
Ln.d("Audio encoder stopped");
|
||||||
listener.onTerminated(fatalError);
|
listener.onTerminated(fatalError);
|
||||||
}
|
}
|
||||||
});
|
}, "audio-encoder");
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +183,7 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
Codec codec = streamer.getCodec();
|
Codec codec = streamer.getCodec();
|
||||||
mediaCodec = createMediaCodec(codec, encoderName);
|
mediaCodec = createMediaCodec(codec, encoderName);
|
||||||
|
|
||||||
mediaCodecThread = new HandlerThread("AudioEncoder");
|
mediaCodecThread = new HandlerThread("media-codec");
|
||||||
mediaCodecThread.start();
|
mediaCodecThread.start();
|
||||||
|
|
||||||
MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions);
|
MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions);
|
||||||
@ -201,7 +201,7 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
} finally {
|
} finally {
|
||||||
end();
|
end();
|
||||||
}
|
}
|
||||||
});
|
}, "audio-in");
|
||||||
|
|
||||||
outputThread = new Thread(() -> {
|
outputThread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
@ -216,7 +216,7 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
} finally {
|
} finally {
|
||||||
end();
|
end();
|
||||||
}
|
}
|
||||||
});
|
}, "audio-out");
|
||||||
|
|
||||||
mediaCodec.start();
|
mediaCodec.start();
|
||||||
mediaCodecStarted = true;
|
mediaCodecStarted = true;
|
||||||
|
@ -69,7 +69,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
|
|||||||
Ln.d("Audio recorder stopped");
|
Ln.d("Audio recorder stopped");
|
||||||
listener.onTerminated(fatalError);
|
listener.onTerminated(fatalError);
|
||||||
}
|
}
|
||||||
});
|
}, "audio-raw");
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ public class Controller implements AsyncProcessor {
|
|||||||
Ln.d("Controller stopped");
|
Ln.d("Controller stopped");
|
||||||
listener.onTerminated(true);
|
listener.onTerminated(true);
|
||||||
}
|
}
|
||||||
});
|
}, "control-recv");
|
||||||
thread.start();
|
thread.start();
|
||||||
sender.start();
|
sender.start();
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ public final class DeviceMessageSender {
|
|||||||
} finally {
|
} finally {
|
||||||
Ln.d("Device message sender stopped");
|
Ln.d("Device message sender stopped");
|
||||||
}
|
}
|
||||||
});
|
}, "control-send");
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.wrappers.ActivityThread;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.AttributionSource;
|
import android.content.AttributionSource;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.ContextWrapper;
|
import android.content.ContextWrapper;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
public final class FakeContext extends ContextWrapper {
|
public final class FakeContext extends ContextWrapper {
|
||||||
|
|
||||||
public static final String PACKAGE_NAME = "com.android.shell";
|
public static final String PACKAGE_NAME = "com.android.shell";
|
||||||
@ -13,12 +18,25 @@ public final class FakeContext extends ContextWrapper {
|
|||||||
|
|
||||||
private static final FakeContext INSTANCE = new FakeContext();
|
private static final FakeContext INSTANCE = new FakeContext();
|
||||||
|
|
||||||
|
private static Context retrieveSystemContext() {
|
||||||
|
try {
|
||||||
|
Class<?> activityThreadClass = ActivityThread.getActivityThreadClass();
|
||||||
|
Object activityThread = ActivityThread.getActivityThread();
|
||||||
|
|
||||||
|
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
|
||||||
|
return (Context) getSystemContextMethod.invoke(activityThread);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Ln.e("Cannot retrieve system context", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static FakeContext get() {
|
public static FakeContext get() {
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FakeContext() {
|
private FakeContext() {
|
||||||
super(null);
|
super(retrieveSystemContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -318,9 +318,9 @@ public class Options {
|
|||||||
case "send_codec_meta":
|
case "send_codec_meta":
|
||||||
options.sendCodecMeta = Boolean.parseBoolean(value);
|
options.sendCodecMeta = Boolean.parseBoolean(value);
|
||||||
break;
|
break;
|
||||||
case "raw_video_stream":
|
case "raw_stream":
|
||||||
boolean rawVideoStream = Boolean.parseBoolean(value);
|
boolean rawStream = Boolean.parseBoolean(value);
|
||||||
if (rawVideoStream) {
|
if (rawStream) {
|
||||||
options.sendDeviceMeta = false;
|
options.sendDeviceMeta = false;
|
||||||
options.sendFrameMeta = false;
|
options.sendFrameMeta = false;
|
||||||
options.sendDummyByte = false;
|
options.sendDummyByte = false;
|
||||||
|
@ -299,7 +299,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
|
|||||||
Ln.d("Screen streaming stopped");
|
Ln.d("Screen streaming stopped");
|
||||||
listener.onTerminated(true);
|
listener.onTerminated(true);
|
||||||
}
|
}
|
||||||
});
|
}, "video");
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ public final class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void scrcpy(Options options) throws IOException, ConfigurationException {
|
private static void scrcpy(Options options) throws IOException, ConfigurationException {
|
||||||
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
||||||
final Device device = new Device(options);
|
final Device device = new Device(options);
|
||||||
|
|
||||||
Thread initThread = startInitThread(options);
|
Thread initThread = startInitThread(options);
|
||||||
@ -109,7 +109,7 @@ public final class Server {
|
|||||||
// But only apply when strictly necessary, since workarounds can cause other issues:
|
// But only apply when strictly necessary, since workarounds can cause other issues:
|
||||||
// - <https://github.com/Genymobile/scrcpy/issues/940>
|
// - <https://github.com/Genymobile/scrcpy/issues/940>
|
||||||
// - <https://github.com/Genymobile/scrcpy/issues/994>
|
// - <https://github.com/Genymobile/scrcpy/issues/994>
|
||||||
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
if (Build.BRAND.equalsIgnoreCase("meizu") || Build.BRAND.equalsIgnoreCase("honor")) {
|
||||||
Workarounds.fillAppInfo();
|
Workarounds.fillAppInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,8 +137,7 @@ public final class Server {
|
|||||||
if (audio) {
|
if (audio) {
|
||||||
AudioCodec audioCodec = options.getAudioCodec();
|
AudioCodec audioCodec = options.getAudioCodec();
|
||||||
AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
|
AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
|
||||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(),
|
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
|
||||||
options.getSendFrameMeta());
|
|
||||||
AsyncProcessor audioRecorder;
|
AsyncProcessor audioRecorder;
|
||||||
if (audioCodec == AudioCodec.RAW) {
|
if (audioCodec == AudioCodec.RAW) {
|
||||||
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
|
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
|
||||||
@ -185,7 +184,7 @@ public final class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Thread startInitThread(final Options options) {
|
private static Thread startInitThread(final Options options) {
|
||||||
Thread thread = new Thread(() -> initAndCleanUp(options));
|
Thread thread = new Thread(() -> initAndCleanUp(options), "init-cleanup");
|
||||||
thread.start();
|
thread.start();
|
||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.wrappers.ActivityThread;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
@ -20,8 +22,7 @@ import java.lang.reflect.Method;
|
|||||||
|
|
||||||
public final class Workarounds {
|
public final class Workarounds {
|
||||||
|
|
||||||
private static Class<?> activityThreadClass;
|
private static boolean activityThreadFilled;
|
||||||
private static Object activityThread;
|
|
||||||
|
|
||||||
private Workarounds() {
|
private Workarounds() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
@ -42,17 +43,16 @@ public final class Workarounds {
|
|||||||
|
|
||||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||||
private static void fillActivityThread() throws Exception {
|
private static void fillActivityThread() throws Exception {
|
||||||
if (activityThread == null) {
|
if (!activityThreadFilled) {
|
||||||
// ActivityThread activityThread = new ActivityThread();
|
Class<?> activityThreadClass = ActivityThread.getActivityThreadClass();
|
||||||
activityThreadClass = Class.forName("android.app.ActivityThread");
|
Object activityThread = ActivityThread.getActivityThread();
|
||||||
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
|
|
||||||
activityThreadConstructor.setAccessible(true);
|
|
||||||
activityThread = activityThreadConstructor.newInstance();
|
|
||||||
|
|
||||||
// ActivityThread.sCurrentActivityThread = activityThread;
|
// ActivityThread.sCurrentActivityThread = activityThread;
|
||||||
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
|
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
|
||||||
sCurrentActivityThreadField.setAccessible(true);
|
sCurrentActivityThreadField.setAccessible(true);
|
||||||
sCurrentActivityThreadField.set(null, activityThread);
|
sCurrentActivityThreadField.set(null, activityThread);
|
||||||
|
|
||||||
|
activityThreadFilled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +75,9 @@ public final class Workarounds {
|
|||||||
appInfoField.setAccessible(true);
|
appInfoField.setAccessible(true);
|
||||||
appInfoField.set(appBindData, applicationInfo);
|
appInfoField.set(appBindData, applicationInfo);
|
||||||
|
|
||||||
|
Class<?> activityThreadClass = ActivityThread.getActivityThreadClass();
|
||||||
|
Object activityThread = ActivityThread.getActivityThread();
|
||||||
|
|
||||||
// activityThread.mBoundApplication = appBindData;
|
// activityThread.mBoundApplication = appBindData;
|
||||||
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
|
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
|
||||||
mBoundApplicationField.setAccessible(true);
|
mBoundApplicationField.setAccessible(true);
|
||||||
@ -95,6 +98,9 @@ public final class Workarounds {
|
|||||||
baseField.setAccessible(true);
|
baseField.setAccessible(true);
|
||||||
baseField.set(app, FakeContext.get());
|
baseField.set(app, FakeContext.get());
|
||||||
|
|
||||||
|
Class<?> activityThreadClass = ActivityThread.getActivityThreadClass();
|
||||||
|
Object activityThread = ActivityThread.getActivityThread();
|
||||||
|
|
||||||
// activityThread.mInitialApplication = app;
|
// activityThread.mInitialApplication = app;
|
||||||
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
||||||
mInitialApplicationField.setAccessible(true);
|
mInitialApplicationField.setAccessible(true);
|
||||||
@ -106,7 +112,7 @@ public final class Workarounds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.R)
|
@TargetApi(Build.VERSION_CODES.R)
|
||||||
@SuppressLint({"WrongConstant", "MissingPermission", "BlockedPrivateApi", "SoonBlockedPrivateApi"})
|
@SuppressLint("WrongConstant,MissingPermission,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi")
|
||||||
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) {
|
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) {
|
||||||
// Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment.
|
// Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment.
|
||||||
//
|
//
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
|
||||||
|
public class ActivityThread {
|
||||||
|
|
||||||
|
private static final Class<?> activityThreadClass;
|
||||||
|
private static final Object activityThread;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||||
|
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
|
||||||
|
activityThreadConstructor.setAccessible(true);
|
||||||
|
activityThread = activityThreadConstructor.newInstance();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActivityThread() {
|
||||||
|
// only static methods
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object getActivityThread() {
|
||||||
|
return activityThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Class<?> getActivityThreadClass() {
|
||||||
|
return activityThreadClass;
|
||||||
|
}
|
||||||
|
}
|
@ -14,13 +14,13 @@ public final class InputManager {
|
|||||||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
|
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
|
||||||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
|
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
|
||||||
|
|
||||||
private final android.hardware.input.InputManager manager;
|
private final Object manager;
|
||||||
private Method injectInputEventMethod;
|
private Method injectInputEventMethod;
|
||||||
|
|
||||||
private static Method setDisplayIdMethod;
|
private static Method setDisplayIdMethod;
|
||||||
private static Method setActionButtonMethod;
|
private static Method setActionButtonMethod;
|
||||||
|
|
||||||
public InputManager(android.hardware.input.InputManager manager) {
|
public InputManager(Object manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,11 +62,21 @@ public final class ServiceManager {
|
|||||||
return displayManager;
|
return displayManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Class<?> getInputManagerClass() {
|
||||||
|
try {
|
||||||
|
// Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview
|
||||||
|
return Class.forName("android.hardware.input.InputManagerGlobal");
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
return android.hardware.input.InputManager.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static InputManager getInputManager() {
|
public static InputManager getInputManager() {
|
||||||
if (inputManager == null) {
|
if (inputManager == null) {
|
||||||
try {
|
try {
|
||||||
Method getInstanceMethod = android.hardware.input.InputManager.class.getDeclaredMethod("getInstance");
|
Class<?> inputManagerClass = getInputManagerClass();
|
||||||
android.hardware.input.InputManager im = (android.hardware.input.InputManager) getInstanceMethod.invoke(null);
|
Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
|
||||||
|
Object im = getInstanceMethod.invoke(null);
|
||||||
inputManager = new InputManager(im);
|
inputManager = new InputManager(im);
|
||||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
|
@ -12,7 +12,6 @@ import java.io.IOException;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
|
||||||
public class ControlMessageReaderTest {
|
public class ControlMessageReaderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
Reference in New Issue
Block a user