Compare commits

..

3 Commits

Author SHA1 Message Date
Romain Vimont
434033d2c3 Fix PTS when not monotonically increasing
Some decoders fail to guarantee that PTS must be strictly
monotonically increasing. Fix the (rescaled) PTS when it does not
respect this constraint.

Fixes #4054 <https://github.com/Genymobile/scrcpy/issues/4054>
2023-06-03 15:20:55 +02:00
Romain Vimont
e849b6dbb5 Extract stream-specific structure in recorder
For now, it only contains the stream index, but more fields will be
added.
2023-06-03 15:18:21 +02:00
Romain Vimont
58f1057f01 Fix recorder waiting when stream disabled
In the recorder, if the video or audio stream is disabled, do not wait
for its initialization (it will never happen) to process the header.

In that case, this caused the whole content to be buffered in memory,
and written only on exit.
2023-06-03 15:16:34 +02:00
30 changed files with 52 additions and 377 deletions

View File

@@ -19,9 +19,8 @@ _scrcpy() {
-f --fullscreen -f --fullscreen
--force-adb-forward --force-adb-forward
--forward-all-clicks --forward-all-clicks
-h --help
--kill-adb-on-close
-K --hid-keyboard -K --hid-keyboard
-h --help
--legacy-paste --legacy-paste
--list-displays --list-displays
--list-encoders --list-encoders
@@ -60,7 +59,6 @@ _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=

View File

@@ -26,9 +26,8 @@ arguments=(
{-f,--fullscreen}'[Start in fullscreen]' {-f,--fullscreen}'[Start in fullscreen]'
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
'--forward-all-clicks[Forward clicks to device]' '--forward-all-clicks[Forward clicks to device]'
{-h,--help}'[Print the help]'
'--kill-adb-on-close[Kill adb when scrcpy terminates]'
{-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]'
{-h,--help}'[Print the help]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
'--list-displays[List displays available on the device]' '--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders available on the device]' '--list-encoders[List video and audio encoders available on the device]'
@@ -65,7 +64,6 @@ 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]'

View File

@@ -51,7 +51,6 @@ 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()

View File

@@ -129,10 +129,6 @@ By default, right-click triggers BACK (or POWER on) and middle-click triggers HO
.B \-h, \-\-help .B \-h, \-\-help
Print this help. Print this help.
.TP
.B \-\-kill\-adb\-on\-close
Kill adb when scrcpy terminates.
.TP .TP
.B \-K, \-\-hid\-keyboard .B \-K, \-\-hid\-keyboard
Simulate a physical keyboard by using HID over AOAv2. Simulate a physical keyboard by using HID over AOAv2.
@@ -354,10 +350,6 @@ 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.

View File

@@ -77,8 +77,6 @@ enum {
OPT_NO_AUDIO_PLAYBACK, OPT_NO_AUDIO_PLAYBACK,
OPT_NO_VIDEO_PLAYBACK, OPT_NO_VIDEO_PLAYBACK,
OPT_AUDIO_SOURCE, OPT_AUDIO_SOURCE,
OPT_KILL_ADB_ON_CLOSE,
OPT_TIME_LIMIT,
}; };
struct sc_option { struct sc_option {
@@ -277,16 +275,6 @@ static const struct sc_option options[] = {
"middle-click triggers HOME. This option disables these " "middle-click triggers HOME. This option disables these "
"shortcuts and forwards the clicks to the device instead.", "shortcuts and forwards the clicks to the device instead.",
}, },
{
.shortopt = 'h',
.longopt = "help",
.text = "Print this help.",
},
{
.longopt_id = OPT_KILL_ADB_ON_CLOSE,
.longopt = "kill-adb-on-close",
.text = "Kill adb when scrcpy terminates.",
},
{ {
.shortopt = 'K', .shortopt = 'K',
.longopt = "hid-keyboard", .longopt = "hid-keyboard",
@@ -304,6 +292,11 @@ static const struct sc_option options[] = {
"is enabled (or a physical keyboard is connected).\n" "is enabled (or a physical keyboard is connected).\n"
"Also see --hid-mouse.", "Also see --hid-mouse.",
}, },
{
.shortopt = 'h',
.longopt = "help",
.text = "Print this help.",
},
{ {
.longopt_id = OPT_LEGACY_PASTE, .longopt_id = OPT_LEGACY_PASTE,
.longopt = "legacy-paste", .longopt = "legacy-paste",
@@ -581,12 +574,6 @@ 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",
@@ -1625,18 +1612,6 @@ 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) {
@@ -1969,14 +1944,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false; return false;
} }
break; break;
case OPT_KILL_ADB_ON_CLOSE:
opts->kill_adb_on_close = true;
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;

View File

@@ -6,4 +6,3 @@
#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)

View File

@@ -42,7 +42,6 @@ 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,
@@ -81,5 +80,4 @@ const struct scrcpy_options scrcpy_options_default = {
.require_audio = false, .require_audio = false,
.list_encoders = false, .list_encoders = false,
.list_displays = false, .list_displays = false,
.kill_adb_on_close = false,
}; };

View File

@@ -142,7 +142,6 @@ 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;
@@ -181,7 +180,6 @@ struct scrcpy_options {
bool require_audio; bool require_audio;
bool list_encoders; bool list_encoders;
bool list_displays; bool list_displays;
bool kill_adb_on_close;
}; };
extern const struct scrcpy_options scrcpy_options_default; extern const struct scrcpy_options scrcpy_options_default;

View File

@@ -101,9 +101,8 @@ sc_recorder_write_stream(struct sc_recorder *recorder,
AVStream *stream = recorder->ctx->streams[st->index]; AVStream *stream = recorder->ctx->streams[st->index];
sc_recorder_rescale_packet(stream, packet); sc_recorder_rescale_packet(stream, packet);
if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) { if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) {
LOGW("Fixing PTS non monotonically increasing in stream %d " LOGW("Fixing PTS non monotonically increasing "
"(%" PRIi64 " >= %" PRIi64 ")", "(%" PRIi64 " >= %" PRIi64 ")", st->last_pts, packet->pts);
st->index, st->last_pts, packet->pts);
packet->pts = ++st->last_pts; packet->pts = ++st->last_pts;
packet->dts = packet->pts; packet->dts = packet->pts;
} else { } else {

View File

@@ -35,7 +35,6 @@
#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
@@ -74,7 +73,6 @@ 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
@@ -173,9 +171,6 @@ 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;
@@ -285,14 +280,6 @@ 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() {
@@ -334,8 +321,6 @@ 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;
@@ -379,7 +364,6 @@ scrcpy(struct scrcpy_options *options) {
.power_on = options->power_on, .power_on = options->power_on,
.list_encoders = options->list_encoders, .list_encoders = options->list_encoders,
.list_displays = options->list_displays, .list_displays = options->list_displays,
.kill_adb_on_close = options->kill_adb_on_close,
}; };
static const struct sc_server_callbacks cbs = { static const struct sc_server_callbacks cbs = {
@@ -758,27 +742,6 @@ 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...");
@@ -787,10 +750,6 @@ 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
@@ -826,13 +785,6 @@ 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) {

View File

@@ -794,15 +794,6 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server,
return sc_server_connect_to_tcpip(server, ip_port); return sc_server_connect_to_tcpip(server, ip_port);
} }
static void
sc_server_kill_adb_if_requested(struct sc_server *server) {
if (server->params.kill_adb_on_close) {
LOGI("Killing adb server...");
unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR;
sc_adb_kill_server(&server->intr, flags);
}
}
static int static int
run_server(void *data) { run_server(void *data) {
struct sc_server *server = data; struct sc_server *server = data;
@@ -814,7 +805,7 @@ run_server(void *data) {
// is parsed, so it is not output) // is parsed, so it is not output)
bool ok = sc_adb_start_server(&server->intr, 0); bool ok = sc_adb_start_server(&server->intr, 0);
if (!ok) { if (!ok) {
LOGE("Could not start adb server"); LOGE("Could not start adb daemon");
goto error_connection_failed; goto error_connection_failed;
} }
@@ -1002,12 +993,9 @@ run_server(void *data) {
sc_process_close(pid); sc_process_close(pid);
sc_server_kill_adb_if_requested(server);
return 0; return 0;
error_connection_failed: error_connection_failed:
sc_server_kill_adb_if_requested(server);
server->cbs->on_connection_failed(server, server->cbs_userdata); server->cbs->on_connection_failed(server, server->cbs_userdata);
return -1; return -1;
} }

View File

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

View File

@@ -83,7 +83,7 @@ scrcpy_otg(struct scrcpy_options *options) {
#ifdef _WIN32 #ifdef _WIN32
// On Windows, only one process could open a USB device // On Windows, only one process could open a USB device
// <https://github.com/Genymobile/scrcpy/issues/2773> // <https://github.com/Genymobile/scrcpy/issues/2773>
LOGI("Killing adb server (if any)..."); LOGI("Killing adb daemon (if any)...");
unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR;
// uninterruptible (intr == NULL), but in practice it's very quick // uninterruptible (intr == NULL), but in practice it's very quick
sc_adb_kill_server(NULL, flags); sc_adb_kill_server(NULL, flags);

View File

@@ -1,77 +0,0 @@
#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);
}

View File

@@ -1,43 +0,0 @@
#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

View File

@@ -56,7 +56,7 @@ For example, to use the device as a dictaphone and record a capture directly on
the computer: the computer:
``` ```
scrcpy --audio-source=mic --no-video --no-playback --record=file.opus scrcpy --audio-source=mic --no-video --no-audio-playback --record=file.opus
``` ```

View File

@@ -17,19 +17,24 @@ To record only the audio:
```bash ```bash
scrcpy --no-video --record=file.opus scrcpy --no-video --record=file.opus
scrcpy --no-video --audio-codec=aac --record=file.aac scrcpy --no-video --audio-codec=aac --record-file=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`)
@@ -43,36 +48,3 @@ 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
```

View File

@@ -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("media-codec"); mediaCodecThread = new HandlerThread("AudioEncoder");
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;

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,11 @@
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";
@@ -18,25 +13,12 @@ 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(retrieveSystemContext()); super(null);
} }
@Override @Override

View File

@@ -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_stream": case "raw_video_stream":
boolean rawStream = Boolean.parseBoolean(value); boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawStream) { if (rawVideoStream) {
options.sendDeviceMeta = false; options.sendDeviceMeta = false;
options.sendFrameMeta = false; options.sendFrameMeta = false;
options.sendDummyByte = false; options.sendDummyByte = false;

View File

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

View File

@@ -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.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); Ln.i("Device: " + Build.MANUFACTURER + " " + 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") || Build.BRAND.equalsIgnoreCase("honor")) { if (Build.BRAND.equalsIgnoreCase("meizu")) {
Workarounds.fillAppInfo(); Workarounds.fillAppInfo();
} }
@@ -137,7 +137,8 @@ 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(), options.getSendFrameMeta()); Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(),
options.getSendFrameMeta());
AsyncProcessor audioRecorder; AsyncProcessor audioRecorder;
if (audioCodec == AudioCodec.RAW) { if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer); audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
@@ -184,7 +185,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), "init-cleanup"); Thread thread = new Thread(() -> initAndCleanUp(options));
thread.start(); thread.start();
return thread; return thread;
} }

View File

@@ -1,7 +1,5 @@
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;
@@ -22,7 +20,8 @@ import java.lang.reflect.Method;
public final class Workarounds { public final class Workarounds {
private static boolean activityThreadFilled; private static Class<?> activityThreadClass;
private static Object activityThread;
private Workarounds() { private Workarounds() {
// not instantiable // not instantiable
@@ -43,16 +42,17 @@ public final class Workarounds {
@SuppressLint("PrivateApi,DiscouragedPrivateApi") @SuppressLint("PrivateApi,DiscouragedPrivateApi")
private static void fillActivityThread() throws Exception { private static void fillActivityThread() throws Exception {
if (!activityThreadFilled) { if (activityThread == null) {
Class<?> activityThreadClass = ActivityThread.getActivityThreadClass(); // ActivityThread activityThread = new ActivityThread();
Object activityThread = ActivityThread.getActivityThread(); activityThreadClass = Class.forName("android.app.ActivityThread");
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,9 +75,6 @@ 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);
@@ -98,9 +95,6 @@ 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);
@@ -112,7 +106,7 @@ public final class Workarounds {
} }
@TargetApi(Build.VERSION_CODES.R) @TargetApi(Build.VERSION_CODES.R)
@SuppressLint("WrongConstant,MissingPermission,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") @SuppressLint({"WrongConstant", "MissingPermission", "BlockedPrivateApi", "SoonBlockedPrivateApi"})
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.
// //

View File

@@ -1,32 +0,0 @@
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;
}
}

View File

@@ -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 Object manager; private final android.hardware.input.InputManager 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(Object manager) { public InputManager(android.hardware.input.InputManager manager) {
this.manager = manager; this.manager = manager;
} }

View File

@@ -62,21 +62,11 @@ 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 {
Class<?> inputManagerClass = getInputManagerClass(); Method getInstanceMethod = android.hardware.input.InputManager.class.getDeclaredMethod("getInstance");
Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); android.hardware.input.InputManager im = (android.hardware.input.InputManager) getInstanceMethod.invoke(null);
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);

View File

@@ -12,6 +12,7 @@ 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