diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index a34a1a44..003f9d73 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -60,6 +60,7 @@ _scrcpy() { -t --show-touches --tcpip --tcpip= + --time-limit= --tunnel-host= --tunnel-port= --v4l2-buffer= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 325ccb76..81142851 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -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)' {-t,--show-touches}'[Show physical touches]' '--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-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]' diff --git a/app/meson.build b/app/meson.build index 061fdcab..e0d92050 100644 --- a/app/meson.build +++ b/app/meson.build @@ -51,6 +51,7 @@ src = [ 'src/util/term.c', 'src/util/thread.c', 'src/util/tick.c', + 'src/util/timeout.c', ] conf = configuration_data() diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 21c3ac8f..0c91701f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -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. +.TP +.BI "\-\-time\-limit " seconds +Set the maximum mirroring time, in seconds. + .TP .BI "\-\-tunnel\-host " ip Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward. diff --git a/app/src/cli.c b/app/src/cli.c index c9b818a1..72f4bea1 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -78,6 +78,7 @@ enum { OPT_NO_VIDEO_PLAYBACK, OPT_AUDIO_SOURCE, OPT_KILL_ADB_ON_CLOSE, + OPT_TIME_LIMIT, }; struct sc_option { @@ -580,6 +581,12 @@ static const struct sc_option options[] = { "connected over USB), enables TCP/IP mode, then connects to " "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 = "tunnel-host", @@ -1618,6 +1625,18 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) { 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 parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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: opts->kill_adb_on_close = true; break; + case OPT_TIME_LIMIT: + if (!parse_time_limit(optarg, &opts->time_limit)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/events.h b/app/src/events.h index 609e3198..8bfa2582 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -6,3 +6,4 @@ #define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5) #define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6) #define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7) +#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8) diff --git a/app/src/options.c b/app/src/options.c index 30b5cb56..530e003b 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -42,6 +42,7 @@ const struct scrcpy_options scrcpy_options_default = { .display_buffer = 0, .audio_buffer = SC_TICK_FROM_MS(50), .audio_output_buffer = SC_TICK_FROM_MS(5), + .time_limit = 0, #ifdef HAVE_V4L2 .v4l2_device = NULL, .v4l2_buffer = 0, diff --git a/app/src/options.h b/app/src/options.h index 75f193b3..1f36ad7f 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -142,6 +142,7 @@ struct scrcpy_options { sc_tick display_buffer; sc_tick audio_buffer; sc_tick audio_output_buffer; + sc_tick time_limit; #ifdef HAVE_V4L2 const char *v4l2_device; sc_tick v4l2_buffer; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f9679ac1..fd310c46 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -35,6 +35,7 @@ #include "util/log.h" #include "util/net.h" #include "util/rand.h" +#include "util/timeout.h" #ifdef HAVE_V4L2 # include "v4l2_sink.h" #endif @@ -73,6 +74,7 @@ struct scrcpy { struct sc_hid_mouse mouse_hid; #endif }; + struct sc_timeout timeout; }; static inline void @@ -171,6 +173,9 @@ event_loop(struct scrcpy *s) { case SC_EVENT_RECORDER_ERROR: LOGE("Recorder error"); return SCRCPY_EXIT_FAILURE; + case SC_EVENT_TIME_LIMIT_REACHED: + LOGI("Time limit reached"); + return SCRCPY_EXIT_SUCCESS; case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; @@ -280,6 +285,14 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) { // 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 static uint32_t scrcpy_generate_scid() { @@ -321,6 +334,8 @@ scrcpy(struct scrcpy_options *options) { bool controller_initialized = false; bool controller_started = false; bool screen_initialized = false; + bool timeout_initialized = false; + bool timeout_started = false; 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); LOGD("quit..."); @@ -751,6 +787,10 @@ aoa_hid_end: sc_screen_hide_window(&s->screen); end: + if (timeout_started) { + sc_timeout_stop(&s->timeout); + } + // The demuxer is not stopped explicitly, because it will stop by itself on // end-of-stream #ifdef HAVE_USB @@ -786,6 +826,13 @@ end: 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 // interrupted, we can join them if (video_demuxer_started) { diff --git a/app/src/util/timeout.c b/app/src/util/timeout.c new file mode 100644 index 00000000..a1665373 --- /dev/null +++ b/app/src/util/timeout.c @@ -0,0 +1,77 @@ +#include "timeout.h" + +#include + +#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); +} diff --git a/app/src/util/timeout.h b/app/src/util/timeout.h new file mode 100644 index 00000000..ae171b86 --- /dev/null +++ b/app/src/util/timeout.h @@ -0,0 +1,43 @@ +#ifndef SC_TIMEOUT_H +#define SC_TIMEOUT_H + +#include "common.h" + +#include + +#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 diff --git a/doc/recording.md b/doc/recording.md index d0c33181..76a7efd6 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -61,3 +61,18 @@ It is also possible to disable video and audio playback separately: # 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 +```